[
  {
    "path": ".browserslistrc",
    "content": "> 1%\nlast 2 versions\nnot dead\nnot ie 11\n"
  },
  {
    "path": ".commitlintrc.js",
    "content": "export { default } from '@vben/commitlint-config';\n"
  },
  {
    "path": ".dockerignore",
    "content": "# Git\n.git\n.gitignore\n\n# IDE\n.idea\n.vscode\n*.iml\n\n# Logs\n*.log\nlogs\n\n# Node\nnode_modules\n.pnpm-store\n.turbo\n\n# Build outputs (will be rebuilt in container)\napps/*/dist\napps/*/.turbo\npackages/*/dist\npackages/*/.turbo\n\n# Test\ncoverage\n*.test.*\n*.spec.*\n\n# Docs\n*.md\n!README.md\ndocs\n\n# Misc\n.DS_Store\n.env\n.env.local\n.env.*.local\n"
  },
  {
    "path": ".editorconfig",
    "content": "root = true\n\n[*]\ncharset=utf-8\nend_of_line=lf\ninsert_final_newline=true\nindent_style=space\nindent_size=2\nmax_line_length = 100\ntrim_trailing_whitespace = true\nquote_type = single\n\n[*.{yml,yaml,json}]\nindent_style = space\nindent_size = 2\n\n[*.md]\ntrim_trailing_whitespace = false\n"
  },
  {
    "path": ".gitattributes",
    "content": "# https://docs.github.com/cn/get-started/getting-started-with-git/configuring-git-to-handle-line-endings\n\n# Automatically normalize line endings (to LF) for all text-based files.\n* text=auto eol=lf\n\n# Declare files that will always have CRLF line endings on checkout.\n*.{cmd,[cC][mM][dD]} text eol=crlf\n*.{bat,[bB][aA][tT]} text eol=crlf\n\n# Denote all files that are truly binary and should not be modified.\n*.{ico,png,jpg,jpeg,gif,webp,svg,woff,woff2} binary"
  },
  {
    "path": ".gitconfig",
    "content": "[core]\n    ignorecase = false\n"
  },
  {
    "path": ".gitignore",
    "content": "node_modules\n.DS_Store\ndist\ndist-ssr\ndist.zip\ndist.tar\ndist.war\n.nitro\n.output\n*-dist.zip\n*-dist.tar\n*-dist.war\ncoverage\n*.local\n**/.vitepress/cache\n.cache\n.turbo\n.temp\ndev-dist\n.stylelintcache\nyarn.lock\npackage-lock.json\npnpm-lock.yaml\n.VSCodeCounter\n**/backend-mock/data\n\n# local env files\n.env.local\n.env.*.local\n.eslintcache\n\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\nvite.config.mts.*\nvite.config.mjs.*\nvite.config.js.*\nvite.config.ts.*\n\n# Editor directories and files\n.idea\n.vscode\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n*.sw?\n# 排除自动生成的类型文件\napps/web-antd/types/components.d.ts\n.history\n.cursor\n.claude\n"
  },
  {
    "path": ".npmrc",
    "content": "registry = \"https://registry.npmmirror.com\"\npublic-hoist-pattern[]=lefthook\npublic-hoist-pattern[]=eslint\npublic-hoist-pattern[]=prettier\npublic-hoist-pattern[]=prettier-plugin-tailwindcss\npublic-hoist-pattern[]=stylelint\npublic-hoist-pattern[]=*postcss*\npublic-hoist-pattern[]=@commitlint/*\npublic-hoist-pattern[]=czg\n\nstrict-peer-dependencies=false\nauto-install-peers=true\ndedupe-peer-dependents=true\n"
  },
  {
    "path": ".prettierignore",
    "content": "dist\ndev-dist\n.local\n.output.js\nnode_modules\n.nvmrc\ncoverage\nCODEOWNERS\n.nitro\n.output\n\n\n**/*.svg\n**/*.sh\n\npublic\n.npmrc\n*-lock.yaml\n"
  },
  {
    "path": ".prettierrc.mjs",
    "content": "// .prettierrc.mjs 是 Prettier 代码格式化工具的配置文件。\n// 作用：\n// 统一代码风格（缩进、引号、分号等）\n// 在保存文件或提交代码时自动格式化\nexport { default } from '@vben/prettier-config';\n\n\n\n"
  },
  {
    "path": ".stylelintignore",
    "content": "dist\npublic\n__tests__\ncoverage\n"
  },
  {
    "path": "LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2026 ruoyi-ai\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\nthe Software, and to permit persons to whom the Software is furnished to do so,\nsubject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\nFOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\nCOPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\nCONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# RuoYi-AI 管理端\n\n<div align=\"center\">\n\n<img src=\"https://github.com/ageerle/ruoyi-ai/raw/main/docs/image/logo.png\" alt=\"RuoYi AI Logo\" width=\"120\" height=\"120\">\n\n### 企业级AI助手平台 - 管理后台\n\n*RuoYi-AI 的管理后台，提供系统管理、模型配置、知识库管理、流程编排等功能*\n\n**[在线体验](https://admin.pandarobot.chat)** | **[后端服务](https://github.com/ageerle/ruoyi-ai)** | **[用户端](https://github.com/ageerle/ruoyi-web)**\n\n</div>\n\n## 技术栈\n\n- **框架**: Vue 3 + Vben Admin\n- **UI组件**: element-plus-x\n- **构建工具**: Vite\n\n## Docker 部署\n\n本管理端支持两种 Docker 部署方式：\n\n### 方式一：一键启动所有服务（推荐）\n\n使用 `docker-compose-all.yaml` 可以一键启动所有服务（包括后端、管理端、用户端及依赖服务）：\n\n```bash\n# 克隆后端仓库\ngit clone https://github.com/ageerle/ruoyi-ai.git\ncd ruoyi-ai\n\n# 启动所有服务（从镜像仓库拉取预构建镜像）\ndocker-compose -f docker-compose-all.yaml up -d\n\n# 访问管理端\n# 地址: http://localhost:25666\n# 账号: admin / admin123\n```\n\n### 方式二：分步部署（源码编译）\n\n如果您需要从源码构建，请按照以下步骤操作：\n\n#### 第一步：部署后端服务\n\n```bash\n# 进入后端项目目录\ncd ruoyi-ai\n\n# 启动后端服务（源码编译构建）\ndocker-compose up -d --build\n\n# 等待后端服务启动完成\ndocker-compose logs -f backend\n```\n\n#### 第二步：部署管理端\n\n```bash\n# 进入管理端项目目录\ncd ruoyi-admin\n\n# 构建并启动管理端\ndocker-compose up -d --build\n\n# 访问管理端\n# 地址: http://localhost:5666\n```\n\n#### 第三步：部署用户端（可选）\n\n```bash\n# 进入用户端项目目录\ncd ruoyi-web\n\n# 构建并启动用户端\ndocker-compose up -d --build\n\n# 访问用户端\n# 地址: http://localhost:5137\n```\n\n### 服务端口说明\n\n| 服务 | 端口 | 说明 |\n|------|------|------|\n| 管理端 | 5666 | 管理后台访问地址 |\n| 用户端 | 5137 | 用户前端访问地址 |\n| 后端服务 | 6039 | 后端 API 服务 |\n| MySQL | 23306 | 数据库服务 |\n| Redis | 6379 | 缓存服务 |\n| Weaviate | 28080 | 向量数据库 |\n| MinIO | 9000/9090 | 对象存储 |\n\n### 镜像仓库\n\n所有镜像托管在阿里云容器镜像服务：\n\n```\ncrpi-31mraxd99y2gqdgr.cn-beijing.personal.cr.aliyuncs.com/ruoyi_ai\n```\n\n可用镜像：\n- `mysql:v3` - MySQL 数据库（包含初始化 SQL）\n- `redis:6.2` - Redis 缓存\n- `weaviate:1.30.0` - 向量数据库\n- `minio:latest` - 对象存储\n- `ruoyi-ai-backend:latest` - 后端服务\n- `ruoyi-ai-admin:latest` - 管理端前端\n- `ruoyi-ai-web:latest` - 用户端前端\n\n## 本地开发\n\n```bash\n# 安装依赖\npnpm install\n\n# 启动开发服务器\npnpm dev\n\n# 构建生产版本\npnpm build\n```\n\n## 常见问题\n\n**Q: 管理端无法连接后端服务？**\n\nA: 请确保后端服务已启动，并检查环境变量 `UPSTREAM_HOST` 配置是否正确。\n\n**Q: 一键启动和分步部署有什么区别？**\n\nA: 一键启动使用预构建的镜像，部署速度快；分步部署从源码编译，适合需要自定义修改的场景。\n\n## 开源协议\n\n本项目采用 **MIT 开源协议**，详情请查看 [LICENSE](license) 文件。\n\n---\n\n<div align=\"center\">\n\n**[⭐ 点个Star支持一下](https://github.com/ageerle/ruoyi-admin)** • **[Fork 开始贡献](https://github.com/ageerle/ruoyi-admin/fork)**\n\n*用 ❤️ 打造，由 RuoYi AI 开源社区维护*\n\n</div>\n"
  },
  {
    "path": "apps/web-antd/Dockerfile",
    "content": "# 构建阶段\nFROM node:22-alpine AS builder\n\n# 接收后端 API 地址（docker-compose 传入）\nARG VITE_GLOB_API_URL=http://localhost:6039\nENV VITE_GLOB_API_URL=${VITE_GLOB_API_URL}\n\n# 设置环境变量 - 优化资源使用，增加内存限制\nENV PNPM_HOME=\"/pnpm\"\nENV PATH=\"$PNPM_HOME:$PATH\"\nENV NODE_OPTIONS=--max-old-space-size=16384\n\n# 安装 pnpm 和设置时区\nRUN npm i -g corepack && corepack enable && \\\n    apk add --no-cache tzdata && \\\n    cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \\\n    echo \"Asia/Shanghai\" > /etc/timezone\n\n# 设置工作目录\nWORKDIR /app\n\n# 先复制 package 文件（优化 Docker 缓存）\nCOPY package.json pnpm-workspace.yaml pnpm-lock.yaml turbo.json .npmrc ./\n\n# 复制 internal 目录（包含 @vben/vite-config, @vben/tsconfig 等）\nCOPY internal ./internal\n\n# 复制 scripts 目录（turbo 依赖）\nCOPY scripts ./scripts\n\n# 复制 packages 目录（workspace 依赖）\nCOPY packages ./packages\n\n# 复制 web-antd 应用\nCOPY apps/web-antd ./apps/web-antd\n\n# 安装依赖（使用 workspace）\nRUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile\n\n# 构建 workspace 依赖包（使用 turbo）\nRUN pnpm run build --filter=\"./packages/**\"\n\n# 构建 web-antd 应用（直接构建，避免 turbo 过滤器问题）\nWORKDIR /app/apps/web-antd\nRUN pnpm run build:prod\nWORKDIR /app\n\n# 生产阶段 - 使用 nginx 部署\nFROM nginx:stable-alpine AS production\n\n# 安装 tzdata 并设置时区\nRUN apk add --no-cache tzdata && \\\n    cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \\\n    echo \"Asia/Shanghai\" > /etc/timezone\n\n# 配置 nginx 支持 mjs\nRUN echo \"types { application/javascript js mjs; }\" > /etc/nginx/conf.d/mjs.conf \\\n    && rm -rf /etc/nginx/conf.d/default.conf\n\n# 从构建阶段复制构建产物\nCOPY --from=builder /app/apps/web-antd/dist /usr/share/nginx/html\n\n# 复制 nginx 配置文件模板\nCOPY apps/web-antd/nginx.conf /etc/nginx/nginx.conf.template\n\n# 复制启动脚本\nCOPY apps/web-antd/docker-entrypoint.sh /docker-entrypoint.sh\nRUN chmod +x /docker-entrypoint.sh\n\n# 暴露端口\nEXPOSE 5666\n\n# 设置默认后端地址（可被 docker-compose environment 覆盖）\nENV UPSTREAM_HOST=127.0.0.1:6039\n\n# 启动脚本（处理环境变量并启动 nginx）\nCMD [\"/docker-entrypoint.sh\"]\n"
  },
  {
    "path": "apps/web-antd/docker-compose.yml",
    "content": "services:\n  # ==================== RuoYi-AI 前端服务 ====================\n  frontend:\n    image: ruoyi-ai-admin:latest\n    build:\n      context: ../..\n      dockerfile: apps/web-antd/Dockerfile\n      tags:\n        - ruoyi-ai-admin:latest\n    container_name: ruoyi-ai-admin\n    restart: always\n    ports:\n      - \"5666:5666\"\n    environment:\n      # 后端 API 地址 - 运行时动态配置（无需重新构建镜像）\n      # 使用后端容器名和内部端口（容器内端口是 6039）\n      UPSTREAM_HOST: ${UPSTREAM_HOST:-ruoyi-ai-backend:6039}\n    # 资源限制 - 防止 CPU 和内存耗尽\n    deploy:\n      resources:\n        limits:\n          cpus: '2'          # 限制最多使用 2 个 CPU 核心\n          memory: 3G         # 限制内存使用为 3GB\n        reservations:\n          cpus: '1'          # 保留 1 个 CPU 核心\n          memory: 1G         # 保留 1GB 内存\n    networks:\n      - ruoyi-net\n\n# ==================== 网络配置 ====================\nnetworks:\n  # 使用后端服务的网络，实现容器间通信\n  ruoyi-net:\n    name: ruoyi-ai_ruoyi-net\n    external: true\n"
  },
  {
    "path": "apps/web-antd/docker-entrypoint.sh",
    "content": "#!/bin/sh\n# Nginx 启动脚本 - 支持运行时环境变量\n\n# 使用 envsubst 替换 nginx.conf 中的环境变量\nenvsubst '${UPSTREAM_HOST}' < /etc/nginx/nginx.conf.template > /etc/nginx/nginx.conf\n\n# 启动 nginx\nexec nginx -g 'daemon off;'\n"
  },
  {
    "path": "apps/web-antd/index.html",
    "content": "<!doctype html>\n<html lang=\"zh\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge,chrome=1\" />\n    <meta name=\"renderer\" content=\"webkit\" />\n    <meta name=\"description\" content=\"A Modern Back-end Management System\" />\n    <meta name=\"keywords\" content=\"Vben Admin Vue3 Vite\" />\n    <meta name=\"author\" content=\"Vben\" />\n    <meta\n      name=\"viewport\"\n      content=\"width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=0\"\n    />\n    <!-- 由 vite 注入 VITE_APP_TITLE 变量，在 .env 文件内配置 -->\n    <title><%= VITE_APP_TITLE %></title>\n    <link rel=\"icon\" href=\"/favicon.ico\" />\n  </head>\n  <body>\n    <div id=\"app\"></div>\n    <script type=\"module\" src=\"/src/main.ts\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "apps/web-antd/nginx.conf",
    "content": "worker_processes 1;\n\nevents {\n  worker_connections 1024;\n}\n\nhttp {\n  include /etc/nginx/mime.types;\n  default_type application/octet-stream;\n\n  sendfile on;\n  tcp_nopush on;\n  tcp_nodelay on;\n  keepalive_timeout 65;\n\n  # Gzip 压缩\n  gzip on;\n  gzip_vary on;\n  gzip_min_length 1024;\n  gzip_types text/plain text/css text/xml text/javascript application/javascript application/json application/xml+rss application/rss+xml font/truetype font/opentype application/vnd.ms-fontobject image/svg+xml;\n\n  # 后端 API 地址（运行时环境变量）\n  upstream backend {\n    server ${UPSTREAM_HOST};\n  }\n\n  server {\n    listen 5666;\n    server_name localhost;\n\n    # API 代理到后端服务（去除 /api 前缀后转发）\n    location /api/ {\n      rewrite ^/api/(.*)$ /$1 break;\n      proxy_pass http://backend;\n      proxy_set_header Host $host;\n      proxy_set_header X-Real-IP $remote_addr;\n      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n      proxy_set_header X-Forwarded-Proto $scheme;\n\n      # 支持WebSocket\n      proxy_http_version 1.1;\n      proxy_set_header Upgrade $http_upgrade;\n      proxy_set_header Connection \"upgrade\";\n    }\n\n    location / {\n      root /usr/share/nginx/html;\n      try_files $uri $uri/ /index.html;\n      index index.html;\n    }\n\n    error_page 500 502 503 504 /50x.html;\n\n    location = /50x.html {\n        root /usr/share/nginx/html;\n    }\n  }\n}\n"
  },
  {
    "path": "apps/web-antd/package.json",
    "content": "{\n  \"name\": \"@vben/web-antd\",\n  \"version\": \"1.5.2\",\n  \"homepage\": \"https://vben.pro\",\n  \"bugs\": \"https://github.com/vbenjs/vue-vben-admin/issues\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/vbenjs/vue-vben-admin.git\",\n    \"directory\": \"apps/web-antd\"\n  },\n  \"license\": \"MIT\",\n  \"author\": {\n    \"name\": \"vben\",\n    \"email\": \"ann.vben@gmail.com\",\n    \"url\": \"https://github.com/anncwb\"\n  },\n  \"type\": \"module\",\n  \"scripts\": {\n    \"build:prod\": \"pnpm vite build\",\n    \"build:test\": \"pnpm vite build --mode test\",\n    \"build:analyze\": \"pnpm vite build --mode analyze\",\n    \"dev\": \"pnpm vite --mode development\",\n    \"preview\": \"vite preview\",\n    \"typecheck\": \"vue-tsc --noEmit --skipLibCheck\"\n  },\n  \"imports\": {\n    \"#/*\": \"./src/*\"\n  },\n  \"dependencies\": {\n    \"@ant-design/icons-vue\": \"^7.0.1\",\n    \"@antv/g6\": \"^5.0.51\",\n    \"@tinymce/tinymce-vue\": \"^6.0.1\",\n    \"@vben/access\": \"workspace:*\",\n    \"@vben/common-ui\": \"workspace:*\",\n    \"@vben/constants\": \"workspace:*\",\n    \"@vben/hooks\": \"workspace:*\",\n    \"@vben/icons\": \"workspace:*\",\n    \"@vben/layouts\": \"workspace:*\",\n    \"@vben/locales\": \"workspace:*\",\n    \"@vben/plugins\": \"workspace:*\",\n    \"@vben/preferences\": \"workspace:*\",\n    \"@vben/request\": \"workspace:*\",\n    \"@vben/stores\": \"workspace:*\",\n    \"@vben/styles\": \"workspace:*\",\n    \"@vben/types\": \"workspace:*\",\n    \"@vben/utils\": \"workspace:*\",\n    \"@vue-flow/background\": \"^1.3.2\",\n    \"@vue-flow/core\": \"^1.46.5\",\n    \"@vueuse/core\": \"catalog:\",\n    \"ant-design-vue\": \"catalog:\",\n    \"cropperjs\": \"^1.6.2\",\n    \"dayjs\": \"catalog:\",\n    \"echarts\": \"^5.5.1\",\n    \"lodash-es\": \"^4.17.21\",\n    \"pinia\": \"catalog:\",\n    \"tinymce\": \"7.9.1\",\n    \"unplugin-vue-components\": \"^0.27.3\",\n    \"uuid\": \"^9.0.0\",\n    \"vue\": \"catalog:\",\n    \"vue-router\": \"catalog:\",\n    \"vue3-colorpicker\": \"^2.3.0\"\n  },\n  \"devDependencies\": {\n    \"@types/lodash-es\": \"^4.17.12\"\n  }\n}\n"
  },
  {
    "path": "apps/web-antd/postcss.config.mjs",
    "content": "export { default } from '@vben/tailwind-config/postcss';\n"
  },
  {
    "path": "apps/web-antd/public/tinymce/langs/README.md",
    "content": "This is where language files should be placed.\n\nPlease DO NOT translate these directly, use this service instead: https://crowdin.com/project/tinymce\n"
  },
  {
    "path": "apps/web-antd/public/tinymce/langs/zh_CN.js",
    "content": "tinymce.addI18n(\"zh_CN\",{\"#\":\"#\",\"Accessibility\":\"\\u8f85\\u52a9\\u529f\\u80fd\",\"Accordion\":\"\",\"Accordion body...\":\"\",\"Accordion summary...\":\"\",\"Action\":\"\\u52a8\\u4f5c\",\"Activity\":\"\\u6d3b\\u52a8\",\"Address\":\"\\u5730\\u5740\",\"Advanced\":\"\\u9ad8\\u7ea7\",\"Align\":\"\\u5bf9\\u9f50\",\"Align center\":\"\\u5c45\\u4e2d\\u5bf9\\u9f50\",\"Align left\":\"\\u5de6\\u5bf9\\u9f50\",\"Align right\":\"\\u53f3\\u5bf9\\u9f50\",\"Alignment\":\"\\u5bf9\\u9f50\",\"Alignment {0}\":\"\",\"All\":\"\\u5168\\u90e8\",\"Alternative description\":\"\\u66ff\\u4ee3\\u63cf\\u8ff0\",\"Alternative source\":\"\\u955c\\u50cf\",\"Alternative source URL\":\"\\u66ff\\u4ee3\\u6765\\u6e90\\u7f51\\u5740\",\"Anchor\":\"\\u951a\\u70b9\",\"Anchor...\":\"\\u951a\\u70b9...\",\"Anchors\":\"\\u951a\\u70b9\",\"Animals and Nature\":\"\\u52a8\\u7269\\u548c\\u81ea\\u7136\",\"Arrows\":\"\\u7bad\\u5934\",\"B\":\"B\",\"Background color\":\"\\u80cc\\u666f\\u989c\\u8272\",\"Background color {0}\":\"\",\"Black\":\"\\u9ed1\\u8272\",\"Block\":\"\\u5757\",\"Block {0}\":\"\",\"Blockquote\":\"\\u5f15\\u6587\\u533a\\u5757\",\"Blocks\":\"\\u6837\\u5f0f\",\"Blue\":\"\\u84dd\\u8272\",\"Blue component\":\"\\u767d\\u8272\\u90e8\\u5206\",\"Body\":\"\\u8868\\u4f53\",\"Bold\":\"\\u7c97\\u4f53\",\"Border\":\"\\u6846\\u7ebf\",\"Border color\":\"\\u6846\\u7ebf\\u989c\\u8272\",\"Border style\":\"\\u8fb9\\u6846\\u6837\\u5f0f\",\"Border width\":\"\\u8fb9\\u6846\\u5bbd\\u5ea6\",\"Bottom\":\"\\u4e0b\\u65b9\\u5bf9\\u9f50\",\"Browse files\":\"\",\"Browse for an image\":\"\\u6d4f\\u89c8\\u56fe\\u50cf\",\"Browse links\":\"\",\"Bullet list\":\"\\u65e0\\u5e8f\\u5217\\u8868\",\"Cancel\":\"\\u53d6\\u6d88\",\"Caption\":\"\\u6807\\u9898\",\"Cell\":\"\\u5355\\u5143\\u683c\",\"Cell padding\":\"\\u5355\\u5143\\u683c\\u5185\\u8fb9\\u8ddd\",\"Cell properties\":\"\\u5355\\u5143\\u683c\\u5c5e\\u6027\",\"Cell spacing\":\"\\u5355\\u5143\\u683c\\u5916\\u95f4\\u8ddd\",\"Cell styles\":\"\\u5355\\u5143\\u683c\\u6837\\u5f0f\",\"Cell type\":\"\\u50a8\\u5b58\\u683c\\u522b\",\"Center\":\"\\u5c45\\u4e2d\",\"Characters\":\"\\u5b57\\u7b26\",\"Characters (no spaces)\":\"\\u5b57\\u7b26(\\u65e0\\u7a7a\\u683c)\",\"Circle\":\"\\u7a7a\\u5fc3\\u5706\",\"Class\":\"\\u7c7b\\u578b\",\"Clear formatting\":\"\\u6e05\\u9664\\u683c\\u5f0f\",\"Close\":\"\\u5173\\u95ed\",\"Code\":\"\\u4ee3\\u7801\",\"Code sample...\":\"\\u793a\\u4f8b\\u4ee3\\u7801...\",\"Code view\":\"\\u4ee3\\u7801\\u89c6\\u56fe\",\"Color Picker\":\"\\u9009\\u8272\\u5668\",\"Color swatch\":\"\\u989c\\u8272\\u6837\\u672c\",\"Cols\":\"\\u5217\",\"Column\":\"\\u5217\",\"Column clipboard actions\":\"\\u5217\\u526a\\u8d34\\u677f\\u64cd\\u4f5c\",\"Column group\":\"\\u5217\\u7ec4\",\"Column header\":\"\\u5217\\u6807\\u9898\",\"Constrain proportions\":\"\\u4fdd\\u6301\\u6bd4\\u4f8b\",\"Copy\":\"\\u590d\\u5236\",\"Copy column\":\"\\u590d\\u5236\\u5217\",\"Copy row\":\"\\u590d\\u5236\\u884c\",\"Could not find the specified string.\":\"\\u672a\\u627e\\u5230\\u641c\\u7d22\\u5185\\u5bb9\\u3002\",\"Could not load emojis\":\"\\u65e0\\u6cd5\\u52a0\\u8f7dEmojis\",\"Count\":\"\\u8ba1\\u6570\",\"Currency\":\"\\u8d27\\u5e01\",\"Current window\":\"\\u5f53\\u524d\\u7a97\\u53e3\",\"Custom color\":\"\\u81ea\\u5b9a\\u4e49\\u989c\\u8272\",\"Custom...\":\"\\u81ea\\u5b9a\\u4e49......\",\"Cut\":\"\\u526a\\u5207\",\"Cut column\":\"\\u526a\\u5207\\u5217\",\"Cut row\":\"\\u526a\\u5207\\u884c\",\"Dark Blue\":\"\\u6df1\\u84dd\\u8272\",\"Dark Gray\":\"\\u6df1\\u7070\\u8272\",\"Dark Green\":\"\\u6df1\\u7eff\\u8272\",\"Dark Orange\":\"\\u6df1\\u6a59\\u8272\",\"Dark Purple\":\"\\u6df1\\u7d2b\\u8272\",\"Dark Red\":\"\\u6df1\\u7ea2\\u8272\",\"Dark Turquoise\":\"\\u6df1\\u84dd\\u7eff\\u8272\",\"Dark Yellow\":\"\\u6697\\u9ec4\\u8272\",\"Dashed\":\"\\u865a\\u7ebf\",\"Date/time\":\"\\u65e5\\u671f/\\u65f6\\u95f4\",\"Decrease indent\":\"\\u51cf\\u5c11\\u7f29\\u8fdb\",\"Default\":\"\\u9884\\u8bbe\",\"Delete accordion\":\"\",\"Delete column\":\"\\u5220\\u9664\\u5217\",\"Delete row\":\"\\u5220\\u9664\\u884c\",\"Delete table\":\"\\u5220\\u9664\\u8868\\u683c\",\"Dimensions\":\"\\u5c3a\\u5bf8\",\"Disc\":\"\\u5b9e\\u5fc3\\u5706\",\"Div\":\"Div\",\"Document\":\"\\u6587\\u6863\",\"Dotted\":\"\\u865a\\u7ebf\",\"Double\":\"\\u53cc\\u7cbe\\u5ea6\",\"Drop an image here\":\"\\u62d6\\u653e\\u4e00\\u5f20\\u56fe\\u50cf\\u81f3\\u6b64\",\"Dropped file type is not supported\":\"\\u6b64\\u6587\\u4ef6\\u7c7b\\u578b\\u4e0d\\u652f\\u6301\\u62d6\\u653e\",\"Edit\":\"\\u7f16\\u8f91\",\"Embed\":\"\\u5185\\u5d4c\",\"Emojis\":\"Emojis\",\"Emojis...\":\"Emojis...\",\"Error\":\"\\u9519\\u8bef\",\"Error: Form submit field collision.\":\"\\u9519\\u8bef: \\u8868\\u5355\\u63d0\\u4ea4\\u5b57\\u6bb5\\u51b2\\u7a81\\u3002\",\"Error: No form element found.\":\"\\u9519\\u8bef: \\u6ca1\\u6709\\u8868\\u5355\\u63a7\\u4ef6\\u3002\",\"Extended Latin\":\"\\u62c9\\u4e01\\u8bed\\u6269\\u5145\",\"Failed to initialize plugin: {0}\":\"\\u63d2\\u4ef6\\u521d\\u59cb\\u5316\\u5931\\u8d25: {0}\",\"Failed to load plugin url: {0}\":\"\\u63d2\\u4ef6\\u52a0\\u8f7d\\u5931\\u8d25 \\u94fe\\u63a5: {0}\",\"Failed to load plugin: {0} from url {1}\":\"\\u63d2\\u4ef6\\u52a0\\u8f7d\\u5931\\u8d25: {0} \\u6765\\u81ea\\u94fe\\u63a5 {1}\",\"Failed to upload image: {0}\":\"\\u56fe\\u7247\\u4e0a\\u4f20\\u5931\\u8d25: {0}\",\"File\":\"\\u6587\\u4ef6\",\"Find\":\"\\u5bfb\\u627e\",\"Find (if searchreplace plugin activated)\":\"\\u67e5\\u627e(\\u5982\\u679c\\u67e5\\u627e\\u66ff\\u6362\\u63d2\\u4ef6\\u5df2\\u6fc0\\u6d3b)\",\"Find and Replace\":\"\\u67e5\\u627e\\u548c\\u66ff\\u6362\",\"Find and replace...\":\"\\u67e5\\u627e\\u5e76\\u66ff\\u6362...\",\"Find in selection\":\"\\u5728\\u9009\\u533a\\u4e2d\\u67e5\\u627e\",\"Find whole words only\":\"\\u5168\\u5b57\\u5339\\u914d\",\"Flags\":\"\\u65d7\\u5e1c\",\"Focus to contextual toolbar\":\"\\u79fb\\u52a8\\u7126\\u70b9\\u5230\\u4e0a\\u4e0b\\u6587\\u83dc\\u5355\",\"Focus to element path\":\"\\u79fb\\u52a8\\u7126\\u70b9\\u5230\\u5143\\u7d20\\u8def\\u5f84\",\"Focus to menubar\":\"\\u79fb\\u52a8\\u7126\\u70b9\\u5230\\u83dc\\u5355\\u680f\",\"Focus to toolbar\":\"\\u79fb\\u52a8\\u7126\\u70b9\\u5230\\u5de5\\u5177\\u680f\",\"Font\":\"\\u5b57\\u4f53\",\"Font size {0}\":\"\",\"Font sizes\":\"\\u5b57\\u4f53\\u5927\\u5c0f\",\"Font {0}\":\"\",\"Fonts\":\"\\u5b57\\u4f53\",\"Food and Drink\":\"\\u98df\\u7269\\u548c\\u996e\\u54c1\",\"Footer\":\"\\u8868\\u5c3e\",\"Format\":\"\\u683c\\u5f0f\",\"Format {0}\":\"\",\"Formats\":\"\\u683c\\u5f0f\",\"Fullscreen\":\"\\u5168\\u5c4f\",\"G\":\"G\",\"General\":\"\\u4e00\\u822c\",\"Gray\":\"\\u7070\\u8272\",\"Green\":\"\\u7eff\\u8272\",\"Green component\":\"\\u7eff\\u8272\\u90e8\\u5206\",\"Groove\":\"\\u51f9\\u69fd\",\"Handy Shortcuts\":\"\\u5feb\\u6377\\u952e\",\"Header\":\"\\u8868\\u5934\",\"Header cell\":\"\\u8868\\u5934\\u5355\\u5143\\u683c\",\"Heading 1\":\"\\u4e00\\u7ea7\\u6807\\u9898\",\"Heading 2\":\"\\u4e8c\\u7ea7\\u6807\\u9898\",\"Heading 3\":\"\\u4e09\\u7ea7\\u6807\\u9898\",\"Heading 4\":\"\\u56db\\u7ea7\\u6807\\u9898\",\"Heading 5\":\"\\u4e94\\u7ea7\\u6807\\u9898\",\"Heading 6\":\"\\u516d\\u7ea7\\u6807\\u9898\",\"Headings\":\"\\u6807\\u9898\",\"Height\":\"\\u9ad8\\u5ea6\",\"Help\":\"\\u5e2e\\u52a9\",\"Hex color code\":\"\\u5341\\u516d\\u8fdb\\u5236\\u989c\\u8272\\u4ee3\\u7801\",\"Hidden\":\"\\u9690\\u85cf\",\"Horizontal align\":\"\\u6c34\\u5e73\\u5bf9\\u9f50\",\"Horizontal line\":\"\\u6c34\\u5e73\\u5206\\u5272\\u7ebf\",\"Horizontal space\":\"\\u6c34\\u5e73\\u95f4\\u8ddd\",\"ID\":\"ID\",\"ID should start with a letter, followed only by letters, numbers, dashes, dots, colons or underscores.\":\"ID\\u5e94\\u8be5\\u4ee5\\u82f1\\u6587\\u5b57\\u6bcd\\u5f00\\u5934\\uff0c\\u540e\\u9762\\u53ea\\u80fd\\u6709\\u82f1\\u6587\\u5b57\\u6bcd\\u3001\\u6570\\u5b57\\u3001\\u7834\\u6298\\u53f7\\u3001\\u70b9\\u3001\\u5192\\u53f7\\u6216\\u4e0b\\u5212\\u7ebf\\u3002\",\"Image is decorative\":\"\\u56fe\\u50cf\\u662f\\u88c5\\u9970\\u6027\\u7684\",\"Image list\":\"\\u56fe\\u7247\\u6e05\\u5355\",\"Image title\":\"\\u56fe\\u7247\\u6807\\u9898\",\"Image...\":\"\\u56fe\\u7247...\",\"ImageProxy HTTP error: Could not find Image Proxy\":\"\\u56fe\\u7247\\u4ee3\\u7406\\u8bf7\\u6c42\\u9519\\u8bef\\uff1a\\u65e0\\u6cd5\\u627e\\u5230\\u56fe\\u7247\\u4ee3\\u7406\",\"ImageProxy HTTP error: Incorrect Image Proxy URL\":\"\\u56fe\\u7247\\u4ee3\\u7406\\u8bf7\\u6c42\\u9519\\u8bef\\uff1a\\u56fe\\u7247\\u4ee3\\u7406\\u5730\\u5740\\u9519\\u8bef\",\"ImageProxy HTTP error: Rejected request\":\"\\u56fe\\u7247\\u4ee3\\u7406\\u8bf7\\u6c42\\u9519\\u8bef\\uff1a\\u8bf7\\u6c42\\u88ab\\u62d2\\u7edd\",\"ImageProxy HTTP error: Unknown ImageProxy error\":\"\\u56fe\\u7247\\u4ee3\\u7406\\u8bf7\\u6c42\\u9519\\u8bef\\uff1a\\u672a\\u77e5\\u7684\\u56fe\\u7247\\u4ee3\\u7406\\u9519\\u8bef\",\"Increase indent\":\"\\u589e\\u52a0\\u7f29\\u8fdb\",\"Inline\":\"\\u6587\\u672c\",\"Insert\":\"\\u63d2\\u5165\",\"Insert Template\":\"\\u63d2\\u5165\\u6a21\\u677f\",\"Insert accordion\":\"\",\"Insert column after\":\"\\u5728\\u53f3\\u4fa7\\u63d2\\u5165\\u5217\",\"Insert column before\":\"\\u5728\\u5de6\\u4fa7\\u63d2\\u5165\\u5217\",\"Insert date/time\":\"\\u63d2\\u5165\\u65e5\\u671f/\\u65f6\\u95f4\",\"Insert image\":\"\\u63d2\\u5165\\u56fe\\u7247\",\"Insert link (if link plugin activated)\":\"\\u63d2\\u5165\\u94fe\\u63a5 (\\u5982\\u679c\\u94fe\\u63a5\\u63d2\\u4ef6\\u5df2\\u6fc0\\u6d3b)\",\"Insert row after\":\"\\u5728\\u4e0b\\u65b9\\u63d2\\u5165\\u884c\",\"Insert row before\":\"\\u5728\\u4e0a\\u65b9\\u63d2\\u5165\\u884c\",\"Insert table\":\"\\u63d2\\u5165\\u8868\\u683c\",\"Insert template...\":\"\\u63d2\\u5165\\u6a21\\u677f...\",\"Insert video\":\"\\u63d2\\u5165\\u89c6\\u9891\",\"Insert/Edit code sample\":\"\\u63d2\\u5165/\\u7f16\\u8f91\\u4ee3\\u7801\\u793a\\u4f8b\",\"Insert/edit image\":\"\\u63d2\\u5165/\\u7f16\\u8f91\\u56fe\\u7247\",\"Insert/edit link\":\"\\u63d2\\u5165/\\u7f16\\u8f91\\u94fe\\u63a5\",\"Insert/edit media\":\"\\u63d2\\u5165/\\u7f16\\u8f91\\u5a92\\u4f53\",\"Insert/edit video\":\"\\u63d2\\u5165/\\u7f16\\u8f91\\u89c6\\u9891\",\"Inset\":\"\\u5d4c\\u5165\",\"Invalid hex color code: {0}\":\"\\u5341\\u516d\\u8fdb\\u5236\\u989c\\u8272\\u4ee3\\u7801\\u65e0\\u6548\\uff1a {0}\",\"Invalid input\":\"\\u65e0\\u6548\\u8f93\\u5165\",\"Italic\":\"\\u659c\\u4f53\",\"Justify\":\"\\u4e24\\u7aef\\u5bf9\\u9f50\",\"Keyboard Navigation\":\"\\u952e\\u76d8\\u6307\\u5f15\",\"Language\":\"\\u8bed\\u8a00\",\"Learn more...\":\"\\u4e86\\u89e3\\u66f4\\u591a...\",\"Left\":\"\\u5de6\",\"Left to right\":\"\\u7531\\u5de6\\u5230\\u53f3\",\"Light Blue\":\"\\u6d45\\u84dd\\u8272\",\"Light Gray\":\"\\u6d45\\u7070\\u8272\",\"Light Green\":\"\\u6d45\\u7eff\\u8272\",\"Light Purple\":\"\\u6d45\\u7d2b\\u8272\",\"Light Red\":\"\\u6d45\\u7ea2\\u8272\",\"Light Yellow\":\"\\u6d45\\u9ec4\\u8272\",\"Line height\":\"\\u884c\\u9ad8\",\"Link list\":\"\\u94fe\\u63a5\\u6e05\\u5355\",\"Link...\":\"\\u94fe\\u63a5...\",\"List Properties\":\"\\u5217\\u8868\\u5c5e\\u6027\",\"List properties...\":\"\\u6807\\u9898\\u5b57\\u4f53\\u5c5e\\u6027\",\"Loading emojis...\":\"\\u6b63\\u5728\\u52a0\\u8f7dEmojis...\",\"Loading...\":\"\\u52a0\\u8f7d\\u4e2d...\",\"Lower Alpha\":\"\\u5c0f\\u5199\\u82f1\\u6587\\u5b57\\u6bcd\",\"Lower Greek\":\"\\u5c0f\\u5199\\u5e0c\\u814a\\u5b57\\u6bcd\",\"Lower Roman\":\"\\u5c0f\\u5199\\u7f57\\u9a6c\\u6570\\u5b57\",\"Match case\":\"\\u5927\\u5c0f\\u5199\\u5339\\u914d\",\"Mathematical\":\"\\u6570\\u5b66\",\"Media poster (Image URL)\":\"\\u5c01\\u9762(\\u56fe\\u7247\\u5730\\u5740)\",\"Media...\":\"\\u591a\\u5a92\\u4f53...\",\"Medium Blue\":\"\\u4e2d\\u84dd\\u8272\",\"Medium Gray\":\"\\u4e2d\\u7070\\u8272\",\"Medium Purple\":\"\\u4e2d\\u7d2b\\u8272\",\"Merge cells\":\"\\u5408\\u5e76\\u5355\\u5143\\u683c\",\"Middle\":\"\\u5c45\\u4e2d\\u5bf9\\u9f50\",\"Midnight Blue\":\"\\u6df1\\u84dd\\u8272\",\"More...\":\"\\u66f4\\u591a...\",\"Name\":\"\\u540d\\u79f0\",\"Navy Blue\":\"\\u6d77\\u519b\\u84dd\",\"New document\":\"\\u65b0\\u5efa\\u6587\\u6863\",\"New window\":\"\\u65b0\\u7a97\\u53e3\",\"Next\":\"\\u4e0b\\u4e00\\u4e2a\",\"No\":\"\\u5426\",\"No alignment\":\"\\u672a\\u5bf9\\u9f50\",\"No color\":\"\\u65e0\",\"Nonbreaking space\":\"\\u4e0d\\u95f4\\u65ad\\u7a7a\\u683c\",\"None\":\"\\u65e0\",\"Numbered list\":\"\\u6709\\u5e8f\\u5217\\u8868\",\"OR\":\"\\u6216\",\"Objects\":\"\\u7269\\u4ef6\",\"Ok\":\"\\u786e\\u5b9a\",\"Open help dialog\":\"\\u6253\\u5f00\\u5e2e\\u52a9\\u5bf9\\u8bdd\\u6846\",\"Open link\":\"\\u6253\\u5f00\\u94fe\\u63a5\",\"Open link in...\":\"\\u94fe\\u63a5\\u6253\\u5f00\\u4f4d\\u7f6e...\",\"Open popup menu for split buttons\":\"\\u6253\\u5f00\\u5f39\\u51fa\\u5f0f\\u83dc\\u5355\\uff0c\\u7528\\u4e8e\\u62c6\\u5206\\u6309\\u94ae\",\"Orange\":\"\\u6a59\\u8272\",\"Outset\":\"\\u5916\\u7f6e\",\"Page break\":\"\\u5206\\u9875\\u7b26\",\"Paragraph\":\"\\u6bb5\\u843d\",\"Paste\":\"\\u7c98\\u8d34\",\"Paste as text\":\"\\u7c98\\u8d34\\u4e3a\\u6587\\u672c\",\"Paste column after\":\"\\u7c98\\u8d34\\u540e\\u9762\\u7684\\u5217\",\"Paste column before\":\"\\u7c98\\u8d34\\u6b64\\u5217\\u524d\",\"Paste is now in plain text mode. Contents will now be pasted as plain text until you toggle this option off.\":\"\\u5f53\\u524d\\u4e3a\\u7eaf\\u6587\\u672c\\u7c98\\u8d34\\u6a21\\u5f0f\\uff0c\\u518d\\u6b21\\u70b9\\u51fb\\u53ef\\u4ee5\\u56de\\u5230\\u666e\\u901a\\u7c98\\u8d34\\u6a21\\u5f0f\\u3002\",\"Paste or type a link\":\"\\u7c98\\u8d34\\u6216\\u8f93\\u5165\\u94fe\\u63a5\",\"Paste row after\":\"\\u7c98\\u8d34\\u884c\\u5230\\u4e0b\\u65b9\",\"Paste row before\":\"\\u7c98\\u8d34\\u884c\\u5230\\u4e0a\\u65b9\",\"Paste your embed code below:\":\"\\u5c06\\u5185\\u5d4c\\u4ee3\\u7801\\u7c98\\u8d34\\u5728\\u4e0b\\u9762:\",\"People\":\"\\u4eba\\u7c7b\",\"Plugins\":\"\\u63d2\\u4ef6\",\"Plugins installed ({0}):\":\"\\u5df2\\u5b89\\u88c5\\u63d2\\u4ef6 ({0}):\",\"Powered by {0}\":\"\\u7531{0}\\u9a71\\u52a8\",\"Pre\":\"\\u524d\\u8a00\",\"Preferences\":\"\\u9996\\u9009\\u9879\",\"Preformatted\":\"\\u9884\\u5148\\u683c\\u5f0f\\u5316\\u7684\",\"Premium plugins:\":\"\\u4f18\\u79c0\\u63d2\\u4ef6\\uff1a\",\"Press the Up and Down arrow keys to resize the editor.\":\"\",\"Press the arrow keys to resize the editor.\":\"\",\"Press {0} for help\":\"\",\"Preview\":\"\\u9884\\u89c8\",\"Previous\":\"\\u4e0a\\u4e00\\u4e2a\",\"Print\":\"\\u6253\\u5370\",\"Print...\":\"\\u6253\\u5370...\",\"Purple\":\"\\u7d2b\\u8272\",\"Quotations\":\"\\u5f15\\u7528\",\"R\":\"R\",\"Range 0 to 255\":\"\\u8303\\u56f40\\u81f3255\",\"Red\":\"\\u7ea2\\u8272\",\"Red component\":\"\\u7ea2\\u8272\\u90e8\\u5206\",\"Redo\":\"\\u91cd\\u505a\",\"Remove\":\"\\u79fb\\u9664\",\"Remove color\":\"\\u79fb\\u9664\\u989c\\u8272\",\"Remove link\":\"\\u79fb\\u9664\\u94fe\\u63a5\",\"Replace\":\"\\u66ff\\u6362\",\"Replace all\":\"\\u66ff\\u6362\\u5168\\u90e8\",\"Replace with\":\"\\u66ff\\u6362\\u4e3a\",\"Resize\":\"\\u8c03\\u6574\\u5927\\u5c0f\",\"Restore last draft\":\"\\u6062\\u590d\\u4e0a\\u6b21\\u7684\\u8349\\u7a3f\",\"Reveal or hide additional toolbar items\":\"\",\"Rich Text Area\":\"\\u5bcc\\u6587\\u672c\\u533a\\u57df\",\"Rich Text Area. Press ALT-0 for help.\":\"\\u7f16\\u8f91\\u533a\\u3002\\u6309Alt+0\\u952e\\u6253\\u5f00\\u5e2e\\u52a9\\u3002\",\"Rich Text Area. Press ALT-F9 for menu. Press ALT-F10 for toolbar. Press ALT-0 for help\":\"\\u7f16\\u8f91\\u533a\\u3002\\u6309ALT-F9\\u6253\\u5f00\\u83dc\\u5355\\uff0c\\u6309ALT-F10\\u6253\\u5f00\\u5de5\\u5177\\u680f\\uff0c\\u6309ALT-0\\u67e5\\u770b\\u5e2e\\u52a9\",\"Ridge\":\"\\u6d77\\u810a\\u5ea7\",\"Right\":\"\\u53f3\",\"Right to left\":\"\\u7531\\u53f3\\u5230\\u5de6\",\"Row\":\"\\u884c\",\"Row clipboard actions\":\"\\u884c\\u526a\\u8d34\\u677f\\u64cd\\u4f5c\",\"Row group\":\"\\u884c\\u7ec4\",\"Row header\":\"\\u884c\\u5934\",\"Row properties\":\"\\u884c\\u5c5e\\u6027\",\"Row type\":\"\\u884c\\u7c7b\\u578b\",\"Rows\":\"\\u884c\\u6570\",\"Save\":\"\\u4fdd\\u5b58\",\"Save (if save plugin activated)\":\"\\u4fdd\\u5b58(\\u5982\\u679c\\u4fdd\\u5b58\\u63d2\\u4ef6\\u5df2\\u6fc0\\u6d3b)\",\"Scope\":\"\\u8303\\u56f4\",\"Search\":\"\\u641c\\u7d22\",\"Select all\":\"\\u5168\\u9009\",\"Select...\":\"\\u9009\\u62e9...\",\"Selection\":\"\\u9009\\u62e9\",\"Shortcut\":\"\\u5feb\\u6377\\u65b9\\u5f0f\",\"Show blocks\":\"\\u663e\\u793a\\u533a\\u5757\\u8fb9\\u6846\",\"Show caption\":\"\\u663e\\u793a\\u6807\\u9898\",\"Show invisible characters\":\"\\u663e\\u793a\\u4e0d\\u53ef\\u89c1\\u5b57\\u7b26\",\"Size\":\"\\u5b57\\u53f7\",\"Solid\":\"\\u5b9e\\u7ebf\",\"Source\":\"\\u6e90\",\"Source code\":\"\\u6e90\\u4ee3\\u7801\",\"Special Character\":\"\\u7279\\u6b8a\\u5b57\\u7b26\",\"Special character...\":\"\\u7279\\u6b8a\\u5b57\\u7b26...\",\"Split cell\":\"\\u62c6\\u5206\\u5355\\u5143\\u683c\",\"Square\":\"\\u5b9e\\u5fc3\\u65b9\\u5757\",\"Start list at number\":\"\\u4ee5\\u6570\\u5b57\\u5f00\\u59cb\\u5217\\u8868\",\"Strikethrough\":\"\\u5220\\u9664\\u7ebf\",\"Style\":\"\\u6837\\u5f0f\",\"Subscript\":\"\\u4e0b\\u6807\",\"Superscript\":\"\\u4e0a\\u6807\",\"Switch to or from fullscreen mode\":\"\\u5207\\u6362\\u5168\\u5c4f\\u6a21\\u5f0f\",\"Symbols\":\"\\u7b26\\u53f7\",\"System Font\":\"\\u7cfb\\u7edf\\u5b57\\u4f53\",\"Table\":\"\\u8868\\u683c\",\"Table caption\":\"\\u8868\\u683c\\u6807\\u9898\",\"Table properties\":\"\\u8868\\u683c\\u5c5e\\u6027\",\"Table styles\":\"\\u8868\\u683c\\u6837\\u5f0f\",\"Template\":\"\\u6a21\\u677f\",\"Templates\":\"\\u6a21\\u677f\",\"Text\":\"\\u6587\\u5b57\",\"Text color\":\"\\u6587\\u672c\\u989c\\u8272\",\"Text color {0}\":\"\",\"Text to display\":\"\\u8981\\u663e\\u793a\\u7684\\u6587\\u672c\",\"The URL you entered seems to be an email address. Do you want to add the required mailto: prefix?\":\"\\u4f60\\u6240\\u586b\\u5199\\u7684URL\\u5730\\u5740\\u4e3a\\u90ae\\u4ef6\\u5730\\u5740\\uff0c\\u9700\\u8981\\u52a0\\u4e0amailto: \\u524d\\u7f00\\u5417\\uff1f\",\"The URL you entered seems to be an external link. Do you want to add the required http:// prefix?\":\"\\u4f60\\u6240\\u586b\\u5199\\u7684URL\\u5730\\u5740\\u5c5e\\u4e8e\\u5916\\u90e8\\u94fe\\u63a5\\uff0c\\u9700\\u8981\\u52a0\\u4e0ahttp:// \\u524d\\u7f00\\u5417\\uff1f\",\"The URL you entered seems to be an external link. Do you want to add the required https:// prefix?\":\"\\u60a8\\u8f93\\u5165\\u7684 URL \\u4f3c\\u4e4e\\u662f\\u4e00\\u4e2a\\u5916\\u90e8\\u94fe\\u63a5\\u3002\\u60a8\\u60f3\\u6dfb\\u52a0\\u6240\\u9700\\u7684 https:// \\u524d\\u7f00\\u5417\\uff1f\",\"Title\":\"\\u6807\\u9898\",\"To open the popup, press Shift+Enter\":\"\\u6309Shitf+Enter\\u952e\\u6253\\u5f00\\u5bf9\\u8bdd\\u6846\",\"Toggle accordion\":\"\",\"Tools\":\"\\u5de5\\u5177\",\"Top\":\"\\u4e0a\\u65b9\\u5bf9\\u9f50\",\"Travel and Places\":\"\\u65c5\\u6e38\\u548c\\u5730\\u70b9\",\"Turquoise\":\"\\u9752\\u7eff\\u8272\",\"Underline\":\"\\u4e0b\\u5212\\u7ebf\",\"Undo\":\"\\u64a4\\u9500\",\"Upload\":\"\\u4e0a\\u4f20\",\"Uploading image\":\"\\u4e0a\\u4f20\\u56fe\\u7247\",\"Upper Alpha\":\"\\u5927\\u5199\\u82f1\\u6587\\u5b57\\u6bcd\",\"Upper Roman\":\"\\u5927\\u5199\\u7f57\\u9a6c\\u6570\\u5b57\",\"Url\":\"\\u5730\\u5740\",\"User Defined\":\"\\u81ea\\u5b9a\\u4e49\",\"Valid\":\"\\u6709\\u6548\",\"Version\":\"\\u7248\\u672c\",\"Vertical align\":\"\\u5782\\u76f4\\u5bf9\\u9f50\",\"Vertical space\":\"\\u5782\\u76f4\\u95f4\\u8ddd\",\"View\":\"\\u67e5\\u770b\",\"Visual aids\":\"\\u7f51\\u683c\\u7ebf\",\"Warn\":\"\\u8b66\\u544a\",\"White\":\"\\u767d\\u8272\",\"Width\":\"\\u5bbd\\u5ea6\",\"Word count\":\"\\u5b57\\u6570\",\"Words\":\"\\u5355\\u8bcd\",\"Words: {0}\":\"\\u5b57\\u6570\\uff1a{0}\",\"Yellow\":\"\\u9ec4\\u8272\",\"Yes\":\"\\u662f\",\"You are using {0}\":\"\\u4f60\\u6b63\\u5728\\u4f7f\\u7528 {0}\",\"You have unsaved changes are you sure you want to navigate away?\":\"\\u4f60\\u8fd8\\u6709\\u6587\\u6863\\u5c1a\\u672a\\u4fdd\\u5b58\\uff0c\\u786e\\u5b9a\\u8981\\u79bb\\u5f00\\uff1f\",\"Your browser doesn't support direct access to the clipboard. Please use the Ctrl+X/C/V keyboard shortcuts instead.\":\"\\u4f60\\u7684\\u6d4f\\u89c8\\u5668\\u4e0d\\u652f\\u6301\\u6253\\u5f00\\u526a\\u8d34\\u677f\\uff0c\\u8bf7\\u4f7f\\u7528Ctrl+X/C/V\\u7b49\\u5feb\\u6377\\u952e\\u3002\",\"alignment\":\"\\u5bf9\\u9f50\",\"austral sign\":\"\\u6fb3\\u5143\\u7b26\\u53f7\",\"cedi sign\":\"\\u585e\\u5730\\u7b26\\u53f7\",\"colon sign\":\"\\u5192\\u53f7\",\"cruzeiro sign\":\"\\u514b\\u9c81\\u8d5b\\u7f57\\u5e01\\u7b26\\u53f7\",\"currency sign\":\"\\u8d27\\u5e01\\u7b26\\u53f7\",\"dollar sign\":\"\\u7f8e\\u5143\\u7b26\\u53f7\",\"dong sign\":\"\\u8d8a\\u5357\\u76fe\\u7b26\\u53f7\",\"drachma sign\":\"\\u5fb7\\u62c9\\u514b\\u9a6c\\u7b26\\u53f7\",\"euro-currency sign\":\"\\u6b27\\u5143\\u7b26\\u53f7\",\"example\":\"\\u793a\\u4f8b\",\"formatting\":\"\\u683c\\u5f0f\\u5316\",\"french franc sign\":\"\\u6cd5\\u90ce\\u7b26\\u53f7\",\"german penny symbol\":\"\\u5fb7\\u56fd\\u4fbf\\u58eb\\u7b26\\u53f7\",\"guarani sign\":\"\\u74dc\\u62c9\\u5c3c\\u7b26\\u53f7\",\"history\":\"\\u5386\\u53f2\",\"hryvnia sign\":\"\\u683c\\u91cc\\u592b\\u5c3c\\u4e9a\\u7b26\\u53f7\",\"indentation\":\"\\u7f29\\u8fdb\",\"indian rupee sign\":\"\\u5370\\u5ea6\\u5362\\u6bd4\",\"kip sign\":\"\\u8001\\u631d\\u57fa\\u666e\\u7b26\\u53f7\",\"lira sign\":\"\\u91cc\\u62c9\\u7b26\\u53f7\",\"livre tournois sign\":\"\\u91cc\\u5f17\\u5f17\\u5c14\\u7b26\\u53f7\",\"manat sign\":\"\\u9a6c\\u7eb3\\u7279\\u7b26\\u53f7\",\"mill sign\":\"\\u5bc6\\u5c14\\u7b26\\u53f7\",\"naira sign\":\"\\u5948\\u62c9\\u7b26\\u53f7\",\"new sheqel sign\":\"\\u65b0\\u8c22\\u514b\\u5c14\\u7b26\\u53f7\",\"nordic mark sign\":\"\\u5317\\u6b27\\u9a6c\\u514b\",\"peseta sign\":\"\\u6bd4\\u585e\\u5854\\u7b26\\u53f7\",\"peso sign\":\"\\u6bd4\\u7d22\\u7b26\\u53f7\",\"ruble sign\":\"\\u5362\\u5e03\\u7b26\\u53f7\",\"rupee sign\":\"\\u5362\\u6bd4\\u7b26\\u53f7\",\"spesmilo sign\":\"spesmilo\\u7b26\\u53f7\",\"styles\":\"\\u6837\\u5f0f\",\"tenge sign\":\"\\u575a\\u6208\\u7b26\\u53f7\",\"tugrik sign\":\"\\u56fe\\u683c\\u91cc\\u514b\\u7b26\\u53f7\",\"turkish lira sign\":\"\\u571f\\u8033\\u5176\\u91cc\\u62c9\",\"won sign\":\"\\u97e9\\u5143\\u7b26\\u53f7\",\"yen character\":\"\\u65e5\\u5143\\u5b57\\u6837\",\"yen/yuan character variant one\":\"\\u5143\\u5b57\\u6837\\uff08\\u5927\\u5199\\uff09\",\"yuan character\":\"\\u4eba\\u6c11\\u5e01\\u5143\\u5b57\\u6837\",\"yuan character, in hong kong and taiwan\":\"\\u5143\\u5b57\\u6837\\uff08\\u6e2f\\u53f0\\u5730\\u533a\\uff09\",\"{0} characters\":\"{0} \\u4e2a\\u5b57\\u7b26\",\"{0} columns, {1} rows\":\"\",\"{0} words\":\"{0} \\u5b57\"});"
  },
  {
    "path": "apps/web-antd/public/tinymce/plugins/emoticons/js/emojiimages.js",
    "content": "window.tinymce.Resource.add(\"tinymce.plugins.emoticons\",{100:{keywords:[\"score\",\"perfect\",\"numbers\",\"century\",\"exam\",\"quiz\",\"test\",\"pass\",\"hundred\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"💯\" src=\"1f4af.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},1234:{keywords:[\"numbers\",\"blue-square\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🔢\" src=\"1f522.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},grinning:{keywords:[\"face\",\"smile\",\"happy\",\"joy\",\":D\",\"grin\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"😀\" src=\"1f600.png\"/>',fitzpatrick_scale:false,category:\"people\"},grimacing:{keywords:[\"face\",\"grimace\",\"teeth\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"😬\" src=\"1f62c.png\"/>',fitzpatrick_scale:false,category:\"people\"},grin:{keywords:[\"face\",\"happy\",\"smile\",\"joy\",\"kawaii\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"😁\" src=\"1f601.png\"/>',fitzpatrick_scale:false,category:\"people\"},joy:{keywords:[\"face\",\"cry\",\"tears\",\"weep\",\"happy\",\"happytears\",\"haha\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"😂\" src=\"1f602.png\"/>',fitzpatrick_scale:false,category:\"people\"},rofl:{keywords:[\"face\",\"rolling\",\"floor\",\"laughing\",\"lol\",\"haha\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🤣\" src=\"1f923.png\"/>',fitzpatrick_scale:false,category:\"people\"},partying:{keywords:[\"face\",\"celebration\",\"woohoo\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🥳\" src=\"1f973.png\"/>',fitzpatrick_scale:false,category:\"people\"},smiley:{keywords:[\"face\",\"happy\",\"joy\",\"haha\",\":D\",\":)\",\"smile\",\"funny\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"😃\" src=\"1f603.png\"/>',fitzpatrick_scale:false,category:\"people\"},smile:{keywords:[\"face\",\"happy\",\"joy\",\"funny\",\"haha\",\"laugh\",\"like\",\":D\",\":)\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"😄\" src=\"1f604.png\"/>',fitzpatrick_scale:false,category:\"people\"},sweat_smile:{keywords:[\"face\",\"hot\",\"happy\",\"laugh\",\"sweat\",\"smile\",\"relief\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"😅\" src=\"1f605.png\"/>',fitzpatrick_scale:false,category:\"people\"},laughing:{keywords:[\"happy\",\"joy\",\"lol\",\"satisfied\",\"haha\",\"face\",\"glad\",\"XD\",\"laugh\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"😆\" src=\"1f606.png\"/>',fitzpatrick_scale:false,category:\"people\"},innocent:{keywords:[\"face\",\"angel\",\"heaven\",\"halo\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"😇\" src=\"1f607.png\"/>',fitzpatrick_scale:false,category:\"people\"},wink:{keywords:[\"face\",\"happy\",\"mischievous\",\"secret\",\";)\",\"smile\",\"eye\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"😉\" src=\"1f609.png\"/>',fitzpatrick_scale:false,category:\"people\"},blush:{keywords:[\"face\",\"smile\",\"happy\",\"flushed\",\"crush\",\"embarrassed\",\"shy\",\"joy\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"😊\" src=\"1f60a.png\"/>',fitzpatrick_scale:false,category:\"people\"},slightly_smiling_face:{keywords:[\"face\",\"smile\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🙂\" src=\"1f642.png\"/>',fitzpatrick_scale:false,category:\"people\"},upside_down_face:{keywords:[\"face\",\"flipped\",\"silly\",\"smile\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🙃\" src=\"1f643.png\"/>',fitzpatrick_scale:false,category:\"people\"},relaxed:{keywords:[\"face\",\"blush\",\"massage\",\"happiness\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"☺️\" src=\"263a.png\"/>',fitzpatrick_scale:false,category:\"people\"},yum:{keywords:[\"happy\",\"joy\",\"tongue\",\"smile\",\"face\",\"silly\",\"yummy\",\"nom\",\"delicious\",\"savouring\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"😋\" src=\"1f60b.png\"/>',fitzpatrick_scale:false,category:\"people\"},relieved:{keywords:[\"face\",\"relaxed\",\"phew\",\"massage\",\"happiness\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"😌\" src=\"1f60c.png\"/>',fitzpatrick_scale:false,category:\"people\"},heart_eyes:{keywords:[\"face\",\"love\",\"like\",\"affection\",\"valentines\",\"infatuation\",\"crush\",\"heart\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"😍\" src=\"1f60d.png\"/>',fitzpatrick_scale:false,category:\"people\"},smiling_face_with_three_hearts:{keywords:[\"face\",\"love\",\"like\",\"affection\",\"valentines\",\"infatuation\",\"crush\",\"hearts\",\"adore\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🥰\" src=\"1f970.png\"/>',fitzpatrick_scale:false,category:\"people\"},kissing_heart:{keywords:[\"face\",\"love\",\"like\",\"affection\",\"valentines\",\"infatuation\",\"kiss\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"😘\" src=\"1f618.png\"/>',fitzpatrick_scale:false,category:\"people\"},kissing:{keywords:[\"love\",\"like\",\"face\",\"3\",\"valentines\",\"infatuation\",\"kiss\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"😗\" src=\"1f617.png\"/>',fitzpatrick_scale:false,category:\"people\"},kissing_smiling_eyes:{keywords:[\"face\",\"affection\",\"valentines\",\"infatuation\",\"kiss\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"😙\" src=\"1f619.png\"/>',fitzpatrick_scale:false,category:\"people\"},kissing_closed_eyes:{keywords:[\"face\",\"love\",\"like\",\"affection\",\"valentines\",\"infatuation\",\"kiss\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"😚\" src=\"1f61a.png\"/>',fitzpatrick_scale:false,category:\"people\"},stuck_out_tongue_winking_eye:{keywords:[\"face\",\"prank\",\"childish\",\"playful\",\"mischievous\",\"smile\",\"wink\",\"tongue\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"😜\" src=\"1f61c.png\"/>',fitzpatrick_scale:false,category:\"people\"},zany:{keywords:[\"face\",\"goofy\",\"crazy\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🤪\" src=\"1f92a.png\"/>',fitzpatrick_scale:false,category:\"people\"},raised_eyebrow:{keywords:[\"face\",\"distrust\",\"scepticism\",\"disapproval\",\"disbelief\",\"surprise\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🤨\" src=\"1f928.png\"/>',fitzpatrick_scale:false,category:\"people\"},monocle:{keywords:[\"face\",\"stuffy\",\"wealthy\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🧐\" src=\"1f9d0.png\"/>',fitzpatrick_scale:false,category:\"people\"},stuck_out_tongue_closed_eyes:{keywords:[\"face\",\"prank\",\"playful\",\"mischievous\",\"smile\",\"tongue\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"😝\" src=\"1f61d.png\"/>',fitzpatrick_scale:false,category:\"people\"},stuck_out_tongue:{keywords:[\"face\",\"prank\",\"childish\",\"playful\",\"mischievous\",\"smile\",\"tongue\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"😛\" src=\"1f61b.png\"/>',fitzpatrick_scale:false,category:\"people\"},money_mouth_face:{keywords:[\"face\",\"rich\",\"dollar\",\"money\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🤑\" src=\"1f911.png\"/>',fitzpatrick_scale:false,category:\"people\"},nerd_face:{keywords:[\"face\",\"nerdy\",\"geek\",\"dork\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🤓\" src=\"1f913.png\"/>',fitzpatrick_scale:false,category:\"people\"},sunglasses:{keywords:[\"face\",\"cool\",\"smile\",\"summer\",\"beach\",\"sunglass\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"😎\" src=\"1f60e.png\"/>',fitzpatrick_scale:false,category:\"people\"},star_struck:{keywords:[\"face\",\"smile\",\"starry\",\"eyes\",\"grinning\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🤩\" src=\"1f929.png\"/>',fitzpatrick_scale:false,category:\"people\"},clown_face:{keywords:[\"face\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🤡\" src=\"1f921.png\"/>',fitzpatrick_scale:false,category:\"people\"},cowboy_hat_face:{keywords:[\"face\",\"cowgirl\",\"hat\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🤠\" src=\"1f920.png\"/>',fitzpatrick_scale:false,category:\"people\"},hugs:{keywords:[\"face\",\"smile\",\"hug\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🤗\" src=\"1f917.png\"/>',fitzpatrick_scale:false,category:\"people\"},smirk:{keywords:[\"face\",\"smile\",\"mean\",\"prank\",\"smug\",\"sarcasm\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"😏\" src=\"1f60f.png\"/>',fitzpatrick_scale:false,category:\"people\"},no_mouth:{keywords:[\"face\",\"hellokitty\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"😶\" src=\"1f636.png\"/>',fitzpatrick_scale:false,category:\"people\"},neutral_face:{keywords:[\"indifference\",\"meh\",\":|\",\"neutral\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"😐\" src=\"1f610.png\"/>',fitzpatrick_scale:false,category:\"people\"},expressionless:{keywords:[\"face\",\"indifferent\",\"-_-\",\"meh\",\"deadpan\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"😑\" src=\"1f611.png\"/>',fitzpatrick_scale:false,category:\"people\"},unamused:{keywords:[\"indifference\",\"bored\",\"straight face\",\"serious\",\"sarcasm\",\"unimpressed\",\"skeptical\",\"dubious\",\"side_eye\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"😒\" src=\"1f612.png\"/>',fitzpatrick_scale:false,category:\"people\"},roll_eyes:{keywords:[\"face\",\"eyeroll\",\"frustrated\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🙄\" src=\"1f644.png\"/>',fitzpatrick_scale:false,category:\"people\"},thinking:{keywords:[\"face\",\"hmmm\",\"think\",\"consider\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🤔\" src=\"1f914.png\"/>',fitzpatrick_scale:false,category:\"people\"},lying_face:{keywords:[\"face\",\"lie\",\"pinocchio\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🤥\" src=\"1f925.png\"/>',fitzpatrick_scale:false,category:\"people\"},hand_over_mouth:{keywords:[\"face\",\"whoops\",\"shock\",\"surprise\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🤭\" src=\"1f92d.png\"/>',fitzpatrick_scale:false,category:\"people\"},shushing:{keywords:[\"face\",\"quiet\",\"shhh\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🤫\" src=\"1f92b.png\"/>',fitzpatrick_scale:false,category:\"people\"},symbols_over_mouth:{keywords:[\"face\",\"swearing\",\"cursing\",\"cussing\",\"profanity\",\"expletive\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🤬\" src=\"1f92c.png\"/>',fitzpatrick_scale:false,category:\"people\"},exploding_head:{keywords:[\"face\",\"shocked\",\"mind\",\"blown\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🤯\" src=\"1f92f.png\"/>',fitzpatrick_scale:false,category:\"people\"},flushed:{keywords:[\"face\",\"blush\",\"shy\",\"flattered\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"😳\" src=\"1f633.png\"/>',fitzpatrick_scale:false,category:\"people\"},disappointed:{keywords:[\"face\",\"sad\",\"upset\",\"depressed\",\":(\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"😞\" src=\"1f61e.png\"/>',fitzpatrick_scale:false,category:\"people\"},worried:{keywords:[\"face\",\"concern\",\"nervous\",\":(\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"😟\" src=\"1f61f.png\"/>',fitzpatrick_scale:false,category:\"people\"},angry:{keywords:[\"mad\",\"face\",\"annoyed\",\"frustrated\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"😠\" src=\"1f620.png\"/>',fitzpatrick_scale:false,category:\"people\"},rage:{keywords:[\"angry\",\"mad\",\"hate\",\"despise\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"😡\" src=\"1f621.png\"/>',fitzpatrick_scale:false,category:\"people\"},pensive:{keywords:[\"face\",\"sad\",\"depressed\",\"upset\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"😔\" src=\"1f614.png\"/>',fitzpatrick_scale:false,category:\"people\"},confused:{keywords:[\"face\",\"indifference\",\"huh\",\"weird\",\"hmmm\",\":/\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"😕\" src=\"1f615.png\"/>',fitzpatrick_scale:false,category:\"people\"},slightly_frowning_face:{keywords:[\"face\",\"frowning\",\"disappointed\",\"sad\",\"upset\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🙁\" src=\"1f641.png\"/>',fitzpatrick_scale:false,category:\"people\"},frowning_face:{keywords:[\"face\",\"sad\",\"upset\",\"frown\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"☹\" src=\"2639.png\"/>',fitzpatrick_scale:false,category:\"people\"},persevere:{keywords:[\"face\",\"sick\",\"no\",\"upset\",\"oops\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"😣\" src=\"1f623.png\"/>',fitzpatrick_scale:false,category:\"people\"},confounded:{keywords:[\"face\",\"confused\",\"sick\",\"unwell\",\"oops\",\":S\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"😖\" src=\"1f616.png\"/>',fitzpatrick_scale:false,category:\"people\"},tired_face:{keywords:[\"sick\",\"whine\",\"upset\",\"frustrated\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"😫\" src=\"1f62b.png\"/>',fitzpatrick_scale:false,category:\"people\"},weary:{keywords:[\"face\",\"tired\",\"sleepy\",\"sad\",\"frustrated\",\"upset\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"😩\" src=\"1f629.png\"/>',fitzpatrick_scale:false,category:\"people\"},pleading:{keywords:[\"face\",\"begging\",\"mercy\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🥺\" src=\"1f97a.png\"/>',fitzpatrick_scale:false,category:\"people\"},triumph:{keywords:[\"face\",\"gas\",\"phew\",\"proud\",\"pride\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"😤\" src=\"1f624.png\"/>',fitzpatrick_scale:false,category:\"people\"},open_mouth:{keywords:[\"face\",\"surprise\",\"impressed\",\"wow\",\"whoa\",\":O\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"😮\" src=\"1f62e.png\"/>',fitzpatrick_scale:false,category:\"people\"},scream:{keywords:[\"face\",\"munch\",\"scared\",\"omg\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"😱\" src=\"1f631.png\"/>',fitzpatrick_scale:false,category:\"people\"},fearful:{keywords:[\"face\",\"scared\",\"terrified\",\"nervous\",\"oops\",\"huh\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"😨\" src=\"1f628.png\"/>',fitzpatrick_scale:false,category:\"people\"},cold_sweat:{keywords:[\"face\",\"nervous\",\"sweat\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"😰\" src=\"1f630.png\"/>',fitzpatrick_scale:false,category:\"people\"},hushed:{keywords:[\"face\",\"woo\",\"shh\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"😯\" src=\"1f62f.png\"/>',fitzpatrick_scale:false,category:\"people\"},frowning:{keywords:[\"face\",\"aw\",\"what\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"😦\" src=\"1f626.png\"/>',fitzpatrick_scale:false,category:\"people\"},anguished:{keywords:[\"face\",\"stunned\",\"nervous\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"😧\" src=\"1f627.png\"/>',fitzpatrick_scale:false,category:\"people\"},cry:{keywords:[\"face\",\"tears\",\"sad\",\"depressed\",\"upset\",\":'(\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"😢\" src=\"1f622.png\"/>',fitzpatrick_scale:false,category:\"people\"},disappointed_relieved:{keywords:[\"face\",\"phew\",\"sweat\",\"nervous\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"😥\" src=\"1f625.png\"/>',fitzpatrick_scale:false,category:\"people\"},drooling_face:{keywords:[\"face\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🤤\" src=\"1f924.png\"/>',fitzpatrick_scale:false,category:\"people\"},sleepy:{keywords:[\"face\",\"tired\",\"rest\",\"nap\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"😪\" src=\"1f62a.png\"/>',fitzpatrick_scale:false,category:\"people\"},sweat:{keywords:[\"face\",\"hot\",\"sad\",\"tired\",\"exercise\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"😓\" src=\"1f613.png\"/>',fitzpatrick_scale:false,category:\"people\"},hot:{keywords:[\"face\",\"feverish\",\"heat\",\"red\",\"sweating\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🥵\" src=\"1f975.png\"/>',fitzpatrick_scale:false,category:\"people\"},cold:{keywords:[\"face\",\"blue\",\"freezing\",\"frozen\",\"frostbite\",\"icicles\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🥶\" src=\"1f976.png\"/>',fitzpatrick_scale:false,category:\"people\"},sob:{keywords:[\"face\",\"cry\",\"tears\",\"sad\",\"upset\",\"depressed\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"😭\" src=\"1f62d.png\"/>',fitzpatrick_scale:false,category:\"people\"},dizzy_face:{keywords:[\"spent\",\"unconscious\",\"xox\",\"dizzy\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"😵\" src=\"1f635.png\"/>',fitzpatrick_scale:false,category:\"people\"},astonished:{keywords:[\"face\",\"xox\",\"surprised\",\"poisoned\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"😲\" src=\"1f632.png\"/>',fitzpatrick_scale:false,category:\"people\"},zipper_mouth_face:{keywords:[\"face\",\"sealed\",\"zipper\",\"secret\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🤐\" src=\"1f910.png\"/>',fitzpatrick_scale:false,category:\"people\"},nauseated_face:{keywords:[\"face\",\"vomit\",\"gross\",\"green\",\"sick\",\"throw up\",\"ill\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🤢\" src=\"1f922.png\"/>',fitzpatrick_scale:false,category:\"people\"},sneezing_face:{keywords:[\"face\",\"gesundheit\",\"sneeze\",\"sick\",\"allergy\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🤧\" src=\"1f927.png\"/>',fitzpatrick_scale:false,category:\"people\"},vomiting:{keywords:[\"face\",\"sick\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🤮\" src=\"1f92e.png\"/>',fitzpatrick_scale:false,category:\"people\"},mask:{keywords:[\"face\",\"sick\",\"ill\",\"disease\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"😷\" src=\"1f637.png\"/>',fitzpatrick_scale:false,category:\"people\"},face_with_thermometer:{keywords:[\"sick\",\"temperature\",\"thermometer\",\"cold\",\"fever\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🤒\" src=\"1f912.png\"/>',fitzpatrick_scale:false,category:\"people\"},face_with_head_bandage:{keywords:[\"injured\",\"clumsy\",\"bandage\",\"hurt\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🤕\" src=\"1f915.png\"/>',fitzpatrick_scale:false,category:\"people\"},woozy:{keywords:[\"face\",\"dizzy\",\"intoxicated\",\"tipsy\",\"wavy\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🥴\" src=\"1f974.png\"/>',fitzpatrick_scale:false,category:\"people\"},sleeping:{keywords:[\"face\",\"tired\",\"sleepy\",\"night\",\"zzz\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"😴\" src=\"1f634.png\"/>',fitzpatrick_scale:false,category:\"people\"},zzz:{keywords:[\"sleepy\",\"tired\",\"dream\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"💤\" src=\"1f4a4.png\"/>',fitzpatrick_scale:false,category:\"people\"},poop:{keywords:[\"hankey\",\"shitface\",\"fail\",\"turd\",\"shit\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"💩\" src=\"1f4a9.png\"/>',fitzpatrick_scale:false,category:\"people\"},smiling_imp:{keywords:[\"devil\",\"horns\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"😈\" src=\"1f608.png\"/>',fitzpatrick_scale:false,category:\"people\"},imp:{keywords:[\"devil\",\"angry\",\"horns\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👿\" src=\"1f47f.png\"/>',fitzpatrick_scale:false,category:\"people\"},japanese_ogre:{keywords:[\"monster\",\"red\",\"mask\",\"halloween\",\"scary\",\"creepy\",\"devil\",\"demon\",\"japanese\",\"ogre\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👹\" src=\"1f479.png\"/>',fitzpatrick_scale:false,category:\"people\"},japanese_goblin:{keywords:[\"red\",\"evil\",\"mask\",\"monster\",\"scary\",\"creepy\",\"japanese\",\"goblin\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👺\" src=\"1f47a.png\"/>',fitzpatrick_scale:false,category:\"people\"},skull:{keywords:[\"dead\",\"skeleton\",\"creepy\",\"death\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"💀\" src=\"1f480.png\"/>',fitzpatrick_scale:false,category:\"people\"},ghost:{keywords:[\"halloween\",\"spooky\",\"scary\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👻\" src=\"1f47b.png\"/>',fitzpatrick_scale:false,category:\"people\"},alien:{keywords:[\"UFO\",\"paul\",\"weird\",\"outer_space\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👽\" src=\"1f47d.png\"/>',fitzpatrick_scale:false,category:\"people\"},robot:{keywords:[\"computer\",\"machine\",\"bot\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🤖\" src=\"1f916.png\"/>',fitzpatrick_scale:false,category:\"people\"},smiley_cat:{keywords:[\"animal\",\"cats\",\"happy\",\"smile\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"😺\" src=\"1f63a.png\"/>',fitzpatrick_scale:false,category:\"people\"},smile_cat:{keywords:[\"animal\",\"cats\",\"smile\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"😸\" src=\"1f638.png\"/>',fitzpatrick_scale:false,category:\"people\"},joy_cat:{keywords:[\"animal\",\"cats\",\"haha\",\"happy\",\"tears\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"😹\" src=\"1f639.png\"/>',fitzpatrick_scale:false,category:\"people\"},heart_eyes_cat:{keywords:[\"animal\",\"love\",\"like\",\"affection\",\"cats\",\"valentines\",\"heart\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"😻\" src=\"1f63b.png\"/>',fitzpatrick_scale:false,category:\"people\"},smirk_cat:{keywords:[\"animal\",\"cats\",\"smirk\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"😼\" src=\"1f63c.png\"/>',fitzpatrick_scale:false,category:\"people\"},kissing_cat:{keywords:[\"animal\",\"cats\",\"kiss\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"😽\" src=\"1f63d.png\"/>',fitzpatrick_scale:false,category:\"people\"},scream_cat:{keywords:[\"animal\",\"cats\",\"munch\",\"scared\",\"scream\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🙀\" src=\"1f640.png\"/>',fitzpatrick_scale:false,category:\"people\"},crying_cat_face:{keywords:[\"animal\",\"tears\",\"weep\",\"sad\",\"cats\",\"upset\",\"cry\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"😿\" src=\"1f63f.png\"/>',fitzpatrick_scale:false,category:\"people\"},pouting_cat:{keywords:[\"animal\",\"cats\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"😾\" src=\"1f63e.png\"/>',fitzpatrick_scale:false,category:\"people\"},palms_up:{keywords:[\"hands\",\"gesture\",\"cupped\",\"prayer\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🤲\" src=\"1f932.png\"/>',fitzpatrick_scale:true,category:\"people\"},raised_hands:{keywords:[\"gesture\",\"hooray\",\"yea\",\"celebration\",\"hands\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🙌\" src=\"1f64c.png\"/>',fitzpatrick_scale:true,category:\"people\"},clap:{keywords:[\"hands\",\"praise\",\"applause\",\"congrats\",\"yay\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👏\" src=\"1f44f.png\"/>',fitzpatrick_scale:true,category:\"people\"},wave:{keywords:[\"hands\",\"gesture\",\"goodbye\",\"solong\",\"farewell\",\"hello\",\"hi\",\"palm\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👋\" src=\"1f44b.png\"/>',fitzpatrick_scale:true,category:\"people\"},call_me_hand:{keywords:[\"hands\",\"gesture\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🤙\" src=\"1f919.png\"/>',fitzpatrick_scale:true,category:\"people\"},\"+1\":{keywords:[\"thumbsup\",\"yes\",\"awesome\",\"good\",\"agree\",\"accept\",\"cool\",\"hand\",\"like\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👍\" src=\"1f44d.png\"/>',fitzpatrick_scale:true,category:\"people\"},\"-1\":{keywords:[\"thumbsdown\",\"no\",\"dislike\",\"hand\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👎\" src=\"1f44e.png\"/>',fitzpatrick_scale:true,category:\"people\"},facepunch:{keywords:[\"angry\",\"violence\",\"fist\",\"hit\",\"attack\",\"hand\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👊\" src=\"1f44a.png\"/>',fitzpatrick_scale:true,category:\"people\"},fist:{keywords:[\"fingers\",\"hand\",\"grasp\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"✊\" src=\"270a.png\"/>',fitzpatrick_scale:true,category:\"people\"},fist_left:{keywords:[\"hand\",\"fistbump\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🤛\" src=\"1f91b.png\"/>',fitzpatrick_scale:true,category:\"people\"},fist_right:{keywords:[\"hand\",\"fistbump\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🤜\" src=\"1f91c.png\"/>',fitzpatrick_scale:true,category:\"people\"},v:{keywords:[\"fingers\",\"ohyeah\",\"hand\",\"peace\",\"victory\",\"two\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"✌\" src=\"270c.png\"/>',fitzpatrick_scale:true,category:\"people\"},ok_hand:{keywords:[\"fingers\",\"limbs\",\"perfect\",\"ok\",\"okay\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👌\" src=\"1f44c.png\"/>',fitzpatrick_scale:true,category:\"people\"},raised_hand:{keywords:[\"fingers\",\"stop\",\"highfive\",\"palm\",\"ban\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"✋\" src=\"270b.png\"/>',fitzpatrick_scale:true,category:\"people\"},raised_back_of_hand:{keywords:[\"fingers\",\"raised\",\"backhand\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🤚\" src=\"1f91a.png\"/>',fitzpatrick_scale:true,category:\"people\"},open_hands:{keywords:[\"fingers\",\"butterfly\",\"hands\",\"open\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👐\" src=\"1f450.png\"/>',fitzpatrick_scale:true,category:\"people\"},muscle:{keywords:[\"arm\",\"flex\",\"hand\",\"summer\",\"strong\",\"biceps\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"💪\" src=\"1f4aa.png\"/>',fitzpatrick_scale:true,category:\"people\"},pray:{keywords:[\"please\",\"hope\",\"wish\",\"namaste\",\"highfive\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🙏\" src=\"1f64f.png\"/>',fitzpatrick_scale:true,category:\"people\"},foot:{keywords:[\"kick\",\"stomp\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🦶\" src=\"1f9b6.png\"/>',fitzpatrick_scale:true,category:\"people\"},leg:{keywords:[\"kick\",\"limb\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🦵\" src=\"1f9b5.png\"/>',fitzpatrick_scale:true,category:\"people\"},handshake:{keywords:[\"agreement\",\"shake\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🤝\" src=\"1f91d.png\"/>',fitzpatrick_scale:false,category:\"people\"},point_up:{keywords:[\"hand\",\"fingers\",\"direction\",\"up\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"☝\" src=\"261d.png\"/>',fitzpatrick_scale:true,category:\"people\"},point_up_2:{keywords:[\"fingers\",\"hand\",\"direction\",\"up\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👆\" src=\"1f446.png\"/>',fitzpatrick_scale:true,category:\"people\"},point_down:{keywords:[\"fingers\",\"hand\",\"direction\",\"down\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👇\" src=\"1f447.png\"/>',fitzpatrick_scale:true,category:\"people\"},point_left:{keywords:[\"direction\",\"fingers\",\"hand\",\"left\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👈\" src=\"1f448.png\"/>',fitzpatrick_scale:true,category:\"people\"},point_right:{keywords:[\"fingers\",\"hand\",\"direction\",\"right\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👉\" src=\"1f449.png\"/>',fitzpatrick_scale:true,category:\"people\"},fu:{keywords:[\"hand\",\"fingers\",\"rude\",\"middle\",\"flipping\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🖕\" src=\"1f595.png\"/>',fitzpatrick_scale:true,category:\"people\"},raised_hand_with_fingers_splayed:{keywords:[\"hand\",\"fingers\",\"palm\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🖐\" src=\"1f590.png\"/>',fitzpatrick_scale:true,category:\"people\"},love_you:{keywords:[\"hand\",\"fingers\",\"gesture\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🤟\" src=\"1f91f.png\"/>',fitzpatrick_scale:true,category:\"people\"},metal:{keywords:[\"hand\",\"fingers\",\"evil_eye\",\"sign_of_horns\",\"rock_on\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🤘\" src=\"1f918.png\"/>',fitzpatrick_scale:true,category:\"people\"},crossed_fingers:{keywords:[\"good\",\"lucky\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🤞\" src=\"1f91e.png\"/>',fitzpatrick_scale:true,category:\"people\"},vulcan_salute:{keywords:[\"hand\",\"fingers\",\"spock\",\"star trek\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🖖\" src=\"1f596.png\"/>',fitzpatrick_scale:true,category:\"people\"},writing_hand:{keywords:[\"lower_left_ballpoint_pen\",\"stationery\",\"write\",\"compose\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"✍\" src=\"270d.png\"/>',fitzpatrick_scale:true,category:\"people\"},selfie:{keywords:[\"camera\",\"phone\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🤳\" src=\"1f933.png\"/>',fitzpatrick_scale:true,category:\"people\"},nail_care:{keywords:[\"beauty\",\"manicure\",\"finger\",\"fashion\",\"nail\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"💅\" src=\"1f485.png\"/>',fitzpatrick_scale:true,category:\"people\"},lips:{keywords:[\"mouth\",\"kiss\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👄\" src=\"1f444.png\"/>',fitzpatrick_scale:false,category:\"people\"},tooth:{keywords:[\"teeth\",\"dentist\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🦷\" src=\"1f9b7.png\"/>',fitzpatrick_scale:false,category:\"people\"},tongue:{keywords:[\"mouth\",\"playful\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👅\" src=\"1f445.png\"/>',fitzpatrick_scale:false,category:\"people\"},ear:{keywords:[\"face\",\"hear\",\"sound\",\"listen\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👂\" src=\"1f442.png\"/>',fitzpatrick_scale:true,category:\"people\"},nose:{keywords:[\"smell\",\"sniff\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👃\" src=\"1f443.png\"/>',fitzpatrick_scale:true,category:\"people\"},eye:{keywords:[\"face\",\"look\",\"see\",\"watch\",\"stare\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👁\" src=\"1f441.png\"/>',fitzpatrick_scale:false,category:\"people\"},eyes:{keywords:[\"look\",\"watch\",\"stalk\",\"peek\",\"see\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👀\" src=\"1f440.png\"/>',fitzpatrick_scale:false,category:\"people\"},brain:{keywords:[\"smart\",\"intelligent\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🧠\" src=\"1f9e0.png\"/>',fitzpatrick_scale:false,category:\"people\"},bust_in_silhouette:{keywords:[\"user\",\"person\",\"human\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👤\" src=\"1f464.png\"/>',fitzpatrick_scale:false,category:\"people\"},busts_in_silhouette:{keywords:[\"user\",\"person\",\"human\",\"group\",\"team\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👥\" src=\"1f465.png\"/>',fitzpatrick_scale:false,category:\"people\"},speaking_head:{keywords:[\"user\",\"person\",\"human\",\"sing\",\"say\",\"talk\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🗣\" src=\"1f5e3.png\"/>',fitzpatrick_scale:false,category:\"people\"},baby:{keywords:[\"child\",\"boy\",\"girl\",\"toddler\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👶\" src=\"1f476.png\"/>',fitzpatrick_scale:true,category:\"people\"},child:{keywords:[\"gender-neutral\",\"young\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🧒\" src=\"1f9d2.png\"/>',fitzpatrick_scale:true,category:\"people\"},boy:{keywords:[\"man\",\"male\",\"guy\",\"teenager\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👦\" src=\"1f466.png\"/>',fitzpatrick_scale:true,category:\"people\"},girl:{keywords:[\"female\",\"woman\",\"teenager\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👧\" src=\"1f467.png\"/>',fitzpatrick_scale:true,category:\"people\"},adult:{keywords:[\"gender-neutral\",\"person\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🧑\" src=\"1f9d1.png\"/>',fitzpatrick_scale:true,category:\"people\"},man:{keywords:[\"mustache\",\"father\",\"dad\",\"guy\",\"classy\",\"sir\",\"moustache\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👨\" src=\"1f468.png\"/>',fitzpatrick_scale:true,category:\"people\"},woman:{keywords:[\"female\",\"girls\",\"lady\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👩\" src=\"1f469.png\"/>',fitzpatrick_scale:true,category:\"people\"},blonde_woman:{keywords:[\"woman\",\"female\",\"girl\",\"blonde\",\"person\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👱‍♀️\" src=\"1f471-200d-2640-fe0f.png\"/>',fitzpatrick_scale:true,category:\"people\"},blonde_man:{keywords:[\"man\",\"male\",\"boy\",\"blonde\",\"guy\",\"person\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👱\" src=\"1f471.png\"/>',fitzpatrick_scale:true,category:\"people\"},bearded_person:{keywords:[\"person\",\"bewhiskered\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🧔\" src=\"1f9d4.png\"/>',fitzpatrick_scale:true,category:\"people\"},older_adult:{keywords:[\"human\",\"elder\",\"senior\",\"gender-neutral\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🧓\" src=\"1f9d3.png\"/>',fitzpatrick_scale:true,category:\"people\"},older_man:{keywords:[\"human\",\"male\",\"men\",\"old\",\"elder\",\"senior\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👴\" src=\"1f474.png\"/>',fitzpatrick_scale:true,category:\"people\"},older_woman:{keywords:[\"human\",\"female\",\"women\",\"lady\",\"old\",\"elder\",\"senior\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👵\" src=\"1f475.png\"/>',fitzpatrick_scale:true,category:\"people\"},man_with_gua_pi_mao:{keywords:[\"male\",\"boy\",\"chinese\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👲\" src=\"1f472.png\"/>',fitzpatrick_scale:true,category:\"people\"},woman_with_headscarf:{keywords:[\"female\",\"hijab\",\"mantilla\",\"tichel\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🧕\" src=\"1f9d5.png\"/>',fitzpatrick_scale:true,category:\"people\"},woman_with_turban:{keywords:[\"female\",\"indian\",\"hinduism\",\"arabs\",\"woman\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👳‍♀️\" src=\"1f473-200d-2640-fe0f.png\"/>',fitzpatrick_scale:true,category:\"people\"},man_with_turban:{keywords:[\"male\",\"indian\",\"hinduism\",\"arabs\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👳\" src=\"1f473.png\"/>',fitzpatrick_scale:true,category:\"people\"},policewoman:{keywords:[\"woman\",\"police\",\"law\",\"legal\",\"enforcement\",\"arrest\",\"911\",\"female\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👮‍♀️\" src=\"1f46e-200d-2640-fe0f.png\"/>',fitzpatrick_scale:true,category:\"people\"},policeman:{keywords:[\"man\",\"police\",\"law\",\"legal\",\"enforcement\",\"arrest\",\"911\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👮\" src=\"1f46e.png\"/>',fitzpatrick_scale:true,category:\"people\"},construction_worker_woman:{keywords:[\"female\",\"human\",\"wip\",\"build\",\"construction\",\"worker\",\"labor\",\"woman\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👷‍♀️\" src=\"1f477-200d-2640-fe0f.png\"/>',fitzpatrick_scale:true,category:\"people\"},construction_worker_man:{keywords:[\"male\",\"human\",\"wip\",\"guy\",\"build\",\"construction\",\"worker\",\"labor\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👷\" src=\"1f477.png\"/>',fitzpatrick_scale:true,category:\"people\"},guardswoman:{keywords:[\"uk\",\"gb\",\"british\",\"female\",\"royal\",\"woman\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"💂‍♀️\" src=\"1f482-200d-2640-fe0f.png\"/>',fitzpatrick_scale:true,category:\"people\"},guardsman:{keywords:[\"uk\",\"gb\",\"british\",\"male\",\"guy\",\"royal\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"💂\" src=\"1f482.png\"/>',fitzpatrick_scale:true,category:\"people\"},female_detective:{keywords:[\"human\",\"spy\",\"detective\",\"female\",\"woman\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🕵️‍♀️\" src=\"1f575-fe0f-200d-2640-fe0f.png\"/>',fitzpatrick_scale:true,category:\"people\"},male_detective:{keywords:[\"human\",\"spy\",\"detective\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🕵\" src=\"1f575.png\"/>',fitzpatrick_scale:true,category:\"people\"},woman_health_worker:{keywords:[\"doctor\",\"nurse\",\"therapist\",\"healthcare\",\"woman\",\"human\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👩‍⚕️\" src=\"1f469-200d-2695-fe0f.png\"/>',fitzpatrick_scale:true,category:\"people\"},man_health_worker:{keywords:[\"doctor\",\"nurse\",\"therapist\",\"healthcare\",\"man\",\"human\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👨‍⚕️\" src=\"1f468-200d-2695-fe0f.png\"/>',fitzpatrick_scale:true,category:\"people\"},woman_farmer:{keywords:[\"rancher\",\"gardener\",\"woman\",\"human\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👩‍🌾\" src=\"1f469-200d-1f33e.png\"/>',fitzpatrick_scale:true,category:\"people\"},man_farmer:{keywords:[\"rancher\",\"gardener\",\"man\",\"human\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👨‍🌾\" src=\"1f468-200d-1f33e.png\"/>',fitzpatrick_scale:true,category:\"people\"},woman_cook:{keywords:[\"chef\",\"woman\",\"human\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👩‍🍳\" src=\"1f469-200d-1f373.png\"/>',fitzpatrick_scale:true,category:\"people\"},man_cook:{keywords:[\"chef\",\"man\",\"human\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👨‍🍳\" src=\"1f468-200d-1f373.png\"/>',fitzpatrick_scale:true,category:\"people\"},woman_student:{keywords:[\"graduate\",\"woman\",\"human\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👩‍🎓\" src=\"1f469-200d-1f393.png\"/>',fitzpatrick_scale:true,category:\"people\"},man_student:{keywords:[\"graduate\",\"man\",\"human\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👨‍🎓\" src=\"1f468-200d-1f393.png\"/>',fitzpatrick_scale:true,category:\"people\"},woman_singer:{keywords:[\"rockstar\",\"entertainer\",\"woman\",\"human\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👩‍🎤\" src=\"1f469-200d-1f3a4.png\"/>',fitzpatrick_scale:true,category:\"people\"},man_singer:{keywords:[\"rockstar\",\"entertainer\",\"man\",\"human\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👨‍🎤\" src=\"1f468-200d-1f3a4.png\"/>',fitzpatrick_scale:true,category:\"people\"},woman_teacher:{keywords:[\"instructor\",\"professor\",\"woman\",\"human\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👩‍🏫\" src=\"1f469-200d-1f3eb.png\"/>',fitzpatrick_scale:true,category:\"people\"},man_teacher:{keywords:[\"instructor\",\"professor\",\"man\",\"human\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👨‍🏫\" src=\"1f468-200d-1f3eb.png\"/>',fitzpatrick_scale:true,category:\"people\"},woman_factory_worker:{keywords:[\"assembly\",\"industrial\",\"woman\",\"human\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👩‍🏭\" src=\"1f469-200d-1f3ed.png\"/>',fitzpatrick_scale:true,category:\"people\"},man_factory_worker:{keywords:[\"assembly\",\"industrial\",\"man\",\"human\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👨‍🏭\" src=\"1f468-200d-1f3ed.png\"/>',fitzpatrick_scale:true,category:\"people\"},woman_technologist:{keywords:[\"coder\",\"developer\",\"engineer\",\"programmer\",\"software\",\"woman\",\"human\",\"laptop\",\"computer\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👩‍💻\" src=\"1f469-200d-1f4bb.png\"/>',fitzpatrick_scale:true,category:\"people\"},man_technologist:{keywords:[\"coder\",\"developer\",\"engineer\",\"programmer\",\"software\",\"man\",\"human\",\"laptop\",\"computer\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👨‍💻\" src=\"1f468-200d-1f4bb.png\"/>',fitzpatrick_scale:true,category:\"people\"},woman_office_worker:{keywords:[\"business\",\"manager\",\"woman\",\"human\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👩‍💼\" src=\"1f469-200d-1f4bc.png\"/>',fitzpatrick_scale:true,category:\"people\"},man_office_worker:{keywords:[\"business\",\"manager\",\"man\",\"human\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👨‍💼\" src=\"1f468-200d-1f4bc.png\"/>',fitzpatrick_scale:true,category:\"people\"},woman_mechanic:{keywords:[\"plumber\",\"woman\",\"human\",\"wrench\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👩‍🔧\" src=\"1f469-200d-1f527.png\"/>',fitzpatrick_scale:true,category:\"people\"},man_mechanic:{keywords:[\"plumber\",\"man\",\"human\",\"wrench\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👨‍🔧\" src=\"1f468-200d-1f527.png\"/>',fitzpatrick_scale:true,category:\"people\"},woman_scientist:{keywords:[\"biologist\",\"chemist\",\"engineer\",\"physicist\",\"woman\",\"human\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👩‍🔬\" src=\"1f469-200d-1f52c.png\"/>',fitzpatrick_scale:true,category:\"people\"},man_scientist:{keywords:[\"biologist\",\"chemist\",\"engineer\",\"physicist\",\"man\",\"human\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👨‍🔬\" src=\"1f468-200d-1f52c.png\"/>',fitzpatrick_scale:true,category:\"people\"},woman_artist:{keywords:[\"painter\",\"woman\",\"human\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👩‍🎨\" src=\"1f469-200d-1f3a8.png\"/>',fitzpatrick_scale:true,category:\"people\"},man_artist:{keywords:[\"painter\",\"man\",\"human\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👨‍🎨\" src=\"1f468-200d-1f3a8.png\"/>',fitzpatrick_scale:true,category:\"people\"},woman_firefighter:{keywords:[\"fireman\",\"woman\",\"human\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👩‍🚒\" src=\"1f469-200d-1f692.png\"/>',fitzpatrick_scale:true,category:\"people\"},man_firefighter:{keywords:[\"fireman\",\"man\",\"human\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👨‍🚒\" src=\"1f468-200d-1f692.png\"/>',fitzpatrick_scale:true,category:\"people\"},woman_pilot:{keywords:[\"aviator\",\"plane\",\"woman\",\"human\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👩‍✈️\" src=\"1f469-200d-2708-fe0f.png\"/>',fitzpatrick_scale:true,category:\"people\"},man_pilot:{keywords:[\"aviator\",\"plane\",\"man\",\"human\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👨‍✈️\" src=\"1f468-200d-2708-fe0f.png\"/>',fitzpatrick_scale:true,category:\"people\"},woman_astronaut:{keywords:[\"space\",\"rocket\",\"woman\",\"human\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👩‍🚀\" src=\"1f469-200d-1f680.png\"/>',fitzpatrick_scale:true,category:\"people\"},man_astronaut:{keywords:[\"space\",\"rocket\",\"man\",\"human\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👨‍🚀\" src=\"1f468-200d-1f680.png\"/>',fitzpatrick_scale:true,category:\"people\"},woman_judge:{keywords:[\"justice\",\"court\",\"woman\",\"human\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👩‍⚖️\" src=\"1f469-200d-2696-fe0f.png\"/>',fitzpatrick_scale:true,category:\"people\"},man_judge:{keywords:[\"justice\",\"court\",\"man\",\"human\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👨‍⚖️\" src=\"1f468-200d-2696-fe0f.png\"/>',fitzpatrick_scale:true,category:\"people\"},woman_superhero:{keywords:[\"woman\",\"female\",\"good\",\"heroine\",\"superpowers\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🦸‍♀️\" src=\"1f9b8-200d-2640-fe0f.png\"/>',fitzpatrick_scale:true,category:\"people\"},man_superhero:{keywords:[\"man\",\"male\",\"good\",\"hero\",\"superpowers\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🦸‍♂️\" src=\"1f9b8-200d-2642-fe0f.png\"/>',fitzpatrick_scale:true,category:\"people\"},woman_supervillain:{keywords:[\"woman\",\"female\",\"evil\",\"bad\",\"criminal\",\"heroine\",\"superpowers\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🦹‍♀️\" src=\"1f9b9-200d-2640-fe0f.png\"/>',fitzpatrick_scale:true,category:\"people\"},man_supervillain:{keywords:[\"man\",\"male\",\"evil\",\"bad\",\"criminal\",\"hero\",\"superpowers\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🦹‍♂️\" src=\"1f9b9-200d-2642-fe0f.png\"/>',fitzpatrick_scale:true,category:\"people\"},mrs_claus:{keywords:[\"woman\",\"female\",\"xmas\",\"mother christmas\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🤶\" src=\"1f936.png\"/>',fitzpatrick_scale:true,category:\"people\"},santa:{keywords:[\"festival\",\"man\",\"male\",\"xmas\",\"father christmas\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🎅\" src=\"1f385.png\"/>',fitzpatrick_scale:true,category:\"people\"},sorceress:{keywords:[\"woman\",\"female\",\"mage\",\"witch\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🧙‍♀️\" src=\"1f9d9-200d-2640-fe0f.png\"/>',fitzpatrick_scale:true,category:\"people\"},wizard:{keywords:[\"man\",\"male\",\"mage\",\"sorcerer\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🧙‍♂️\" src=\"1f9d9-200d-2642-fe0f.png\"/>',fitzpatrick_scale:true,category:\"people\"},woman_elf:{keywords:[\"woman\",\"female\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🧝‍♀️\" src=\"1f9dd-200d-2640-fe0f.png\"/>',fitzpatrick_scale:true,category:\"people\"},man_elf:{keywords:[\"man\",\"male\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🧝‍♂️\" src=\"1f9dd-200d-2642-fe0f.png\"/>',fitzpatrick_scale:true,category:\"people\"},woman_vampire:{keywords:[\"woman\",\"female\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🧛‍♀️\" src=\"1f9db-200d-2640-fe0f.png\"/>',fitzpatrick_scale:true,category:\"people\"},man_vampire:{keywords:[\"man\",\"male\",\"dracula\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🧛‍♂️\" src=\"1f9db-200d-2642-fe0f.png\"/>',fitzpatrick_scale:true,category:\"people\"},woman_zombie:{keywords:[\"woman\",\"female\",\"undead\",\"walking dead\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🧟‍♀️\" src=\"1f9df-200d-2640-fe0f.png\"/>',fitzpatrick_scale:false,category:\"people\"},man_zombie:{keywords:[\"man\",\"male\",\"dracula\",\"undead\",\"walking dead\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🧟‍♂️\" src=\"1f9df-200d-2642-fe0f.png\"/>',fitzpatrick_scale:false,category:\"people\"},woman_genie:{keywords:[\"woman\",\"female\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🧞‍♀️\" src=\"1f9de-200d-2640-fe0f.png\"/>',fitzpatrick_scale:false,category:\"people\"},man_genie:{keywords:[\"man\",\"male\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🧞‍♂️\" src=\"1f9de-200d-2642-fe0f.png\"/>',fitzpatrick_scale:false,category:\"people\"},mermaid:{keywords:[\"woman\",\"female\",\"merwoman\",\"ariel\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🧜‍♀️\" src=\"1f9dc-200d-2640-fe0f.png\"/>',fitzpatrick_scale:true,category:\"people\"},merman:{keywords:[\"man\",\"male\",\"triton\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🧜‍♂️\" src=\"1f9dc-200d-2642-fe0f.png\"/>',fitzpatrick_scale:true,category:\"people\"},woman_fairy:{keywords:[\"woman\",\"female\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🧚‍♀️\" src=\"1f9da-200d-2640-fe0f.png\"/>',fitzpatrick_scale:true,category:\"people\"},man_fairy:{keywords:[\"man\",\"male\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🧚‍♂️\" src=\"1f9da-200d-2642-fe0f.png\"/>',fitzpatrick_scale:true,category:\"people\"},angel:{keywords:[\"heaven\",\"wings\",\"halo\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👼\" src=\"1f47c.png\"/>',fitzpatrick_scale:true,category:\"people\"},pregnant_woman:{keywords:[\"baby\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🤰\" src=\"1f930.png\"/>',fitzpatrick_scale:true,category:\"people\"},breastfeeding:{keywords:[\"nursing\",\"baby\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🤱\" src=\"1f931.png\"/>',fitzpatrick_scale:true,category:\"people\"},princess:{keywords:[\"girl\",\"woman\",\"female\",\"blond\",\"crown\",\"royal\",\"queen\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👸\" src=\"1f478.png\"/>',fitzpatrick_scale:true,category:\"people\"},prince:{keywords:[\"boy\",\"man\",\"male\",\"crown\",\"royal\",\"king\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🤴\" src=\"1f934.png\"/>',fitzpatrick_scale:true,category:\"people\"},bride_with_veil:{keywords:[\"couple\",\"marriage\",\"wedding\",\"woman\",\"bride\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👰\" src=\"1f470.png\"/>',fitzpatrick_scale:true,category:\"people\"},man_in_tuxedo:{keywords:[\"couple\",\"marriage\",\"wedding\",\"groom\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🤵\" src=\"1f935.png\"/>',fitzpatrick_scale:true,category:\"people\"},running_woman:{keywords:[\"woman\",\"walking\",\"exercise\",\"race\",\"running\",\"female\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🏃‍♀️\" src=\"1f3c3-200d-2640-fe0f.png\"/>',fitzpatrick_scale:true,category:\"people\"},running_man:{keywords:[\"man\",\"walking\",\"exercise\",\"race\",\"running\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🏃\" src=\"1f3c3.png\"/>',fitzpatrick_scale:true,category:\"people\"},walking_woman:{keywords:[\"human\",\"feet\",\"steps\",\"woman\",\"female\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🚶‍♀️\" src=\"1f6b6-200d-2640-fe0f.png\"/>',fitzpatrick_scale:true,category:\"people\"},walking_man:{keywords:[\"human\",\"feet\",\"steps\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🚶\" src=\"1f6b6.png\"/>',fitzpatrick_scale:true,category:\"people\"},dancer:{keywords:[\"female\",\"girl\",\"woman\",\"fun\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"💃\" src=\"1f483.png\"/>',fitzpatrick_scale:true,category:\"people\"},man_dancing:{keywords:[\"male\",\"boy\",\"fun\",\"dancer\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🕺\" src=\"1f57a.png\"/>',fitzpatrick_scale:true,category:\"people\"},dancing_women:{keywords:[\"female\",\"bunny\",\"women\",\"girls\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👯\" src=\"1f46f.png\"/>',fitzpatrick_scale:false,category:\"people\"},dancing_men:{keywords:[\"male\",\"bunny\",\"men\",\"boys\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👯‍♂️\" src=\"1f46f-200d-2642-fe0f.png\"/>',fitzpatrick_scale:false,category:\"people\"},couple:{keywords:[\"pair\",\"people\",\"human\",\"love\",\"date\",\"dating\",\"like\",\"affection\",\"valentines\",\"marriage\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👫\" src=\"1f46b.png\"/>',fitzpatrick_scale:false,category:\"people\"},two_men_holding_hands:{keywords:[\"pair\",\"couple\",\"love\",\"like\",\"bromance\",\"friendship\",\"people\",\"human\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👬\" src=\"1f46c.png\"/>',fitzpatrick_scale:false,category:\"people\"},two_women_holding_hands:{keywords:[\"pair\",\"friendship\",\"couple\",\"love\",\"like\",\"female\",\"people\",\"human\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👭\" src=\"1f46d.png\"/>',fitzpatrick_scale:false,category:\"people\"},bowing_woman:{keywords:[\"woman\",\"female\",\"girl\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🙇‍♀️\" src=\"1f647-200d-2640-fe0f.png\"/>',fitzpatrick_scale:true,category:\"people\"},bowing_man:{keywords:[\"man\",\"male\",\"boy\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🙇\" src=\"1f647.png\"/>',fitzpatrick_scale:true,category:\"people\"},man_facepalming:{keywords:[\"man\",\"male\",\"boy\",\"disbelief\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🤦‍♂️\" src=\"1f926-200d-2642-fe0f.png\"/>',fitzpatrick_scale:true,category:\"people\"},woman_facepalming:{keywords:[\"woman\",\"female\",\"girl\",\"disbelief\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🤦‍♀️\" src=\"1f926-200d-2640-fe0f.png\"/>',fitzpatrick_scale:true,category:\"people\"},woman_shrugging:{keywords:[\"woman\",\"female\",\"girl\",\"confused\",\"indifferent\",\"doubt\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🤷\" src=\"1f937.png\"/>',fitzpatrick_scale:true,category:\"people\"},man_shrugging:{keywords:[\"man\",\"male\",\"boy\",\"confused\",\"indifferent\",\"doubt\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🤷‍♂️\" src=\"1f937-200d-2642-fe0f.png\"/>',fitzpatrick_scale:true,category:\"people\"},tipping_hand_woman:{keywords:[\"female\",\"girl\",\"woman\",\"human\",\"information\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"💁\" src=\"1f481.png\"/>',fitzpatrick_scale:true,category:\"people\"},tipping_hand_man:{keywords:[\"male\",\"boy\",\"man\",\"human\",\"information\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"💁‍♂️\" src=\"1f481-200d-2642-fe0f.png\"/>',fitzpatrick_scale:true,category:\"people\"},no_good_woman:{keywords:[\"female\",\"girl\",\"woman\",\"nope\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🙅\" src=\"1f645.png\"/>',fitzpatrick_scale:true,category:\"people\"},no_good_man:{keywords:[\"male\",\"boy\",\"man\",\"nope\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🙅‍♂️\" src=\"1f645-200d-2642-fe0f.png\"/>',fitzpatrick_scale:true,category:\"people\"},ok_woman:{keywords:[\"women\",\"girl\",\"female\",\"pink\",\"human\",\"woman\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🙆\" src=\"1f646.png\"/>',fitzpatrick_scale:true,category:\"people\"},ok_man:{keywords:[\"men\",\"boy\",\"male\",\"blue\",\"human\",\"man\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🙆‍♂️\" src=\"1f646-200d-2642-fe0f.png\"/>',fitzpatrick_scale:true,category:\"people\"},raising_hand_woman:{keywords:[\"female\",\"girl\",\"woman\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🙋\" src=\"1f64b.png\"/>',fitzpatrick_scale:true,category:\"people\"},raising_hand_man:{keywords:[\"male\",\"boy\",\"man\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🙋‍♂️\" src=\"1f64b-200d-2642-fe0f.png\"/>',fitzpatrick_scale:true,category:\"people\"},pouting_woman:{keywords:[\"female\",\"girl\",\"woman\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🙎\" src=\"1f64e.png\"/>',fitzpatrick_scale:true,category:\"people\"},pouting_man:{keywords:[\"male\",\"boy\",\"man\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🙎‍♂️\" src=\"1f64e-200d-2642-fe0f.png\"/>',fitzpatrick_scale:true,category:\"people\"},frowning_woman:{keywords:[\"female\",\"girl\",\"woman\",\"sad\",\"depressed\",\"discouraged\",\"unhappy\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🙍\" src=\"1f64d.png\"/>',fitzpatrick_scale:true,category:\"people\"},frowning_man:{keywords:[\"male\",\"boy\",\"man\",\"sad\",\"depressed\",\"discouraged\",\"unhappy\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🙍‍♂️\" src=\"1f64d-200d-2642-fe0f.png\"/>',fitzpatrick_scale:true,category:\"people\"},haircut_woman:{keywords:[\"female\",\"girl\",\"woman\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"💇\" src=\"1f487.png\"/>',fitzpatrick_scale:true,category:\"people\"},haircut_man:{keywords:[\"male\",\"boy\",\"man\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"💇‍♂️\" src=\"1f487-200d-2642-fe0f.png\"/>',fitzpatrick_scale:true,category:\"people\"},massage_woman:{keywords:[\"female\",\"girl\",\"woman\",\"head\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"💆\" src=\"1f486.png\"/>',fitzpatrick_scale:true,category:\"people\"},massage_man:{keywords:[\"male\",\"boy\",\"man\",\"head\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"💆‍♂️\" src=\"1f486-200d-2642-fe0f.png\"/>',fitzpatrick_scale:true,category:\"people\"},woman_in_steamy_room:{keywords:[\"female\",\"woman\",\"spa\",\"steamroom\",\"sauna\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🧖‍♀️\" src=\"1f9d6-200d-2640-fe0f.png\"/>',fitzpatrick_scale:true,category:\"people\"},man_in_steamy_room:{keywords:[\"male\",\"man\",\"spa\",\"steamroom\",\"sauna\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🧖‍♂️\" src=\"1f9d6-200d-2642-fe0f.png\"/>',fitzpatrick_scale:true,category:\"people\"},couple_with_heart_woman_man:{keywords:[\"pair\",\"love\",\"like\",\"affection\",\"human\",\"dating\",\"valentines\",\"marriage\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"💑\" src=\"1f491.png\"/>',fitzpatrick_scale:false,category:\"people\"},couple_with_heart_woman_woman:{keywords:[\"pair\",\"love\",\"like\",\"affection\",\"human\",\"dating\",\"valentines\",\"marriage\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👩‍❤️‍👩\" src=\"1f469-200d-2764-fe0f-200d-1f469.png\"/>',fitzpatrick_scale:false,category:\"people\"},couple_with_heart_man_man:{keywords:[\"pair\",\"love\",\"like\",\"affection\",\"human\",\"dating\",\"valentines\",\"marriage\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👨‍❤️‍👨\" src=\"1f468-200d-2764-fe0f-200d-1f468.png\"/>',fitzpatrick_scale:false,category:\"people\"},couplekiss_man_woman:{keywords:[\"pair\",\"valentines\",\"love\",\"like\",\"dating\",\"marriage\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"💏\" src=\"1f48f.png\"/>',fitzpatrick_scale:false,category:\"people\"},couplekiss_woman_woman:{keywords:[\"pair\",\"valentines\",\"love\",\"like\",\"dating\",\"marriage\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👩‍❤️‍💋‍👩\" src=\"1f469-200d-2764-fe0f-200d-1f48b-200d-1f469.png\"/>',fitzpatrick_scale:false,category:\"people\"},couplekiss_man_man:{keywords:[\"pair\",\"valentines\",\"love\",\"like\",\"dating\",\"marriage\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👨‍❤️‍💋‍👨\" src=\"1f468-200d-2764-fe0f-200d-1f48b-200d-1f468.png\"/>',fitzpatrick_scale:false,category:\"people\"},family_man_woman_boy:{keywords:[\"home\",\"parents\",\"child\",\"mom\",\"dad\",\"father\",\"mother\",\"people\",\"human\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👪\" src=\"1f46a.png\"/>',fitzpatrick_scale:false,category:\"people\"},family_man_woman_girl:{keywords:[\"home\",\"parents\",\"people\",\"human\",\"child\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👨‍👩‍👧\" src=\"1f468-200d-1f469-200d-1f467.png\"/>',fitzpatrick_scale:false,category:\"people\"},family_man_woman_girl_boy:{keywords:[\"home\",\"parents\",\"people\",\"human\",\"children\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👨‍👩‍👧‍👦\" src=\"1f468-200d-1f469-200d-1f467-200d-1f466.png\"/>',fitzpatrick_scale:false,category:\"people\"},family_man_woman_boy_boy:{keywords:[\"home\",\"parents\",\"people\",\"human\",\"children\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👨‍👩‍👦‍👦\" src=\"1f468-200d-1f469-200d-1f466-200d-1f466.png\"/>',fitzpatrick_scale:false,category:\"people\"},family_man_woman_girl_girl:{keywords:[\"home\",\"parents\",\"people\",\"human\",\"children\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👨‍👩‍👧‍👧\" src=\"1f468-200d-1f469-200d-1f467-200d-1f467.png\"/>',fitzpatrick_scale:false,category:\"people\"},family_woman_woman_boy:{keywords:[\"home\",\"parents\",\"people\",\"human\",\"children\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👩‍👩‍👦\" src=\"1f469-200d-1f469-200d-1f466.png\"/>',fitzpatrick_scale:false,category:\"people\"},family_woman_woman_girl:{keywords:[\"home\",\"parents\",\"people\",\"human\",\"children\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👩‍👩‍👧\" src=\"1f469-200d-1f469-200d-1f467.png\"/>',fitzpatrick_scale:false,category:\"people\"},family_woman_woman_girl_boy:{keywords:[\"home\",\"parents\",\"people\",\"human\",\"children\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👩‍👩‍👧‍👦\" src=\"1f469-200d-1f469-200d-1f467-200d-1f466.png\"/>',fitzpatrick_scale:false,category:\"people\"},family_woman_woman_boy_boy:{keywords:[\"home\",\"parents\",\"people\",\"human\",\"children\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👩‍👩‍👦‍👦\" src=\"1f469-200d-1f469-200d-1f466-200d-1f466.png\"/>',fitzpatrick_scale:false,category:\"people\"},family_woman_woman_girl_girl:{keywords:[\"home\",\"parents\",\"people\",\"human\",\"children\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👩‍👩‍👧‍👧\" src=\"1f469-200d-1f469-200d-1f467-200d-1f467.png\"/>',fitzpatrick_scale:false,category:\"people\"},family_man_man_boy:{keywords:[\"home\",\"parents\",\"people\",\"human\",\"children\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👨‍👨‍👦\" src=\"1f468-200d-1f468-200d-1f466.png\"/>',fitzpatrick_scale:false,category:\"people\"},family_man_man_girl:{keywords:[\"home\",\"parents\",\"people\",\"human\",\"children\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👨‍👨‍👧\" src=\"1f468-200d-1f468-200d-1f467.png\"/>',fitzpatrick_scale:false,category:\"people\"},family_man_man_girl_boy:{keywords:[\"home\",\"parents\",\"people\",\"human\",\"children\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👨‍👨‍👧‍👦\" src=\"1f468-200d-1f468-200d-1f467-200d-1f466.png\"/>',fitzpatrick_scale:false,category:\"people\"},family_man_man_boy_boy:{keywords:[\"home\",\"parents\",\"people\",\"human\",\"children\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👨‍👨‍👦‍👦\" src=\"1f468-200d-1f468-200d-1f466-200d-1f466.png\"/>',fitzpatrick_scale:false,category:\"people\"},family_man_man_girl_girl:{keywords:[\"home\",\"parents\",\"people\",\"human\",\"children\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👨‍👨‍👧‍👧\" src=\"1f468-200d-1f468-200d-1f467-200d-1f467.png\"/>',fitzpatrick_scale:false,category:\"people\"},family_woman_boy:{keywords:[\"home\",\"parent\",\"people\",\"human\",\"child\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👩‍👦\" src=\"1f469-200d-1f466.png\"/>',fitzpatrick_scale:false,category:\"people\"},family_woman_girl:{keywords:[\"home\",\"parent\",\"people\",\"human\",\"child\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👩‍👧\" src=\"1f469-200d-1f467.png\"/>',fitzpatrick_scale:false,category:\"people\"},family_woman_girl_boy:{keywords:[\"home\",\"parent\",\"people\",\"human\",\"children\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👩‍👧‍👦\" src=\"1f469-200d-1f467-200d-1f466.png\"/>',fitzpatrick_scale:false,category:\"people\"},family_woman_boy_boy:{keywords:[\"home\",\"parent\",\"people\",\"human\",\"children\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👩‍👦‍👦\" src=\"1f469-200d-1f466-200d-1f466.png\"/>',fitzpatrick_scale:false,category:\"people\"},family_woman_girl_girl:{keywords:[\"home\",\"parent\",\"people\",\"human\",\"children\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👩‍👧‍👧\" src=\"1f469-200d-1f467-200d-1f467.png\"/>',fitzpatrick_scale:false,category:\"people\"},family_man_boy:{keywords:[\"home\",\"parent\",\"people\",\"human\",\"child\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👨‍👦\" src=\"1f468-200d-1f466.png\"/>',fitzpatrick_scale:false,category:\"people\"},family_man_girl:{keywords:[\"home\",\"parent\",\"people\",\"human\",\"child\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👨‍👧\" src=\"1f468-200d-1f467.png\"/>',fitzpatrick_scale:false,category:\"people\"},family_man_girl_boy:{keywords:[\"home\",\"parent\",\"people\",\"human\",\"children\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👨‍👧‍👦\" src=\"1f468-200d-1f467-200d-1f466.png\"/>',fitzpatrick_scale:false,category:\"people\"},family_man_boy_boy:{keywords:[\"home\",\"parent\",\"people\",\"human\",\"children\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👨‍👦‍👦\" src=\"1f468-200d-1f466-200d-1f466.png\"/>',fitzpatrick_scale:false,category:\"people\"},family_man_girl_girl:{keywords:[\"home\",\"parent\",\"people\",\"human\",\"children\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👨‍👧‍👧\" src=\"1f468-200d-1f467-200d-1f467.png\"/>',fitzpatrick_scale:false,category:\"people\"},yarn:{keywords:[\"ball\",\"crochet\",\"knit\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🧶\" src=\"1f9f6.png\"/>',fitzpatrick_scale:false,category:\"people\"},thread:{keywords:[\"needle\",\"sewing\",\"spool\",\"string\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🧵\" src=\"1f9f5.png\"/>',fitzpatrick_scale:false,category:\"people\"},coat:{keywords:[\"jacket\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🧥\" src=\"1f9e5.png\"/>',fitzpatrick_scale:false,category:\"people\"},labcoat:{keywords:[\"doctor\",\"experiment\",\"scientist\",\"chemist\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🥼\" src=\"1f97c.png\"/>',fitzpatrick_scale:false,category:\"people\"},womans_clothes:{keywords:[\"fashion\",\"shopping_bags\",\"female\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👚\" src=\"1f45a.png\"/>',fitzpatrick_scale:false,category:\"people\"},tshirt:{keywords:[\"fashion\",\"cloth\",\"casual\",\"shirt\",\"tee\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👕\" src=\"1f455.png\"/>',fitzpatrick_scale:false,category:\"people\"},jeans:{keywords:[\"fashion\",\"shopping\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👖\" src=\"1f456.png\"/>',fitzpatrick_scale:false,category:\"people\"},necktie:{keywords:[\"shirt\",\"suitup\",\"formal\",\"fashion\",\"cloth\",\"business\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👔\" src=\"1f454.png\"/>',fitzpatrick_scale:false,category:\"people\"},dress:{keywords:[\"clothes\",\"fashion\",\"shopping\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👗\" src=\"1f457.png\"/>',fitzpatrick_scale:false,category:\"people\"},bikini:{keywords:[\"swimming\",\"female\",\"woman\",\"girl\",\"fashion\",\"beach\",\"summer\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👙\" src=\"1f459.png\"/>',fitzpatrick_scale:false,category:\"people\"},kimono:{keywords:[\"dress\",\"fashion\",\"women\",\"female\",\"japanese\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👘\" src=\"1f458.png\"/>',fitzpatrick_scale:false,category:\"people\"},lipstick:{keywords:[\"female\",\"girl\",\"fashion\",\"woman\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"💄\" src=\"1f484.png\"/>',fitzpatrick_scale:false,category:\"people\"},kiss:{keywords:[\"face\",\"lips\",\"love\",\"like\",\"affection\",\"valentines\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"💋\" src=\"1f48b.png\"/>',fitzpatrick_scale:false,category:\"people\"},footprints:{keywords:[\"feet\",\"tracking\",\"walking\",\"beach\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👣\" src=\"1f463.png\"/>',fitzpatrick_scale:false,category:\"people\"},flat_shoe:{keywords:[\"ballet\",\"slip-on\",\"slipper\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🥿\" src=\"1f97f.png\"/>',fitzpatrick_scale:false,category:\"people\"},high_heel:{keywords:[\"fashion\",\"shoes\",\"female\",\"pumps\",\"stiletto\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👠\" src=\"1f460.png\"/>',fitzpatrick_scale:false,category:\"people\"},sandal:{keywords:[\"shoes\",\"fashion\",\"flip flops\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👡\" src=\"1f461.png\"/>',fitzpatrick_scale:false,category:\"people\"},boot:{keywords:[\"shoes\",\"fashion\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👢\" src=\"1f462.png\"/>',fitzpatrick_scale:false,category:\"people\"},mans_shoe:{keywords:[\"fashion\",\"male\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👞\" src=\"1f45e.png\"/>',fitzpatrick_scale:false,category:\"people\"},athletic_shoe:{keywords:[\"shoes\",\"sports\",\"sneakers\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👟\" src=\"1f45f.png\"/>',fitzpatrick_scale:false,category:\"people\"},hiking_boot:{keywords:[\"backpacking\",\"camping\",\"hiking\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🥾\" src=\"1f97e.png\"/>',fitzpatrick_scale:false,category:\"people\"},socks:{keywords:[\"stockings\",\"clothes\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🧦\" src=\"1f9e6.png\"/>',fitzpatrick_scale:false,category:\"people\"},gloves:{keywords:[\"hands\",\"winter\",\"clothes\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🧤\" src=\"1f9e4.png\"/>',fitzpatrick_scale:false,category:\"people\"},scarf:{keywords:[\"neck\",\"winter\",\"clothes\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🧣\" src=\"1f9e3.png\"/>',fitzpatrick_scale:false,category:\"people\"},womans_hat:{keywords:[\"fashion\",\"accessories\",\"female\",\"lady\",\"spring\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👒\" src=\"1f452.png\"/>',fitzpatrick_scale:false,category:\"people\"},tophat:{keywords:[\"magic\",\"gentleman\",\"classy\",\"circus\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🎩\" src=\"1f3a9.png\"/>',fitzpatrick_scale:false,category:\"people\"},billed_hat:{keywords:[\"cap\",\"baseball\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🧢\" src=\"1f9e2.png\"/>',fitzpatrick_scale:false,category:\"people\"},rescue_worker_helmet:{keywords:[\"construction\",\"build\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"⛑\" src=\"26d1.png\"/>',fitzpatrick_scale:false,category:\"people\"},mortar_board:{keywords:[\"school\",\"college\",\"degree\",\"university\",\"graduation\",\"cap\",\"hat\",\"legal\",\"learn\",\"education\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🎓\" src=\"1f393.png\"/>',fitzpatrick_scale:false,category:\"people\"},crown:{keywords:[\"king\",\"kod\",\"leader\",\"royalty\",\"lord\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👑\" src=\"1f451.png\"/>',fitzpatrick_scale:false,category:\"people\"},school_satchel:{keywords:[\"student\",\"education\",\"bag\",\"backpack\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🎒\" src=\"1f392.png\"/>',fitzpatrick_scale:false,category:\"people\"},luggage:{keywords:[\"packing\",\"travel\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🧳\" src=\"1f9f3.png\"/>',fitzpatrick_scale:false,category:\"people\"},pouch:{keywords:[\"bag\",\"accessories\",\"shopping\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👝\" src=\"1f45d.png\"/>',fitzpatrick_scale:false,category:\"people\"},purse:{keywords:[\"fashion\",\"accessories\",\"money\",\"sales\",\"shopping\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👛\" src=\"1f45b.png\"/>',fitzpatrick_scale:false,category:\"people\"},handbag:{keywords:[\"fashion\",\"accessory\",\"accessories\",\"shopping\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👜\" src=\"1f45c.png\"/>',fitzpatrick_scale:false,category:\"people\"},briefcase:{keywords:[\"business\",\"documents\",\"work\",\"law\",\"legal\",\"job\",\"career\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"💼\" src=\"1f4bc.png\"/>',fitzpatrick_scale:false,category:\"people\"},eyeglasses:{keywords:[\"fashion\",\"accessories\",\"eyesight\",\"nerdy\",\"dork\",\"geek\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👓\" src=\"1f453.png\"/>',fitzpatrick_scale:false,category:\"people\"},dark_sunglasses:{keywords:[\"face\",\"cool\",\"accessories\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🕶\" src=\"1f576.png\"/>',fitzpatrick_scale:false,category:\"people\"},goggles:{keywords:[\"eyes\",\"protection\",\"safety\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🥽\" src=\"1f97d.png\"/>',fitzpatrick_scale:false,category:\"people\"},ring:{keywords:[\"wedding\",\"propose\",\"marriage\",\"valentines\",\"diamond\",\"fashion\",\"jewelry\",\"gem\",\"engagement\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"💍\" src=\"1f48d.png\"/>',fitzpatrick_scale:false,category:\"people\"},closed_umbrella:{keywords:[\"weather\",\"rain\",\"drizzle\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🌂\" src=\"1f302.png\"/>',fitzpatrick_scale:false,category:\"people\"},dog:{keywords:[\"animal\",\"friend\",\"nature\",\"woof\",\"puppy\",\"pet\",\"faithful\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🐶\" src=\"1f436.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},cat:{keywords:[\"animal\",\"meow\",\"nature\",\"pet\",\"kitten\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🐱\" src=\"1f431.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},mouse:{keywords:[\"animal\",\"nature\",\"cheese_wedge\",\"rodent\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🐭\" src=\"1f42d.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},hamster:{keywords:[\"animal\",\"nature\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🐹\" src=\"1f439.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},rabbit:{keywords:[\"animal\",\"nature\",\"pet\",\"spring\",\"magic\",\"bunny\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🐰\" src=\"1f430.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},fox_face:{keywords:[\"animal\",\"nature\",\"face\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🦊\" src=\"1f98a.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},bear:{keywords:[\"animal\",\"nature\",\"wild\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🐻\" src=\"1f43b.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},panda_face:{keywords:[\"animal\",\"nature\",\"panda\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🐼\" src=\"1f43c.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},koala:{keywords:[\"animal\",\"nature\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🐨\" src=\"1f428.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},tiger:{keywords:[\"animal\",\"cat\",\"danger\",\"wild\",\"nature\",\"roar\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🐯\" src=\"1f42f.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},lion:{keywords:[\"animal\",\"nature\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🦁\" src=\"1f981.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},cow:{keywords:[\"beef\",\"ox\",\"animal\",\"nature\",\"moo\",\"milk\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🐮\" src=\"1f42e.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},pig:{keywords:[\"animal\",\"oink\",\"nature\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🐷\" src=\"1f437.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},pig_nose:{keywords:[\"animal\",\"oink\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🐽\" src=\"1f43d.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},frog:{keywords:[\"animal\",\"nature\",\"croak\",\"toad\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🐸\" src=\"1f438.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},squid:{keywords:[\"animal\",\"nature\",\"ocean\",\"sea\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🦑\" src=\"1f991.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},octopus:{keywords:[\"animal\",\"creature\",\"ocean\",\"sea\",\"nature\",\"beach\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🐙\" src=\"1f419.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},shrimp:{keywords:[\"animal\",\"ocean\",\"nature\",\"seafood\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🦐\" src=\"1f990.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},monkey_face:{keywords:[\"animal\",\"nature\",\"circus\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🐵\" src=\"1f435.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},gorilla:{keywords:[\"animal\",\"nature\",\"circus\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🦍\" src=\"1f98d.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},see_no_evil:{keywords:[\"monkey\",\"animal\",\"nature\",\"haha\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🙈\" src=\"1f648.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},hear_no_evil:{keywords:[\"animal\",\"monkey\",\"nature\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🙉\" src=\"1f649.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},speak_no_evil:{keywords:[\"monkey\",\"animal\",\"nature\",\"omg\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🙊\" src=\"1f64a.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},monkey:{keywords:[\"animal\",\"nature\",\"banana\",\"circus\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🐒\" src=\"1f412.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},chicken:{keywords:[\"animal\",\"cluck\",\"nature\",\"bird\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🐔\" src=\"1f414.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},penguin:{keywords:[\"animal\",\"nature\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🐧\" src=\"1f427.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},bird:{keywords:[\"animal\",\"nature\",\"fly\",\"tweet\",\"spring\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🐦\" src=\"1f426.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},baby_chick:{keywords:[\"animal\",\"chicken\",\"bird\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🐤\" src=\"1f424.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},hatching_chick:{keywords:[\"animal\",\"chicken\",\"egg\",\"born\",\"baby\",\"bird\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🐣\" src=\"1f423.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},hatched_chick:{keywords:[\"animal\",\"chicken\",\"baby\",\"bird\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🐥\" src=\"1f425.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},duck:{keywords:[\"animal\",\"nature\",\"bird\",\"mallard\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🦆\" src=\"1f986.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},eagle:{keywords:[\"animal\",\"nature\",\"bird\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🦅\" src=\"1f985.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},owl:{keywords:[\"animal\",\"nature\",\"bird\",\"hoot\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🦉\" src=\"1f989.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},bat:{keywords:[\"animal\",\"nature\",\"blind\",\"vampire\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🦇\" src=\"1f987.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},wolf:{keywords:[\"animal\",\"nature\",\"wild\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🐺\" src=\"1f43a.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},boar:{keywords:[\"animal\",\"nature\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🐗\" src=\"1f417.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},horse:{keywords:[\"animal\",\"brown\",\"nature\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🐴\" src=\"1f434.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},unicorn:{keywords:[\"animal\",\"nature\",\"mystical\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🦄\" src=\"1f984.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},honeybee:{keywords:[\"animal\",\"insect\",\"nature\",\"bug\",\"spring\",\"honey\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🐝\" src=\"1f41d.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},bug:{keywords:[\"animal\",\"insect\",\"nature\",\"worm\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🐛\" src=\"1f41b.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},butterfly:{keywords:[\"animal\",\"insect\",\"nature\",\"caterpillar\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🦋\" src=\"1f98b.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},snail:{keywords:[\"slow\",\"animal\",\"shell\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🐌\" src=\"1f40c.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},beetle:{keywords:[\"animal\",\"insect\",\"nature\",\"ladybug\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🐞\" src=\"1f41e.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},ant:{keywords:[\"animal\",\"insect\",\"nature\",\"bug\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🐜\" src=\"1f41c.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},grasshopper:{keywords:[\"animal\",\"cricket\",\"chirp\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🦗\" src=\"1f997.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},spider:{keywords:[\"animal\",\"arachnid\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🕷\" src=\"1f577.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},scorpion:{keywords:[\"animal\",\"arachnid\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🦂\" src=\"1f982.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},crab:{keywords:[\"animal\",\"crustacean\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🦀\" src=\"1f980.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},snake:{keywords:[\"animal\",\"evil\",\"nature\",\"hiss\",\"python\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🐍\" src=\"1f40d.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},lizard:{keywords:[\"animal\",\"nature\",\"reptile\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🦎\" src=\"1f98e.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},\"t-rex\":{keywords:[\"animal\",\"nature\",\"dinosaur\",\"tyrannosaurus\",\"extinct\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🦖\" src=\"1f996.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},sauropod:{keywords:[\"animal\",\"nature\",\"dinosaur\",\"brachiosaurus\",\"brontosaurus\",\"diplodocus\",\"extinct\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🦕\" src=\"1f995.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},turtle:{keywords:[\"animal\",\"slow\",\"nature\",\"tortoise\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🐢\" src=\"1f422.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},tropical_fish:{keywords:[\"animal\",\"swim\",\"ocean\",\"beach\",\"nemo\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🐠\" src=\"1f420.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},fish:{keywords:[\"animal\",\"food\",\"nature\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🐟\" src=\"1f41f.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},blowfish:{keywords:[\"animal\",\"nature\",\"food\",\"sea\",\"ocean\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🐡\" src=\"1f421.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},dolphin:{keywords:[\"animal\",\"nature\",\"fish\",\"sea\",\"ocean\",\"flipper\",\"fins\",\"beach\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🐬\" src=\"1f42c.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},shark:{keywords:[\"animal\",\"nature\",\"fish\",\"sea\",\"ocean\",\"jaws\",\"fins\",\"beach\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🦈\" src=\"1f988.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},whale:{keywords:[\"animal\",\"nature\",\"sea\",\"ocean\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🐳\" src=\"1f433.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},whale2:{keywords:[\"animal\",\"nature\",\"sea\",\"ocean\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🐋\" src=\"1f40b.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},crocodile:{keywords:[\"animal\",\"nature\",\"reptile\",\"lizard\",\"alligator\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🐊\" src=\"1f40a.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},leopard:{keywords:[\"animal\",\"nature\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🐆\" src=\"1f406.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},zebra:{keywords:[\"animal\",\"nature\",\"stripes\",\"safari\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🦓\" src=\"1f993.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},tiger2:{keywords:[\"animal\",\"nature\",\"roar\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🐅\" src=\"1f405.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},water_buffalo:{keywords:[\"animal\",\"nature\",\"ox\",\"cow\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🐃\" src=\"1f403.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},ox:{keywords:[\"animal\",\"cow\",\"beef\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🐂\" src=\"1f402.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},cow2:{keywords:[\"beef\",\"ox\",\"animal\",\"nature\",\"moo\",\"milk\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🐄\" src=\"1f404.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},deer:{keywords:[\"animal\",\"nature\",\"horns\",\"venison\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🦌\" src=\"1f98c.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},dromedary_camel:{keywords:[\"animal\",\"hot\",\"desert\",\"hump\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🐪\" src=\"1f42a.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},camel:{keywords:[\"animal\",\"nature\",\"hot\",\"desert\",\"hump\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🐫\" src=\"1f42b.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},giraffe:{keywords:[\"animal\",\"nature\",\"spots\",\"safari\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🦒\" src=\"1f992.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},elephant:{keywords:[\"animal\",\"nature\",\"nose\",\"th\",\"circus\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🐘\" src=\"1f418.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},rhinoceros:{keywords:[\"animal\",\"nature\",\"horn\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🦏\" src=\"1f98f.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},goat:{keywords:[\"animal\",\"nature\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🐐\" src=\"1f410.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},ram:{keywords:[\"animal\",\"sheep\",\"nature\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🐏\" src=\"1f40f.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},sheep:{keywords:[\"animal\",\"nature\",\"wool\",\"shipit\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🐑\" src=\"1f411.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},racehorse:{keywords:[\"animal\",\"gamble\",\"luck\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🐎\" src=\"1f40e.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},pig2:{keywords:[\"animal\",\"nature\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🐖\" src=\"1f416.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},rat:{keywords:[\"animal\",\"mouse\",\"rodent\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🐀\" src=\"1f400.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},mouse2:{keywords:[\"animal\",\"nature\",\"rodent\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🐁\" src=\"1f401.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},rooster:{keywords:[\"animal\",\"nature\",\"chicken\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🐓\" src=\"1f413.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},turkey:{keywords:[\"animal\",\"bird\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🦃\" src=\"1f983.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},dove:{keywords:[\"animal\",\"bird\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🕊\" src=\"1f54a.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},dog2:{keywords:[\"animal\",\"nature\",\"friend\",\"doge\",\"pet\",\"faithful\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🐕\" src=\"1f415.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},poodle:{keywords:[\"dog\",\"animal\",\"101\",\"nature\",\"pet\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🐩\" src=\"1f429.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},cat2:{keywords:[\"animal\",\"meow\",\"pet\",\"cats\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🐈\" src=\"1f408.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},rabbit2:{keywords:[\"animal\",\"nature\",\"pet\",\"magic\",\"spring\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🐇\" src=\"1f407.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},chipmunk:{keywords:[\"animal\",\"nature\",\"rodent\",\"squirrel\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🐿\" src=\"1f43f.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},hedgehog:{keywords:[\"animal\",\"nature\",\"spiny\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🦔\" src=\"1f994.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},raccoon:{keywords:[\"animal\",\"nature\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🦝\" src=\"1f99d.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},llama:{keywords:[\"animal\",\"nature\",\"alpaca\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🦙\" src=\"1f999.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},hippopotamus:{keywords:[\"animal\",\"nature\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🦛\" src=\"1f99b.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},kangaroo:{keywords:[\"animal\",\"nature\",\"australia\",\"joey\",\"hop\",\"marsupial\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🦘\" src=\"1f998.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},badger:{keywords:[\"animal\",\"nature\",\"honey\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🦡\" src=\"1f9a1.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},swan:{keywords:[\"animal\",\"nature\",\"bird\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🦢\" src=\"1f9a2.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},peacock:{keywords:[\"animal\",\"nature\",\"peahen\",\"bird\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🦚\" src=\"1f99a.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},parrot:{keywords:[\"animal\",\"nature\",\"bird\",\"pirate\",\"talk\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🦜\" src=\"1f99c.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},lobster:{keywords:[\"animal\",\"nature\",\"bisque\",\"claws\",\"seafood\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🦞\" src=\"1f99e.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},mosquito:{keywords:[\"animal\",\"nature\",\"insect\",\"malaria\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🦟\" src=\"1f99f.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},paw_prints:{keywords:[\"animal\",\"tracking\",\"footprints\",\"dog\",\"cat\",\"pet\",\"feet\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🐾\" src=\"1f43e.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},dragon:{keywords:[\"animal\",\"myth\",\"nature\",\"chinese\",\"green\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🐉\" src=\"1f409.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},dragon_face:{keywords:[\"animal\",\"myth\",\"nature\",\"chinese\",\"green\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🐲\" src=\"1f432.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},cactus:{keywords:[\"vegetable\",\"plant\",\"nature\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🌵\" src=\"1f335.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},christmas_tree:{keywords:[\"festival\",\"vacation\",\"december\",\"xmas\",\"celebration\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🎄\" src=\"1f384.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},evergreen_tree:{keywords:[\"plant\",\"nature\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🌲\" src=\"1f332.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},deciduous_tree:{keywords:[\"plant\",\"nature\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🌳\" src=\"1f333.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},palm_tree:{keywords:[\"plant\",\"vegetable\",\"nature\",\"summer\",\"beach\",\"mojito\",\"tropical\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🌴\" src=\"1f334.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},seedling:{keywords:[\"plant\",\"nature\",\"grass\",\"lawn\",\"spring\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🌱\" src=\"1f331.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},herb:{keywords:[\"vegetable\",\"plant\",\"medicine\",\"weed\",\"grass\",\"lawn\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🌿\" src=\"1f33f.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},shamrock:{keywords:[\"vegetable\",\"plant\",\"nature\",\"irish\",\"clover\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"☘\" src=\"2618.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},four_leaf_clover:{keywords:[\"vegetable\",\"plant\",\"nature\",\"lucky\",\"irish\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🍀\" src=\"1f340.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},bamboo:{keywords:[\"plant\",\"nature\",\"vegetable\",\"panda\",\"pine_decoration\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🎍\" src=\"1f38d.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},tanabata_tree:{keywords:[\"plant\",\"nature\",\"branch\",\"summer\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🎋\" src=\"1f38b.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},leaves:{keywords:[\"nature\",\"plant\",\"tree\",\"vegetable\",\"grass\",\"lawn\",\"spring\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🍃\" src=\"1f343.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},fallen_leaf:{keywords:[\"nature\",\"plant\",\"vegetable\",\"leaves\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🍂\" src=\"1f342.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},maple_leaf:{keywords:[\"nature\",\"plant\",\"vegetable\",\"ca\",\"fall\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🍁\" src=\"1f341.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},ear_of_rice:{keywords:[\"nature\",\"plant\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🌾\" src=\"1f33e.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},hibiscus:{keywords:[\"plant\",\"vegetable\",\"flowers\",\"beach\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🌺\" src=\"1f33a.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},sunflower:{keywords:[\"nature\",\"plant\",\"fall\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🌻\" src=\"1f33b.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},rose:{keywords:[\"flowers\",\"valentines\",\"love\",\"spring\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🌹\" src=\"1f339.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},wilted_flower:{keywords:[\"plant\",\"nature\",\"flower\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🥀\" src=\"1f940.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},tulip:{keywords:[\"flowers\",\"plant\",\"nature\",\"summer\",\"spring\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🌷\" src=\"1f337.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},blossom:{keywords:[\"nature\",\"flowers\",\"yellow\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🌼\" src=\"1f33c.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},cherry_blossom:{keywords:[\"nature\",\"plant\",\"spring\",\"flower\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🌸\" src=\"1f338.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},bouquet:{keywords:[\"flowers\",\"nature\",\"spring\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"💐\" src=\"1f490.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},mushroom:{keywords:[\"plant\",\"vegetable\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🍄\" src=\"1f344.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},chestnut:{keywords:[\"food\",\"squirrel\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🌰\" src=\"1f330.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},jack_o_lantern:{keywords:[\"halloween\",\"light\",\"pumpkin\",\"creepy\",\"fall\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🎃\" src=\"1f383.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},shell:{keywords:[\"nature\",\"sea\",\"beach\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🐚\" src=\"1f41a.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},spider_web:{keywords:[\"animal\",\"insect\",\"arachnid\",\"silk\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🕸\" src=\"1f578.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},earth_americas:{keywords:[\"globe\",\"world\",\"USA\",\"international\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🌎\" src=\"1f30e.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},earth_africa:{keywords:[\"globe\",\"world\",\"international\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🌍\" src=\"1f30d.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},earth_asia:{keywords:[\"globe\",\"world\",\"east\",\"international\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🌏\" src=\"1f30f.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},full_moon:{keywords:[\"nature\",\"yellow\",\"twilight\",\"planet\",\"space\",\"night\",\"evening\",\"sleep\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🌕\" src=\"1f315.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},waning_gibbous_moon:{keywords:[\"nature\",\"twilight\",\"planet\",\"space\",\"night\",\"evening\",\"sleep\",\"waxing_gibbous_moon\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🌖\" src=\"1f316.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},last_quarter_moon:{keywords:[\"nature\",\"twilight\",\"planet\",\"space\",\"night\",\"evening\",\"sleep\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🌗\" src=\"1f317.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},waning_crescent_moon:{keywords:[\"nature\",\"twilight\",\"planet\",\"space\",\"night\",\"evening\",\"sleep\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🌘\" src=\"1f318.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},new_moon:{keywords:[\"nature\",\"twilight\",\"planet\",\"space\",\"night\",\"evening\",\"sleep\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🌑\" src=\"1f311.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},waxing_crescent_moon:{keywords:[\"nature\",\"twilight\",\"planet\",\"space\",\"night\",\"evening\",\"sleep\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🌒\" src=\"1f312.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},first_quarter_moon:{keywords:[\"nature\",\"twilight\",\"planet\",\"space\",\"night\",\"evening\",\"sleep\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🌓\" src=\"1f313.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},waxing_gibbous_moon:{keywords:[\"nature\",\"night\",\"sky\",\"gray\",\"twilight\",\"planet\",\"space\",\"evening\",\"sleep\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🌔\" src=\"1f314.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},new_moon_with_face:{keywords:[\"nature\",\"twilight\",\"planet\",\"space\",\"night\",\"evening\",\"sleep\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🌚\" src=\"1f31a.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},full_moon_with_face:{keywords:[\"nature\",\"twilight\",\"planet\",\"space\",\"night\",\"evening\",\"sleep\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🌝\" src=\"1f31d.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},first_quarter_moon_with_face:{keywords:[\"nature\",\"twilight\",\"planet\",\"space\",\"night\",\"evening\",\"sleep\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🌛\" src=\"1f31b.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},last_quarter_moon_with_face:{keywords:[\"nature\",\"twilight\",\"planet\",\"space\",\"night\",\"evening\",\"sleep\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🌜\" src=\"1f31c.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},sun_with_face:{keywords:[\"nature\",\"morning\",\"sky\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🌞\" src=\"1f31e.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},crescent_moon:{keywords:[\"night\",\"sleep\",\"sky\",\"evening\",\"magic\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🌙\" src=\"1f319.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},star:{keywords:[\"night\",\"yellow\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"⭐\" src=\"2b50.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},star2:{keywords:[\"night\",\"sparkle\",\"awesome\",\"good\",\"magic\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🌟\" src=\"1f31f.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},dizzy:{keywords:[\"star\",\"sparkle\",\"shoot\",\"magic\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"💫\" src=\"1f4ab.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},sparkles:{keywords:[\"stars\",\"shine\",\"shiny\",\"cool\",\"awesome\",\"good\",\"magic\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"✨\" src=\"2728.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},comet:{keywords:[\"space\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"☄\" src=\"2604.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},sunny:{keywords:[\"weather\",\"nature\",\"brightness\",\"summer\",\"beach\",\"spring\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"☀️\" src=\"2600.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},sun_behind_small_cloud:{keywords:[\"weather\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🌤\" src=\"1f324.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},partly_sunny:{keywords:[\"weather\",\"nature\",\"cloudy\",\"morning\",\"fall\",\"spring\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"⛅\" src=\"26c5.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},sun_behind_large_cloud:{keywords:[\"weather\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🌥\" src=\"1f325.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},sun_behind_rain_cloud:{keywords:[\"weather\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🌦\" src=\"1f326.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},cloud:{keywords:[\"weather\",\"sky\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"☁️\" src=\"2601.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},cloud_with_rain:{keywords:[\"weather\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🌧\" src=\"1f327.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},cloud_with_lightning_and_rain:{keywords:[\"weather\",\"lightning\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"⛈\" src=\"26c8.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},cloud_with_lightning:{keywords:[\"weather\",\"thunder\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🌩\" src=\"1f329.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},zap:{keywords:[\"thunder\",\"weather\",\"lightning bolt\",\"fast\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"⚡\" src=\"26a1.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},fire:{keywords:[\"hot\",\"cook\",\"flame\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🔥\" src=\"1f525.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},boom:{keywords:[\"bomb\",\"explode\",\"explosion\",\"collision\",\"blown\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"💥\" src=\"1f4a5.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},snowflake:{keywords:[\"winter\",\"season\",\"cold\",\"weather\",\"christmas\",\"xmas\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"❄️\" src=\"2744.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},cloud_with_snow:{keywords:[\"weather\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🌨\" src=\"1f328.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},snowman:{keywords:[\"winter\",\"season\",\"cold\",\"weather\",\"christmas\",\"xmas\",\"frozen\",\"without_snow\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"⛄\" src=\"26c4.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},snowman_with_snow:{keywords:[\"winter\",\"season\",\"cold\",\"weather\",\"christmas\",\"xmas\",\"frozen\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"☃\" src=\"2603.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},wind_face:{keywords:[\"gust\",\"air\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🌬\" src=\"1f32c.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},dash:{keywords:[\"wind\",\"air\",\"fast\",\"shoo\",\"fart\",\"smoke\",\"puff\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"💨\" src=\"1f4a8.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},tornado:{keywords:[\"weather\",\"cyclone\",\"twister\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🌪\" src=\"1f32a.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},fog:{keywords:[\"weather\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🌫\" src=\"1f32b.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},open_umbrella:{keywords:[\"weather\",\"spring\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"☂\" src=\"2602.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},umbrella:{keywords:[\"rainy\",\"weather\",\"spring\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"☔\" src=\"2614.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},droplet:{keywords:[\"water\",\"drip\",\"faucet\",\"spring\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"💧\" src=\"1f4a7.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},sweat_drops:{keywords:[\"water\",\"drip\",\"oops\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"💦\" src=\"1f4a6.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},ocean:{keywords:[\"sea\",\"water\",\"wave\",\"nature\",\"tsunami\",\"disaster\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🌊\" src=\"1f30a.png\"/>',fitzpatrick_scale:false,category:\"animals_and_nature\"},green_apple:{keywords:[\"fruit\",\"nature\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🍏\" src=\"1f34f.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},apple:{keywords:[\"fruit\",\"mac\",\"school\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🍎\" src=\"1f34e.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},pear:{keywords:[\"fruit\",\"nature\",\"food\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🍐\" src=\"1f350.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},tangerine:{keywords:[\"food\",\"fruit\",\"nature\",\"orange\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🍊\" src=\"1f34a.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},lemon:{keywords:[\"fruit\",\"nature\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🍋\" src=\"1f34b.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},banana:{keywords:[\"fruit\",\"food\",\"monkey\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🍌\" src=\"1f34c.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},watermelon:{keywords:[\"fruit\",\"food\",\"picnic\",\"summer\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🍉\" src=\"1f349.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},grapes:{keywords:[\"fruit\",\"food\",\"wine\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🍇\" src=\"1f347.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},strawberry:{keywords:[\"fruit\",\"food\",\"nature\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🍓\" src=\"1f353.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},melon:{keywords:[\"fruit\",\"nature\",\"food\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🍈\" src=\"1f348.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},cherries:{keywords:[\"food\",\"fruit\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🍒\" src=\"1f352.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},peach:{keywords:[\"fruit\",\"nature\",\"food\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🍑\" src=\"1f351.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},pineapple:{keywords:[\"fruit\",\"nature\",\"food\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🍍\" src=\"1f34d.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},coconut:{keywords:[\"fruit\",\"nature\",\"food\",\"palm\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🥥\" src=\"1f965.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},kiwi_fruit:{keywords:[\"fruit\",\"food\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🥝\" src=\"1f95d.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},mango:{keywords:[\"fruit\",\"food\",\"tropical\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🥭\" src=\"1f96d.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},avocado:{keywords:[\"fruit\",\"food\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🥑\" src=\"1f951.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},broccoli:{keywords:[\"fruit\",\"food\",\"vegetable\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🥦\" src=\"1f966.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},tomato:{keywords:[\"fruit\",\"vegetable\",\"nature\",\"food\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🍅\" src=\"1f345.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},eggplant:{keywords:[\"vegetable\",\"nature\",\"food\",\"aubergine\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🍆\" src=\"1f346.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},cucumber:{keywords:[\"fruit\",\"food\",\"pickle\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🥒\" src=\"1f952.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},carrot:{keywords:[\"vegetable\",\"food\",\"orange\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🥕\" src=\"1f955.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},hot_pepper:{keywords:[\"food\",\"spicy\",\"chilli\",\"chili\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🌶\" src=\"1f336.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},potato:{keywords:[\"food\",\"tuber\",\"vegatable\",\"starch\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🥔\" src=\"1f954.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},corn:{keywords:[\"food\",\"vegetable\",\"plant\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🌽\" src=\"1f33d.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},leafy_greens:{keywords:[\"food\",\"vegetable\",\"plant\",\"bok choy\",\"cabbage\",\"kale\",\"lettuce\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🥬\" src=\"1f96c.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},sweet_potato:{keywords:[\"food\",\"nature\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🍠\" src=\"1f360.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},peanuts:{keywords:[\"food\",\"nut\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🥜\" src=\"1f95c.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},honey_pot:{keywords:[\"bees\",\"sweet\",\"kitchen\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🍯\" src=\"1f36f.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},croissant:{keywords:[\"food\",\"bread\",\"french\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🥐\" src=\"1f950.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},bread:{keywords:[\"food\",\"wheat\",\"breakfast\",\"toast\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🍞\" src=\"1f35e.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},baguette_bread:{keywords:[\"food\",\"bread\",\"french\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🥖\" src=\"1f956.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},bagel:{keywords:[\"food\",\"bread\",\"bakery\",\"schmear\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🥯\" src=\"1f96f.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},pretzel:{keywords:[\"food\",\"bread\",\"twisted\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🥨\" src=\"1f968.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},cheese:{keywords:[\"food\",\"chadder\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🧀\" src=\"1f9c0.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},egg:{keywords:[\"food\",\"chicken\",\"breakfast\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🥚\" src=\"1f95a.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},bacon:{keywords:[\"food\",\"breakfast\",\"pork\",\"pig\",\"meat\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🥓\" src=\"1f953.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},steak:{keywords:[\"food\",\"cow\",\"meat\",\"cut\",\"chop\",\"lambchop\",\"porkchop\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🥩\" src=\"1f969.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},pancakes:{keywords:[\"food\",\"breakfast\",\"flapjacks\",\"hotcakes\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🥞\" src=\"1f95e.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},poultry_leg:{keywords:[\"food\",\"meat\",\"drumstick\",\"bird\",\"chicken\",\"turkey\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🍗\" src=\"1f357.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},meat_on_bone:{keywords:[\"good\",\"food\",\"drumstick\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🍖\" src=\"1f356.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},bone:{keywords:[\"skeleton\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🦴\" src=\"1f9b4.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},fried_shrimp:{keywords:[\"food\",\"animal\",\"appetizer\",\"summer\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🍤\" src=\"1f364.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},fried_egg:{keywords:[\"food\",\"breakfast\",\"kitchen\",\"egg\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🍳\" src=\"1f373.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},hamburger:{keywords:[\"meat\",\"fast food\",\"beef\",\"cheeseburger\",\"mcdonalds\",\"burger king\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🍔\" src=\"1f354.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},fries:{keywords:[\"chips\",\"snack\",\"fast food\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🍟\" src=\"1f35f.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},stuffed_flatbread:{keywords:[\"food\",\"flatbread\",\"stuffed\",\"gyro\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🥙\" src=\"1f959.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},hotdog:{keywords:[\"food\",\"frankfurter\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🌭\" src=\"1f32d.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},pizza:{keywords:[\"food\",\"party\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🍕\" src=\"1f355.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},sandwich:{keywords:[\"food\",\"lunch\",\"bread\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🥪\" src=\"1f96a.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},canned_food:{keywords:[\"food\",\"soup\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🥫\" src=\"1f96b.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},spaghetti:{keywords:[\"food\",\"italian\",\"noodle\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🍝\" src=\"1f35d.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},taco:{keywords:[\"food\",\"mexican\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🌮\" src=\"1f32e.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},burrito:{keywords:[\"food\",\"mexican\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🌯\" src=\"1f32f.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},green_salad:{keywords:[\"food\",\"healthy\",\"lettuce\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🥗\" src=\"1f957.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},shallow_pan_of_food:{keywords:[\"food\",\"cooking\",\"casserole\",\"paella\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🥘\" src=\"1f958.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},ramen:{keywords:[\"food\",\"japanese\",\"noodle\",\"chopsticks\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🍜\" src=\"1f35c.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},stew:{keywords:[\"food\",\"meat\",\"soup\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🍲\" src=\"1f372.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},fish_cake:{keywords:[\"food\",\"japan\",\"sea\",\"beach\",\"narutomaki\",\"pink\",\"swirl\",\"kamaboko\",\"surimi\",\"ramen\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🍥\" src=\"1f365.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},fortune_cookie:{keywords:[\"food\",\"prophecy\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🥠\" src=\"1f960.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},sushi:{keywords:[\"food\",\"fish\",\"japanese\",\"rice\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🍣\" src=\"1f363.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},bento:{keywords:[\"food\",\"japanese\",\"box\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🍱\" src=\"1f371.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},curry:{keywords:[\"food\",\"spicy\",\"hot\",\"indian\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🍛\" src=\"1f35b.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},rice_ball:{keywords:[\"food\",\"japanese\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🍙\" src=\"1f359.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},rice:{keywords:[\"food\",\"china\",\"asian\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🍚\" src=\"1f35a.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},rice_cracker:{keywords:[\"food\",\"japanese\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🍘\" src=\"1f358.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},oden:{keywords:[\"food\",\"japanese\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🍢\" src=\"1f362.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},dango:{keywords:[\"food\",\"dessert\",\"sweet\",\"japanese\",\"barbecue\",\"meat\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🍡\" src=\"1f361.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},shaved_ice:{keywords:[\"hot\",\"dessert\",\"summer\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🍧\" src=\"1f367.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},ice_cream:{keywords:[\"food\",\"hot\",\"dessert\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🍨\" src=\"1f368.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},icecream:{keywords:[\"food\",\"hot\",\"dessert\",\"summer\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🍦\" src=\"1f366.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},pie:{keywords:[\"food\",\"dessert\",\"pastry\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🥧\" src=\"1f967.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},cake:{keywords:[\"food\",\"dessert\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🍰\" src=\"1f370.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},cupcake:{keywords:[\"food\",\"dessert\",\"bakery\",\"sweet\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🧁\" src=\"1f9c1.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},moon_cake:{keywords:[\"food\",\"autumn\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🥮\" src=\"1f96e.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},birthday:{keywords:[\"food\",\"dessert\",\"cake\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🎂\" src=\"1f382.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},custard:{keywords:[\"dessert\",\"food\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🍮\" src=\"1f36e.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},candy:{keywords:[\"snack\",\"dessert\",\"sweet\",\"lolly\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🍬\" src=\"1f36c.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},lollipop:{keywords:[\"food\",\"snack\",\"candy\",\"sweet\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🍭\" src=\"1f36d.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},chocolate_bar:{keywords:[\"food\",\"snack\",\"dessert\",\"sweet\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🍫\" src=\"1f36b.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},popcorn:{keywords:[\"food\",\"movie theater\",\"films\",\"snack\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🍿\" src=\"1f37f.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},dumpling:{keywords:[\"food\",\"empanada\",\"pierogi\",\"potsticker\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🥟\" src=\"1f95f.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},doughnut:{keywords:[\"food\",\"dessert\",\"snack\",\"sweet\",\"donut\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🍩\" src=\"1f369.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},cookie:{keywords:[\"food\",\"snack\",\"oreo\",\"chocolate\",\"sweet\",\"dessert\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🍪\" src=\"1f36a.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},milk_glass:{keywords:[\"beverage\",\"drink\",\"cow\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🥛\" src=\"1f95b.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},beer:{keywords:[\"relax\",\"beverage\",\"drink\",\"drunk\",\"party\",\"pub\",\"summer\",\"alcohol\",\"booze\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🍺\" src=\"1f37a.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},beers:{keywords:[\"relax\",\"beverage\",\"drink\",\"drunk\",\"party\",\"pub\",\"summer\",\"alcohol\",\"booze\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🍻\" src=\"1f37b.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},clinking_glasses:{keywords:[\"beverage\",\"drink\",\"party\",\"alcohol\",\"celebrate\",\"cheers\",\"wine\",\"champagne\",\"toast\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🥂\" src=\"1f942.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},wine_glass:{keywords:[\"drink\",\"beverage\",\"drunk\",\"alcohol\",\"booze\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🍷\" src=\"1f377.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},tumbler_glass:{keywords:[\"drink\",\"beverage\",\"drunk\",\"alcohol\",\"liquor\",\"booze\",\"bourbon\",\"scotch\",\"whisky\",\"glass\",\"shot\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🥃\" src=\"1f943.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},cocktail:{keywords:[\"drink\",\"drunk\",\"alcohol\",\"beverage\",\"booze\",\"mojito\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🍸\" src=\"1f378.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},tropical_drink:{keywords:[\"beverage\",\"cocktail\",\"summer\",\"beach\",\"alcohol\",\"booze\",\"mojito\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🍹\" src=\"1f379.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},champagne:{keywords:[\"drink\",\"wine\",\"bottle\",\"celebration\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🍾\" src=\"1f37e.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},sake:{keywords:[\"wine\",\"drink\",\"drunk\",\"beverage\",\"japanese\",\"alcohol\",\"booze\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🍶\" src=\"1f376.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},tea:{keywords:[\"drink\",\"bowl\",\"breakfast\",\"green\",\"british\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🍵\" src=\"1f375.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},cup_with_straw:{keywords:[\"drink\",\"soda\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🥤\" src=\"1f964.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},coffee:{keywords:[\"beverage\",\"caffeine\",\"latte\",\"espresso\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"☕\" src=\"2615.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},baby_bottle:{keywords:[\"food\",\"container\",\"milk\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🍼\" src=\"1f37c.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},salt:{keywords:[\"condiment\",\"shaker\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🧂\" src=\"1f9c2.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},spoon:{keywords:[\"cutlery\",\"kitchen\",\"tableware\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🥄\" src=\"1f944.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},fork_and_knife:{keywords:[\"cutlery\",\"kitchen\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🍴\" src=\"1f374.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},plate_with_cutlery:{keywords:[\"food\",\"eat\",\"meal\",\"lunch\",\"dinner\",\"restaurant\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🍽\" src=\"1f37d.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},bowl_with_spoon:{keywords:[\"food\",\"breakfast\",\"cereal\",\"oatmeal\",\"porridge\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🥣\" src=\"1f963.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},takeout_box:{keywords:[\"food\",\"leftovers\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🥡\" src=\"1f961.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},chopsticks:{keywords:[\"food\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🥢\" src=\"1f962.png\"/>',fitzpatrick_scale:false,category:\"food_and_drink\"},soccer:{keywords:[\"sports\",\"football\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"⚽\" src=\"26bd.png\"/>',fitzpatrick_scale:false,category:\"activity\"},basketball:{keywords:[\"sports\",\"balls\",\"NBA\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🏀\" src=\"1f3c0.png\"/>',fitzpatrick_scale:false,category:\"activity\"},football:{keywords:[\"sports\",\"balls\",\"NFL\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🏈\" src=\"1f3c8.png\"/>',fitzpatrick_scale:false,category:\"activity\"},baseball:{keywords:[\"sports\",\"balls\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"⚾\" src=\"26be.png\"/>',fitzpatrick_scale:false,category:\"activity\"},softball:{keywords:[\"sports\",\"balls\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🥎\" src=\"1f94e.png\"/>',fitzpatrick_scale:false,category:\"activity\"},tennis:{keywords:[\"sports\",\"balls\",\"green\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🎾\" src=\"1f3be.png\"/>',fitzpatrick_scale:false,category:\"activity\"},volleyball:{keywords:[\"sports\",\"balls\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🏐\" src=\"1f3d0.png\"/>',fitzpatrick_scale:false,category:\"activity\"},rugby_football:{keywords:[\"sports\",\"team\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🏉\" src=\"1f3c9.png\"/>',fitzpatrick_scale:false,category:\"activity\"},flying_disc:{keywords:[\"sports\",\"frisbee\",\"ultimate\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🥏\" src=\"1f94f.png\"/>',fitzpatrick_scale:false,category:\"activity\"},\"8ball\":{keywords:[\"pool\",\"hobby\",\"game\",\"luck\",\"magic\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🎱\" src=\"1f3b1.png\"/>',fitzpatrick_scale:false,category:\"activity\"},golf:{keywords:[\"sports\",\"business\",\"flag\",\"hole\",\"summer\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"⛳\" src=\"26f3.png\"/>',fitzpatrick_scale:false,category:\"activity\"},golfing_woman:{keywords:[\"sports\",\"business\",\"woman\",\"female\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🏌️‍♀️\" src=\"1f3cc-fe0f-200d-2640-fe0f.png\"/>',fitzpatrick_scale:false,category:\"activity\"},golfing_man:{keywords:[\"sports\",\"business\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🏌\" src=\"1f3cc.png\"/>',fitzpatrick_scale:true,category:\"activity\"},ping_pong:{keywords:[\"sports\",\"pingpong\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🏓\" src=\"1f3d3.png\"/>',fitzpatrick_scale:false,category:\"activity\"},badminton:{keywords:[\"sports\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🏸\" src=\"1f3f8.png\"/>',fitzpatrick_scale:false,category:\"activity\"},goal_net:{keywords:[\"sports\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🥅\" src=\"1f945.png\"/>',fitzpatrick_scale:false,category:\"activity\"},ice_hockey:{keywords:[\"sports\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🏒\" src=\"1f3d2.png\"/>',fitzpatrick_scale:false,category:\"activity\"},field_hockey:{keywords:[\"sports\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🏑\" src=\"1f3d1.png\"/>',fitzpatrick_scale:false,category:\"activity\"},lacrosse:{keywords:[\"sports\",\"ball\",\"stick\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🥍\" src=\"1f94d.png\"/>',fitzpatrick_scale:false,category:\"activity\"},cricket:{keywords:[\"sports\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🏏\" src=\"1f3cf.png\"/>',fitzpatrick_scale:false,category:\"activity\"},ski:{keywords:[\"sports\",\"winter\",\"cold\",\"snow\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🎿\" src=\"1f3bf.png\"/>',fitzpatrick_scale:false,category:\"activity\"},skier:{keywords:[\"sports\",\"winter\",\"snow\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"⛷\" src=\"26f7.png\"/>',fitzpatrick_scale:false,category:\"activity\"},snowboarder:{keywords:[\"sports\",\"winter\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🏂\" src=\"1f3c2.png\"/>',fitzpatrick_scale:true,category:\"activity\"},person_fencing:{keywords:[\"sports\",\"fencing\",\"sword\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🤺\" src=\"1f93a.png\"/>',fitzpatrick_scale:false,category:\"activity\"},women_wrestling:{keywords:[\"sports\",\"wrestlers\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🤼‍♀️\" src=\"1f93c-200d-2640-fe0f.png\"/>',fitzpatrick_scale:false,category:\"activity\"},men_wrestling:{keywords:[\"sports\",\"wrestlers\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🤼‍♂️\" src=\"1f93c-200d-2642-fe0f.png\"/>',fitzpatrick_scale:false,category:\"activity\"},woman_cartwheeling:{keywords:[\"gymnastics\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🤸‍♀️\" src=\"1f938-200d-2640-fe0f.png\"/>',fitzpatrick_scale:true,category:\"activity\"},man_cartwheeling:{keywords:[\"gymnastics\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🤸‍♂️\" src=\"1f938-200d-2642-fe0f.png\"/>',fitzpatrick_scale:true,category:\"activity\"},woman_playing_handball:{keywords:[\"sports\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🤾‍♀️\" src=\"1f93e-200d-2640-fe0f.png\"/>',fitzpatrick_scale:true,category:\"activity\"},man_playing_handball:{keywords:[\"sports\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🤾‍♂️\" src=\"1f93e-200d-2642-fe0f.png\"/>',fitzpatrick_scale:true,category:\"activity\"},ice_skate:{keywords:[\"sports\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"⛸\" src=\"26f8.png\"/>',fitzpatrick_scale:false,category:\"activity\"},curling_stone:{keywords:[\"sports\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🥌\" src=\"1f94c.png\"/>',fitzpatrick_scale:false,category:\"activity\"},skateboard:{keywords:[\"board\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🛹\" src=\"1f6f9.png\"/>',fitzpatrick_scale:false,category:\"activity\"},sled:{keywords:[\"sleigh\",\"luge\",\"toboggan\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🛷\" src=\"1f6f7.png\"/>',fitzpatrick_scale:false,category:\"activity\"},bow_and_arrow:{keywords:[\"sports\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🏹\" src=\"1f3f9.png\"/>',fitzpatrick_scale:false,category:\"activity\"},fishing_pole_and_fish:{keywords:[\"food\",\"hobby\",\"summer\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🎣\" src=\"1f3a3.png\"/>',fitzpatrick_scale:false,category:\"activity\"},boxing_glove:{keywords:[\"sports\",\"fighting\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🥊\" src=\"1f94a.png\"/>',fitzpatrick_scale:false,category:\"activity\"},martial_arts_uniform:{keywords:[\"judo\",\"karate\",\"taekwondo\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🥋\" src=\"1f94b.png\"/>',fitzpatrick_scale:false,category:\"activity\"},rowing_woman:{keywords:[\"sports\",\"hobby\",\"water\",\"ship\",\"woman\",\"female\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🚣‍♀️\" src=\"1f6a3-200d-2640-fe0f.png\"/>',fitzpatrick_scale:true,category:\"activity\"},rowing_man:{keywords:[\"sports\",\"hobby\",\"water\",\"ship\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🚣\" src=\"1f6a3.png\"/>',fitzpatrick_scale:true,category:\"activity\"},climbing_woman:{keywords:[\"sports\",\"hobby\",\"woman\",\"female\",\"rock\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🧗‍♀️\" src=\"1f9d7-200d-2640-fe0f.png\"/>',fitzpatrick_scale:true,category:\"activity\"},climbing_man:{keywords:[\"sports\",\"hobby\",\"man\",\"male\",\"rock\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🧗‍♂️\" src=\"1f9d7-200d-2642-fe0f.png\"/>',fitzpatrick_scale:true,category:\"activity\"},swimming_woman:{keywords:[\"sports\",\"exercise\",\"human\",\"athlete\",\"water\",\"summer\",\"woman\",\"female\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🏊‍♀️\" src=\"1f3ca-200d-2640-fe0f.png\"/>',fitzpatrick_scale:true,category:\"activity\"},swimming_man:{keywords:[\"sports\",\"exercise\",\"human\",\"athlete\",\"water\",\"summer\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🏊\" src=\"1f3ca.png\"/>',fitzpatrick_scale:true,category:\"activity\"},woman_playing_water_polo:{keywords:[\"sports\",\"pool\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🤽‍♀️\" src=\"1f93d-200d-2640-fe0f.png\"/>',fitzpatrick_scale:true,category:\"activity\"},man_playing_water_polo:{keywords:[\"sports\",\"pool\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🤽‍♂️\" src=\"1f93d-200d-2642-fe0f.png\"/>',fitzpatrick_scale:true,category:\"activity\"},woman_in_lotus_position:{keywords:[\"woman\",\"female\",\"meditation\",\"yoga\",\"serenity\",\"zen\",\"mindfulness\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🧘‍♀️\" src=\"1f9d8-200d-2640-fe0f.png\"/>',fitzpatrick_scale:true,category:\"activity\"},man_in_lotus_position:{keywords:[\"man\",\"male\",\"meditation\",\"yoga\",\"serenity\",\"zen\",\"mindfulness\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🧘‍♂️\" src=\"1f9d8-200d-2642-fe0f.png\"/>',fitzpatrick_scale:true,category:\"activity\"},surfing_woman:{keywords:[\"sports\",\"ocean\",\"sea\",\"summer\",\"beach\",\"woman\",\"female\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🏄‍♀️\" src=\"1f3c4-200d-2640-fe0f.png\"/>',fitzpatrick_scale:true,category:\"activity\"},surfing_man:{keywords:[\"sports\",\"ocean\",\"sea\",\"summer\",\"beach\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🏄\" src=\"1f3c4.png\"/>',fitzpatrick_scale:true,category:\"activity\"},bath:{keywords:[\"clean\",\"shower\",\"bathroom\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🛀\" src=\"1f6c0.png\"/>',fitzpatrick_scale:true,category:\"activity\"},basketball_woman:{keywords:[\"sports\",\"human\",\"woman\",\"female\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"⛹️‍♀️\" src=\"26f9-fe0f-200d-2640-fe0f.png\"/>',fitzpatrick_scale:true,category:\"activity\"},basketball_man:{keywords:[\"sports\",\"human\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"⛹\" src=\"26f9.png\"/>',fitzpatrick_scale:true,category:\"activity\"},weight_lifting_woman:{keywords:[\"sports\",\"training\",\"exercise\",\"woman\",\"female\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🏋️‍♀️\" src=\"1f3cb-fe0f-200d-2640-fe0f.png\"/>',fitzpatrick_scale:true,category:\"activity\"},weight_lifting_man:{keywords:[\"sports\",\"training\",\"exercise\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🏋\" src=\"1f3cb.png\"/>',fitzpatrick_scale:true,category:\"activity\"},biking_woman:{keywords:[\"sports\",\"bike\",\"exercise\",\"hipster\",\"woman\",\"female\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🚴‍♀️\" src=\"1f6b4-200d-2640-fe0f.png\"/>',fitzpatrick_scale:true,category:\"activity\"},biking_man:{keywords:[\"sports\",\"bike\",\"exercise\",\"hipster\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🚴\" src=\"1f6b4.png\"/>',fitzpatrick_scale:true,category:\"activity\"},mountain_biking_woman:{keywords:[\"transportation\",\"sports\",\"human\",\"race\",\"bike\",\"woman\",\"female\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🚵‍♀️\" src=\"1f6b5-200d-2640-fe0f.png\"/>',fitzpatrick_scale:true,category:\"activity\"},mountain_biking_man:{keywords:[\"transportation\",\"sports\",\"human\",\"race\",\"bike\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🚵\" src=\"1f6b5.png\"/>',fitzpatrick_scale:true,category:\"activity\"},horse_racing:{keywords:[\"animal\",\"betting\",\"competition\",\"gambling\",\"luck\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🏇\" src=\"1f3c7.png\"/>',fitzpatrick_scale:true,category:\"activity\"},business_suit_levitating:{keywords:[\"suit\",\"business\",\"levitate\",\"hover\",\"jump\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🕴\" src=\"1f574.png\"/>',fitzpatrick_scale:true,category:\"activity\"},trophy:{keywords:[\"win\",\"award\",\"contest\",\"place\",\"ftw\",\"ceremony\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🏆\" src=\"1f3c6.png\"/>',fitzpatrick_scale:false,category:\"activity\"},running_shirt_with_sash:{keywords:[\"play\",\"pageant\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🎽\" src=\"1f3bd.png\"/>',fitzpatrick_scale:false,category:\"activity\"},medal_sports:{keywords:[\"award\",\"winning\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🏅\" src=\"1f3c5.png\"/>',fitzpatrick_scale:false,category:\"activity\"},medal_military:{keywords:[\"award\",\"winning\",\"army\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🎖\" src=\"1f396.png\"/>',fitzpatrick_scale:false,category:\"activity\"},\"1st_place_medal\":{keywords:[\"award\",\"winning\",\"first\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🥇\" src=\"1f947.png\"/>',fitzpatrick_scale:false,category:\"activity\"},\"2nd_place_medal\":{keywords:[\"award\",\"second\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🥈\" src=\"1f948.png\"/>',fitzpatrick_scale:false,category:\"activity\"},\"3rd_place_medal\":{keywords:[\"award\",\"third\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🥉\" src=\"1f949.png\"/>',fitzpatrick_scale:false,category:\"activity\"},reminder_ribbon:{keywords:[\"sports\",\"cause\",\"support\",\"awareness\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🎗\" src=\"1f397.png\"/>',fitzpatrick_scale:false,category:\"activity\"},rosette:{keywords:[\"flower\",\"decoration\",\"military\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🏵\" src=\"1f3f5.png\"/>',fitzpatrick_scale:false,category:\"activity\"},ticket:{keywords:[\"event\",\"concert\",\"pass\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🎫\" src=\"1f3ab.png\"/>',fitzpatrick_scale:false,category:\"activity\"},tickets:{keywords:[\"sports\",\"concert\",\"entrance\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🎟\" src=\"1f39f.png\"/>',fitzpatrick_scale:false,category:\"activity\"},performing_arts:{keywords:[\"acting\",\"theater\",\"drama\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🎭\" src=\"1f3ad.png\"/>',fitzpatrick_scale:false,category:\"activity\"},art:{keywords:[\"design\",\"paint\",\"draw\",\"colors\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🎨\" src=\"1f3a8.png\"/>',fitzpatrick_scale:false,category:\"activity\"},circus_tent:{keywords:[\"festival\",\"carnival\",\"party\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🎪\" src=\"1f3aa.png\"/>',fitzpatrick_scale:false,category:\"activity\"},woman_juggling:{keywords:[\"juggle\",\"balance\",\"skill\",\"multitask\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🤹‍♀️\" src=\"1f939-200d-2640-fe0f.png\"/>',fitzpatrick_scale:true,category:\"activity\"},man_juggling:{keywords:[\"juggle\",\"balance\",\"skill\",\"multitask\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🤹‍♂️\" src=\"1f939-200d-2642-fe0f.png\"/>',fitzpatrick_scale:true,category:\"activity\"},microphone:{keywords:[\"sound\",\"music\",\"PA\",\"sing\",\"talkshow\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🎤\" src=\"1f3a4.png\"/>',fitzpatrick_scale:false,category:\"activity\"},headphones:{keywords:[\"music\",\"score\",\"gadgets\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🎧\" src=\"1f3a7.png\"/>',fitzpatrick_scale:false,category:\"activity\"},musical_score:{keywords:[\"treble\",\"clef\",\"compose\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🎼\" src=\"1f3bc.png\"/>',fitzpatrick_scale:false,category:\"activity\"},musical_keyboard:{keywords:[\"piano\",\"instrument\",\"compose\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🎹\" src=\"1f3b9.png\"/>',fitzpatrick_scale:false,category:\"activity\"},drum:{keywords:[\"music\",\"instrument\",\"drumsticks\",\"snare\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🥁\" src=\"1f941.png\"/>',fitzpatrick_scale:false,category:\"activity\"},saxophone:{keywords:[\"music\",\"instrument\",\"jazz\",\"blues\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🎷\" src=\"1f3b7.png\"/>',fitzpatrick_scale:false,category:\"activity\"},trumpet:{keywords:[\"music\",\"brass\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🎺\" src=\"1f3ba.png\"/>',fitzpatrick_scale:false,category:\"activity\"},guitar:{keywords:[\"music\",\"instrument\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🎸\" src=\"1f3b8.png\"/>',fitzpatrick_scale:false,category:\"activity\"},violin:{keywords:[\"music\",\"instrument\",\"orchestra\",\"symphony\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🎻\" src=\"1f3bb.png\"/>',fitzpatrick_scale:false,category:\"activity\"},clapper:{keywords:[\"movie\",\"film\",\"record\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🎬\" src=\"1f3ac.png\"/>',fitzpatrick_scale:false,category:\"activity\"},video_game:{keywords:[\"play\",\"console\",\"PS4\",\"controller\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🎮\" src=\"1f3ae.png\"/>',fitzpatrick_scale:false,category:\"activity\"},space_invader:{keywords:[\"game\",\"arcade\",\"play\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"👾\" src=\"1f47e.png\"/>',fitzpatrick_scale:false,category:\"activity\"},dart:{keywords:[\"game\",\"play\",\"bar\",\"target\",\"bullseye\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🎯\" src=\"1f3af.png\"/>',fitzpatrick_scale:false,category:\"activity\"},game_die:{keywords:[\"dice\",\"random\",\"tabletop\",\"play\",\"luck\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🎲\" src=\"1f3b2.png\"/>',fitzpatrick_scale:false,category:\"activity\"},chess_pawn:{keywords:[\"expendable\"],char:\"♟\",fitzpatrick_scale:false,category:\"activity\"},slot_machine:{keywords:[\"bet\",\"gamble\",\"vegas\",\"fruit machine\",\"luck\",\"casino\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🎰\" src=\"1f3b0.png\"/>',fitzpatrick_scale:false,category:\"activity\"},jigsaw:{keywords:[\"interlocking\",\"puzzle\",\"piece\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🧩\" src=\"1f9e9.png\"/>',fitzpatrick_scale:false,category:\"activity\"},bowling:{keywords:[\"sports\",\"fun\",\"play\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🎳\" src=\"1f3b3.png\"/>',fitzpatrick_scale:false,category:\"activity\"},red_car:{keywords:[\"red\",\"transportation\",\"vehicle\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🚗\" src=\"1f697.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},taxi:{keywords:[\"uber\",\"vehicle\",\"cars\",\"transportation\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🚕\" src=\"1f695.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},blue_car:{keywords:[\"transportation\",\"vehicle\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🚙\" src=\"1f699.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},bus:{keywords:[\"car\",\"vehicle\",\"transportation\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🚌\" src=\"1f68c.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},trolleybus:{keywords:[\"bart\",\"transportation\",\"vehicle\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🚎\" src=\"1f68e.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},racing_car:{keywords:[\"sports\",\"race\",\"fast\",\"formula\",\"f1\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🏎\" src=\"1f3ce.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},police_car:{keywords:[\"vehicle\",\"cars\",\"transportation\",\"law\",\"legal\",\"enforcement\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🚓\" src=\"1f693.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},ambulance:{keywords:[\"health\",\"911\",\"hospital\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🚑\" src=\"1f691.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},fire_engine:{keywords:[\"transportation\",\"cars\",\"vehicle\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🚒\" src=\"1f692.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},minibus:{keywords:[\"vehicle\",\"car\",\"transportation\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🚐\" src=\"1f690.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},truck:{keywords:[\"cars\",\"transportation\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🚚\" src=\"1f69a.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},articulated_lorry:{keywords:[\"vehicle\",\"cars\",\"transportation\",\"express\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🚛\" src=\"1f69b.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},tractor:{keywords:[\"vehicle\",\"car\",\"farming\",\"agriculture\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🚜\" src=\"1f69c.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},kick_scooter:{keywords:[\"vehicle\",\"kick\",\"razor\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🛴\" src=\"1f6f4.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},motorcycle:{keywords:[\"race\",\"sports\",\"fast\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🏍\" src=\"1f3cd.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},bike:{keywords:[\"sports\",\"bicycle\",\"exercise\",\"hipster\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🚲\" src=\"1f6b2.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},motor_scooter:{keywords:[\"vehicle\",\"vespa\",\"sasha\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🛵\" src=\"1f6f5.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},rotating_light:{keywords:[\"police\",\"ambulance\",\"911\",\"emergency\",\"alert\",\"error\",\"pinged\",\"law\",\"legal\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🚨\" src=\"1f6a8.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},oncoming_police_car:{keywords:[\"vehicle\",\"law\",\"legal\",\"enforcement\",\"911\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🚔\" src=\"1f694.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},oncoming_bus:{keywords:[\"vehicle\",\"transportation\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🚍\" src=\"1f68d.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},oncoming_automobile:{keywords:[\"car\",\"vehicle\",\"transportation\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🚘\" src=\"1f698.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},oncoming_taxi:{keywords:[\"vehicle\",\"cars\",\"uber\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🚖\" src=\"1f696.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},aerial_tramway:{keywords:[\"transportation\",\"vehicle\",\"ski\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🚡\" src=\"1f6a1.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},mountain_cableway:{keywords:[\"transportation\",\"vehicle\",\"ski\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🚠\" src=\"1f6a0.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},suspension_railway:{keywords:[\"vehicle\",\"transportation\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🚟\" src=\"1f69f.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},railway_car:{keywords:[\"transportation\",\"vehicle\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🚃\" src=\"1f683.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},train:{keywords:[\"transportation\",\"vehicle\",\"carriage\",\"public\",\"travel\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🚋\" src=\"1f68b.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},monorail:{keywords:[\"transportation\",\"vehicle\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🚝\" src=\"1f69d.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},bullettrain_side:{keywords:[\"transportation\",\"vehicle\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🚄\" src=\"1f684.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},bullettrain_front:{keywords:[\"transportation\",\"vehicle\",\"speed\",\"fast\",\"public\",\"travel\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🚅\" src=\"1f685.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},light_rail:{keywords:[\"transportation\",\"vehicle\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🚈\" src=\"1f688.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},mountain_railway:{keywords:[\"transportation\",\"vehicle\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🚞\" src=\"1f69e.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},steam_locomotive:{keywords:[\"transportation\",\"vehicle\",\"train\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🚂\" src=\"1f682.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},train2:{keywords:[\"transportation\",\"vehicle\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🚆\" src=\"1f686.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},metro:{keywords:[\"transportation\",\"blue-square\",\"mrt\",\"underground\",\"tube\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🚇\" src=\"1f687.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},tram:{keywords:[\"transportation\",\"vehicle\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🚊\" src=\"1f68a.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},station:{keywords:[\"transportation\",\"vehicle\",\"public\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🚉\" src=\"1f689.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},flying_saucer:{keywords:[\"transportation\",\"vehicle\",\"ufo\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🛸\" src=\"1f6f8.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},helicopter:{keywords:[\"transportation\",\"vehicle\",\"fly\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🚁\" src=\"1f681.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},small_airplane:{keywords:[\"flight\",\"transportation\",\"fly\",\"vehicle\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🛩\" src=\"1f6e9.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},airplane:{keywords:[\"vehicle\",\"transportation\",\"flight\",\"fly\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"✈️\" src=\"2708.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},flight_departure:{keywords:[\"airport\",\"flight\",\"landing\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🛫\" src=\"1f6eb.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},flight_arrival:{keywords:[\"airport\",\"flight\",\"boarding\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🛬\" src=\"1f6ec.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},sailboat:{keywords:[\"ship\",\"summer\",\"transportation\",\"water\",\"sailing\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"⛵\" src=\"26f5.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},motor_boat:{keywords:[\"ship\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🛥\" src=\"1f6e5.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},speedboat:{keywords:[\"ship\",\"transportation\",\"vehicle\",\"summer\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🚤\" src=\"1f6a4.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},ferry:{keywords:[\"boat\",\"ship\",\"yacht\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"⛴\" src=\"26f4.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},passenger_ship:{keywords:[\"yacht\",\"cruise\",\"ferry\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🛳\" src=\"1f6f3.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},rocket:{keywords:[\"launch\",\"ship\",\"staffmode\",\"NASA\",\"outer space\",\"outer_space\",\"fly\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🚀\" src=\"1f680.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},artificial_satellite:{keywords:[\"communication\",\"gps\",\"orbit\",\"spaceflight\",\"NASA\",\"ISS\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🛰\" src=\"1f6f0.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},seat:{keywords:[\"sit\",\"airplane\",\"transport\",\"bus\",\"flight\",\"fly\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"💺\" src=\"1f4ba.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},canoe:{keywords:[\"boat\",\"paddle\",\"water\",\"ship\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🛶\" src=\"1f6f6.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},anchor:{keywords:[\"ship\",\"ferry\",\"sea\",\"boat\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"⚓\" src=\"2693.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},construction:{keywords:[\"wip\",\"progress\",\"caution\",\"warning\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🚧\" src=\"1f6a7.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},fuelpump:{keywords:[\"gas station\",\"petroleum\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"⛽\" src=\"26fd.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},busstop:{keywords:[\"transportation\",\"wait\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🚏\" src=\"1f68f.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},vertical_traffic_light:{keywords:[\"transportation\",\"driving\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🚦\" src=\"1f6a6.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},traffic_light:{keywords:[\"transportation\",\"signal\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🚥\" src=\"1f6a5.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},checkered_flag:{keywords:[\"contest\",\"finishline\",\"race\",\"gokart\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🏁\" src=\"1f3c1.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},ship:{keywords:[\"transportation\",\"titanic\",\"deploy\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🚢\" src=\"1f6a2.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},ferris_wheel:{keywords:[\"photo\",\"carnival\",\"londoneye\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🎡\" src=\"1f3a1.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},roller_coaster:{keywords:[\"carnival\",\"playground\",\"photo\",\"fun\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🎢\" src=\"1f3a2.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},carousel_horse:{keywords:[\"photo\",\"carnival\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🎠\" src=\"1f3a0.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},building_construction:{keywords:[\"wip\",\"working\",\"progress\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🏗\" src=\"1f3d7.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},foggy:{keywords:[\"photo\",\"mountain\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🌁\" src=\"1f301.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},tokyo_tower:{keywords:[\"photo\",\"japanese\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🗼\" src=\"1f5fc.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},factory:{keywords:[\"building\",\"industry\",\"pollution\",\"smoke\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🏭\" src=\"1f3ed.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},fountain:{keywords:[\"photo\",\"summer\",\"water\",\"fresh\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"⛲\" src=\"26f2.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},rice_scene:{keywords:[\"photo\",\"japan\",\"asia\",\"tsukimi\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🎑\" src=\"1f391.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},mountain:{keywords:[\"photo\",\"nature\",\"environment\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"⛰\" src=\"26f0.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},mountain_snow:{keywords:[\"photo\",\"nature\",\"environment\",\"winter\",\"cold\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🏔\" src=\"1f3d4.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},mount_fuji:{keywords:[\"photo\",\"mountain\",\"nature\",\"japanese\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🗻\" src=\"1f5fb.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},volcano:{keywords:[\"photo\",\"nature\",\"disaster\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🌋\" src=\"1f30b.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},japan:{keywords:[\"nation\",\"country\",\"japanese\",\"asia\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🗾\" src=\"1f5fe.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},camping:{keywords:[\"photo\",\"outdoors\",\"tent\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🏕\" src=\"1f3d5.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},tent:{keywords:[\"photo\",\"camping\",\"outdoors\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"⛺\" src=\"26fa.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},national_park:{keywords:[\"photo\",\"environment\",\"nature\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🏞\" src=\"1f3de.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},motorway:{keywords:[\"road\",\"cupertino\",\"interstate\",\"highway\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🛣\" src=\"1f6e3.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},railway_track:{keywords:[\"train\",\"transportation\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🛤\" src=\"1f6e4.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},sunrise:{keywords:[\"morning\",\"view\",\"vacation\",\"photo\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🌅\" src=\"1f305.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},sunrise_over_mountains:{keywords:[\"view\",\"vacation\",\"photo\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🌄\" src=\"1f304.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},desert:{keywords:[\"photo\",\"warm\",\"saharah\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🏜\" src=\"1f3dc.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},beach_umbrella:{keywords:[\"weather\",\"summer\",\"sunny\",\"sand\",\"mojito\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🏖\" src=\"1f3d6.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},desert_island:{keywords:[\"photo\",\"tropical\",\"mojito\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🏝\" src=\"1f3dd.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},city_sunrise:{keywords:[\"photo\",\"good morning\",\"dawn\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🌇\" src=\"1f307.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},city_sunset:{keywords:[\"photo\",\"evening\",\"sky\",\"buildings\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🌆\" src=\"1f306.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},cityscape:{keywords:[\"photo\",\"night life\",\"urban\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🏙\" src=\"1f3d9.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},night_with_stars:{keywords:[\"evening\",\"city\",\"downtown\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🌃\" src=\"1f303.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},bridge_at_night:{keywords:[\"photo\",\"sanfrancisco\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🌉\" src=\"1f309.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},milky_way:{keywords:[\"photo\",\"space\",\"stars\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🌌\" src=\"1f30c.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},stars:{keywords:[\"night\",\"photo\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🌠\" src=\"1f320.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},sparkler:{keywords:[\"stars\",\"night\",\"shine\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🎇\" src=\"1f387.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},fireworks:{keywords:[\"photo\",\"festival\",\"carnival\",\"congratulations\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🎆\" src=\"1f386.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},rainbow:{keywords:[\"nature\",\"happy\",\"unicorn_face\",\"photo\",\"sky\",\"spring\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🌈\" src=\"1f308.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},houses:{keywords:[\"buildings\",\"photo\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🏘\" src=\"1f3d8.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},european_castle:{keywords:[\"building\",\"royalty\",\"history\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🏰\" src=\"1f3f0.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},japanese_castle:{keywords:[\"photo\",\"building\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🏯\" src=\"1f3ef.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},stadium:{keywords:[\"photo\",\"place\",\"sports\",\"concert\",\"venue\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🏟\" src=\"1f3df.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},statue_of_liberty:{keywords:[\"american\",\"newyork\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🗽\" src=\"1f5fd.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},house:{keywords:[\"building\",\"home\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🏠\" src=\"1f3e0.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},house_with_garden:{keywords:[\"home\",\"plant\",\"nature\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🏡\" src=\"1f3e1.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},derelict_house:{keywords:[\"abandon\",\"evict\",\"broken\",\"building\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🏚\" src=\"1f3da.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},office:{keywords:[\"building\",\"bureau\",\"work\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🏢\" src=\"1f3e2.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},department_store:{keywords:[\"building\",\"shopping\",\"mall\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🏬\" src=\"1f3ec.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},post_office:{keywords:[\"building\",\"envelope\",\"communication\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🏣\" src=\"1f3e3.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},european_post_office:{keywords:[\"building\",\"email\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🏤\" src=\"1f3e4.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},hospital:{keywords:[\"building\",\"health\",\"surgery\",\"doctor\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🏥\" src=\"1f3e5.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},bank:{keywords:[\"building\",\"money\",\"sales\",\"cash\",\"business\",\"enterprise\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🏦\" src=\"1f3e6.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},hotel:{keywords:[\"building\",\"accomodation\",\"checkin\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🏨\" src=\"1f3e8.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},convenience_store:{keywords:[\"building\",\"shopping\",\"groceries\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🏪\" src=\"1f3ea.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},school:{keywords:[\"building\",\"student\",\"education\",\"learn\",\"teach\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🏫\" src=\"1f3eb.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},love_hotel:{keywords:[\"like\",\"affection\",\"dating\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🏩\" src=\"1f3e9.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},wedding:{keywords:[\"love\",\"like\",\"affection\",\"couple\",\"marriage\",\"bride\",\"groom\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"💒\" src=\"1f492.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},classical_building:{keywords:[\"art\",\"culture\",\"history\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🏛\" src=\"1f3db.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},church:{keywords:[\"building\",\"religion\",\"christ\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"⛪\" src=\"26ea.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},mosque:{keywords:[\"islam\",\"worship\",\"minaret\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🕌\" src=\"1f54c.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},synagogue:{keywords:[\"judaism\",\"worship\",\"temple\",\"jewish\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🕍\" src=\"1f54d.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},kaaba:{keywords:[\"mecca\",\"mosque\",\"islam\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🕋\" src=\"1f54b.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},shinto_shrine:{keywords:[\"temple\",\"japan\",\"kyoto\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"⛩\" src=\"26e9.png\"/>',fitzpatrick_scale:false,category:\"travel_and_places\"},watch:{keywords:[\"time\",\"accessories\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"⌚\" src=\"231a.png\"/>',fitzpatrick_scale:false,category:\"objects\"},iphone:{keywords:[\"technology\",\"apple\",\"gadgets\",\"dial\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"📱\" src=\"1f4f1.png\"/>',fitzpatrick_scale:false,category:\"objects\"},calling:{keywords:[\"iphone\",\"incoming\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"📲\" src=\"1f4f2.png\"/>',fitzpatrick_scale:false,category:\"objects\"},computer:{keywords:[\"technology\",\"laptop\",\"screen\",\"display\",\"monitor\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"💻\" src=\"1f4bb.png\"/>',fitzpatrick_scale:false,category:\"objects\"},keyboard:{keywords:[\"technology\",\"computer\",\"type\",\"input\",\"text\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"⌨\" src=\"2328.png\"/>',fitzpatrick_scale:false,category:\"objects\"},desktop_computer:{keywords:[\"technology\",\"computing\",\"screen\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🖥\" src=\"1f5a5.png\"/>',fitzpatrick_scale:false,category:\"objects\"},printer:{keywords:[\"paper\",\"ink\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🖨\" src=\"1f5a8.png\"/>',fitzpatrick_scale:false,category:\"objects\"},computer_mouse:{keywords:[\"click\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🖱\" src=\"1f5b1.png\"/>',fitzpatrick_scale:false,category:\"objects\"},trackball:{keywords:[\"technology\",\"trackpad\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🖲\" src=\"1f5b2.png\"/>',fitzpatrick_scale:false,category:\"objects\"},joystick:{keywords:[\"game\",\"play\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🕹\" src=\"1f579.png\"/>',fitzpatrick_scale:false,category:\"objects\"},clamp:{keywords:[\"tool\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🗜\" src=\"1f5dc.png\"/>',fitzpatrick_scale:false,category:\"objects\"},minidisc:{keywords:[\"technology\",\"record\",\"data\",\"disk\",\"90s\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"💽\" src=\"1f4bd.png\"/>',fitzpatrick_scale:false,category:\"objects\"},floppy_disk:{keywords:[\"oldschool\",\"technology\",\"save\",\"90s\",\"80s\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"💾\" src=\"1f4be.png\"/>',fitzpatrick_scale:false,category:\"objects\"},cd:{keywords:[\"technology\",\"dvd\",\"disk\",\"disc\",\"90s\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"💿\" src=\"1f4bf.png\"/>',fitzpatrick_scale:false,category:\"objects\"},dvd:{keywords:[\"cd\",\"disk\",\"disc\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"📀\" src=\"1f4c0.png\"/>',fitzpatrick_scale:false,category:\"objects\"},vhs:{keywords:[\"record\",\"video\",\"oldschool\",\"90s\",\"80s\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"📼\" src=\"1f4fc.png\"/>',fitzpatrick_scale:false,category:\"objects\"},camera:{keywords:[\"gadgets\",\"photography\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"📷\" src=\"1f4f7.png\"/>',fitzpatrick_scale:false,category:\"objects\"},camera_flash:{keywords:[\"photography\",\"gadgets\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"📸\" src=\"1f4f8.png\"/>',fitzpatrick_scale:false,category:\"objects\"},video_camera:{keywords:[\"film\",\"record\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"📹\" src=\"1f4f9.png\"/>',fitzpatrick_scale:false,category:\"objects\"},movie_camera:{keywords:[\"film\",\"record\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🎥\" src=\"1f3a5.png\"/>',fitzpatrick_scale:false,category:\"objects\"},film_projector:{keywords:[\"video\",\"tape\",\"record\",\"movie\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"📽\" src=\"1f4fd.png\"/>',fitzpatrick_scale:false,category:\"objects\"},film_strip:{keywords:[\"movie\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🎞\" src=\"1f39e.png\"/>',fitzpatrick_scale:false,category:\"objects\"},telephone_receiver:{keywords:[\"technology\",\"communication\",\"dial\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"📞\" src=\"1f4de.png\"/>',fitzpatrick_scale:false,category:\"objects\"},phone:{keywords:[\"technology\",\"communication\",\"dial\",\"telephone\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"☎️\" src=\"260e.png\"/>',fitzpatrick_scale:false,category:\"objects\"},pager:{keywords:[\"bbcall\",\"oldschool\",\"90s\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"📟\" src=\"1f4df.png\"/>',fitzpatrick_scale:false,category:\"objects\"},fax:{keywords:[\"communication\",\"technology\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"📠\" src=\"1f4e0.png\"/>',fitzpatrick_scale:false,category:\"objects\"},tv:{keywords:[\"technology\",\"program\",\"oldschool\",\"show\",\"television\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"📺\" src=\"1f4fa.png\"/>',fitzpatrick_scale:false,category:\"objects\"},radio:{keywords:[\"communication\",\"music\",\"podcast\",\"program\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"📻\" src=\"1f4fb.png\"/>',fitzpatrick_scale:false,category:\"objects\"},studio_microphone:{keywords:[\"sing\",\"recording\",\"artist\",\"talkshow\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🎙\" src=\"1f399.png\"/>',fitzpatrick_scale:false,category:\"objects\"},level_slider:{keywords:[\"scale\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🎚\" src=\"1f39a.png\"/>',fitzpatrick_scale:false,category:\"objects\"},control_knobs:{keywords:[\"dial\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🎛\" src=\"1f39b.png\"/>',fitzpatrick_scale:false,category:\"objects\"},compass:{keywords:[\"magnetic\",\"navigation\",\"orienteering\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🧭\" src=\"1f9ed.png\"/>',fitzpatrick_scale:false,category:\"objects\"},stopwatch:{keywords:[\"time\",\"deadline\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"⏱\" src=\"23f1.png\"/>',fitzpatrick_scale:false,category:\"objects\"},timer_clock:{keywords:[\"alarm\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"⏲\" src=\"23f2.png\"/>',fitzpatrick_scale:false,category:\"objects\"},alarm_clock:{keywords:[\"time\",\"wake\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"⏰\" src=\"23f0.png\"/>',fitzpatrick_scale:false,category:\"objects\"},mantelpiece_clock:{keywords:[\"time\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🕰\" src=\"1f570.png\"/>',fitzpatrick_scale:false,category:\"objects\"},hourglass_flowing_sand:{keywords:[\"oldschool\",\"time\",\"countdown\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"⏳\" src=\"23f3.png\"/>',fitzpatrick_scale:false,category:\"objects\"},hourglass:{keywords:[\"time\",\"clock\",\"oldschool\",\"limit\",\"exam\",\"quiz\",\"test\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"⌛\" src=\"231b.png\"/>',fitzpatrick_scale:false,category:\"objects\"},satellite:{keywords:[\"communication\",\"future\",\"radio\",\"space\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"📡\" src=\"1f4e1.png\"/>',fitzpatrick_scale:false,category:\"objects\"},battery:{keywords:[\"power\",\"energy\",\"sustain\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🔋\" src=\"1f50b.png\"/>',fitzpatrick_scale:false,category:\"objects\"},electric_plug:{keywords:[\"charger\",\"power\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🔌\" src=\"1f50c.png\"/>',fitzpatrick_scale:false,category:\"objects\"},bulb:{keywords:[\"light\",\"electricity\",\"idea\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"💡\" src=\"1f4a1.png\"/>',fitzpatrick_scale:false,category:\"objects\"},flashlight:{keywords:[\"dark\",\"camping\",\"sight\",\"night\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🔦\" src=\"1f526.png\"/>',fitzpatrick_scale:false,category:\"objects\"},candle:{keywords:[\"fire\",\"wax\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🕯\" src=\"1f56f.png\"/>',fitzpatrick_scale:false,category:\"objects\"},fire_extinguisher:{keywords:[\"quench\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🧯\" src=\"1f9ef.png\"/>',fitzpatrick_scale:false,category:\"objects\"},wastebasket:{keywords:[\"bin\",\"trash\",\"rubbish\",\"garbage\",\"toss\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🗑\" src=\"1f5d1.png\"/>',fitzpatrick_scale:false,category:\"objects\"},oil_drum:{keywords:[\"barrell\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🛢\" src=\"1f6e2.png\"/>',fitzpatrick_scale:false,category:\"objects\"},money_with_wings:{keywords:[\"dollar\",\"bills\",\"payment\",\"sale\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"💸\" src=\"1f4b8.png\"/>',fitzpatrick_scale:false,category:\"objects\"},dollar:{keywords:[\"money\",\"sales\",\"bill\",\"currency\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"💵\" src=\"1f4b5.png\"/>',fitzpatrick_scale:false,category:\"objects\"},yen:{keywords:[\"money\",\"sales\",\"japanese\",\"dollar\",\"currency\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"💴\" src=\"1f4b4.png\"/>',fitzpatrick_scale:false,category:\"objects\"},euro:{keywords:[\"money\",\"sales\",\"dollar\",\"currency\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"💶\" src=\"1f4b6.png\"/>',fitzpatrick_scale:false,category:\"objects\"},pound:{keywords:[\"british\",\"sterling\",\"money\",\"sales\",\"bills\",\"uk\",\"england\",\"currency\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"💷\" src=\"1f4b7.png\"/>',fitzpatrick_scale:false,category:\"objects\"},moneybag:{keywords:[\"dollar\",\"payment\",\"coins\",\"sale\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"💰\" src=\"1f4b0.png\"/>',fitzpatrick_scale:false,category:\"objects\"},credit_card:{keywords:[\"money\",\"sales\",\"dollar\",\"bill\",\"payment\",\"shopping\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"💳\" src=\"1f4b3.png\"/>',fitzpatrick_scale:false,category:\"objects\"},gem:{keywords:[\"blue\",\"ruby\",\"diamond\",\"jewelry\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"💎\" src=\"1f48e.png\"/>',fitzpatrick_scale:false,category:\"objects\"},balance_scale:{keywords:[\"law\",\"fairness\",\"weight\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"⚖\" src=\"2696.png\"/>',fitzpatrick_scale:false,category:\"objects\"},toolbox:{keywords:[\"tools\",\"diy\",\"fix\",\"maintainer\",\"mechanic\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🧰\" src=\"1f9f0.png\"/>',fitzpatrick_scale:false,category:\"objects\"},wrench:{keywords:[\"tools\",\"diy\",\"ikea\",\"fix\",\"maintainer\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🔧\" src=\"1f527.png\"/>',fitzpatrick_scale:false,category:\"objects\"},hammer:{keywords:[\"tools\",\"build\",\"create\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🔨\" src=\"1f528.png\"/>',fitzpatrick_scale:false,category:\"objects\"},hammer_and_pick:{keywords:[\"tools\",\"build\",\"create\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"⚒\" src=\"2692.png\"/>',fitzpatrick_scale:false,category:\"objects\"},hammer_and_wrench:{keywords:[\"tools\",\"build\",\"create\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🛠\" src=\"1f6e0.png\"/>',fitzpatrick_scale:false,category:\"objects\"},pick:{keywords:[\"tools\",\"dig\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"⛏\" src=\"26cf.png\"/>',fitzpatrick_scale:false,category:\"objects\"},nut_and_bolt:{keywords:[\"handy\",\"tools\",\"fix\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🔩\" src=\"1f529.png\"/>',fitzpatrick_scale:false,category:\"objects\"},gear:{keywords:[\"cog\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"⚙\" src=\"2699.png\"/>',fitzpatrick_scale:false,category:\"objects\"},brick:{keywords:[\"bricks\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🧱\" src=\"1f9f1.png\"/>',fitzpatrick_scale:false,category:\"objects\"},chains:{keywords:[\"lock\",\"arrest\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"⛓\" src=\"26d3.png\"/>',fitzpatrick_scale:false,category:\"objects\"},magnet:{keywords:[\"attraction\",\"magnetic\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🧲\" src=\"1f9f2.png\"/>',fitzpatrick_scale:false,category:\"objects\"},gun:{keywords:[\"violence\",\"weapon\",\"pistol\",\"revolver\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🔫\" src=\"1f52b.png\"/>',fitzpatrick_scale:false,category:\"objects\"},bomb:{keywords:[\"boom\",\"explode\",\"explosion\",\"terrorism\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"💣\" src=\"1f4a3.png\"/>',fitzpatrick_scale:false,category:\"objects\"},firecracker:{keywords:[\"dynamite\",\"boom\",\"explode\",\"explosion\",\"explosive\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🧨\" src=\"1f9e8.png\"/>',fitzpatrick_scale:false,category:\"objects\"},hocho:{keywords:[\"knife\",\"blade\",\"cutlery\",\"kitchen\",\"weapon\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🔪\" src=\"1f52a.png\"/>',fitzpatrick_scale:false,category:\"objects\"},dagger:{keywords:[\"weapon\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🗡\" src=\"1f5e1.png\"/>',fitzpatrick_scale:false,category:\"objects\"},crossed_swords:{keywords:[\"weapon\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"⚔\" src=\"2694.png\"/>',fitzpatrick_scale:false,category:\"objects\"},shield:{keywords:[\"protection\",\"security\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🛡\" src=\"1f6e1.png\"/>',fitzpatrick_scale:false,category:\"objects\"},smoking:{keywords:[\"kills\",\"tobacco\",\"cigarette\",\"joint\",\"smoke\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🚬\" src=\"1f6ac.png\"/>',fitzpatrick_scale:false,category:\"objects\"},skull_and_crossbones:{keywords:[\"poison\",\"danger\",\"deadly\",\"scary\",\"death\",\"pirate\",\"evil\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"☠\" src=\"2620.png\"/>',fitzpatrick_scale:false,category:\"objects\"},coffin:{keywords:[\"vampire\",\"dead\",\"die\",\"death\",\"rip\",\"graveyard\",\"cemetery\",\"casket\",\"funeral\",\"box\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"⚰\" src=\"26b0.png\"/>',fitzpatrick_scale:false,category:\"objects\"},funeral_urn:{keywords:[\"dead\",\"die\",\"death\",\"rip\",\"ashes\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"⚱\" src=\"26b1.png\"/>',fitzpatrick_scale:false,category:\"objects\"},amphora:{keywords:[\"vase\",\"jar\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🏺\" src=\"1f3fa.png\"/>',fitzpatrick_scale:false,category:\"objects\"},crystal_ball:{keywords:[\"disco\",\"party\",\"magic\",\"circus\",\"fortune_teller\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🔮\" src=\"1f52e.png\"/>',fitzpatrick_scale:false,category:\"objects\"},prayer_beads:{keywords:[\"dhikr\",\"religious\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"📿\" src=\"1f4ff.png\"/>',fitzpatrick_scale:false,category:\"objects\"},nazar_amulet:{keywords:[\"bead\",\"charm\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🧿\" src=\"1f9ff.png\"/>',fitzpatrick_scale:false,category:\"objects\"},barber:{keywords:[\"hair\",\"salon\",\"style\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"💈\" src=\"1f488.png\"/>',fitzpatrick_scale:false,category:\"objects\"},alembic:{keywords:[\"distilling\",\"science\",\"experiment\",\"chemistry\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"⚗\" src=\"2697.png\"/>',fitzpatrick_scale:false,category:\"objects\"},telescope:{keywords:[\"stars\",\"space\",\"zoom\",\"science\",\"astronomy\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🔭\" src=\"1f52d.png\"/>',fitzpatrick_scale:false,category:\"objects\"},microscope:{keywords:[\"laboratory\",\"experiment\",\"zoomin\",\"science\",\"study\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🔬\" src=\"1f52c.png\"/>',fitzpatrick_scale:false,category:\"objects\"},hole:{keywords:[\"embarrassing\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🕳\" src=\"1f573.png\"/>',fitzpatrick_scale:false,category:\"objects\"},pill:{keywords:[\"health\",\"medicine\",\"doctor\",\"pharmacy\",\"drug\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"💊\" src=\"1f48a.png\"/>',fitzpatrick_scale:false,category:\"objects\"},syringe:{keywords:[\"health\",\"hospital\",\"drugs\",\"blood\",\"medicine\",\"needle\",\"doctor\",\"nurse\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"💉\" src=\"1f489.png\"/>',fitzpatrick_scale:false,category:\"objects\"},dna:{keywords:[\"biologist\",\"genetics\",\"life\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🧬\" src=\"1f9ec.png\"/>',fitzpatrick_scale:false,category:\"objects\"},microbe:{keywords:[\"amoeba\",\"bacteria\",\"germs\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🦠\" src=\"1f9a0.png\"/>',fitzpatrick_scale:false,category:\"objects\"},petri_dish:{keywords:[\"bacteria\",\"biology\",\"culture\",\"lab\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🧫\" src=\"1f9eb.png\"/>',fitzpatrick_scale:false,category:\"objects\"},test_tube:{keywords:[\"chemistry\",\"experiment\",\"lab\",\"science\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🧪\" src=\"1f9ea.png\"/>',fitzpatrick_scale:false,category:\"objects\"},thermometer:{keywords:[\"weather\",\"temperature\",\"hot\",\"cold\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🌡\" src=\"1f321.png\"/>',fitzpatrick_scale:false,category:\"objects\"},broom:{keywords:[\"cleaning\",\"sweeping\",\"witch\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🧹\" src=\"1f9f9.png\"/>',fitzpatrick_scale:false,category:\"objects\"},basket:{keywords:[\"laundry\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🧺\" src=\"1f9fa.png\"/>',fitzpatrick_scale:false,category:\"objects\"},toilet_paper:{keywords:[\"roll\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🧻\" src=\"1f9fb.png\"/>',fitzpatrick_scale:false,category:\"objects\"},label:{keywords:[\"sale\",\"tag\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🏷\" src=\"1f3f7.png\"/>',fitzpatrick_scale:false,category:\"objects\"},bookmark:{keywords:[\"favorite\",\"label\",\"save\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🔖\" src=\"1f516.png\"/>',fitzpatrick_scale:false,category:\"objects\"},toilet:{keywords:[\"restroom\",\"wc\",\"washroom\",\"bathroom\",\"potty\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🚽\" src=\"1f6bd.png\"/>',fitzpatrick_scale:false,category:\"objects\"},shower:{keywords:[\"clean\",\"water\",\"bathroom\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🚿\" src=\"1f6bf.png\"/>',fitzpatrick_scale:false,category:\"objects\"},bathtub:{keywords:[\"clean\",\"shower\",\"bathroom\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🛁\" src=\"1f6c1.png\"/>',fitzpatrick_scale:false,category:\"objects\"},soap:{keywords:[\"bar\",\"bathing\",\"cleaning\",\"lather\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🧼\" src=\"1f9fc.png\"/>',fitzpatrick_scale:false,category:\"objects\"},sponge:{keywords:[\"absorbing\",\"cleaning\",\"porous\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🧽\" src=\"1f9fd.png\"/>',fitzpatrick_scale:false,category:\"objects\"},lotion_bottle:{keywords:[\"moisturizer\",\"sunscreen\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🧴\" src=\"1f9f4.png\"/>',fitzpatrick_scale:false,category:\"objects\"},key:{keywords:[\"lock\",\"door\",\"password\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🔑\" src=\"1f511.png\"/>',fitzpatrick_scale:false,category:\"objects\"},old_key:{keywords:[\"lock\",\"door\",\"password\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🗝\" src=\"1f5dd.png\"/>',fitzpatrick_scale:false,category:\"objects\"},couch_and_lamp:{keywords:[\"read\",\"chill\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🛋\" src=\"1f6cb.png\"/>',fitzpatrick_scale:false,category:\"objects\"},sleeping_bed:{keywords:[\"bed\",\"rest\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🛌\" src=\"1f6cc.png\"/>',fitzpatrick_scale:true,category:\"objects\"},bed:{keywords:[\"sleep\",\"rest\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🛏\" src=\"1f6cf.png\"/>',fitzpatrick_scale:false,category:\"objects\"},door:{keywords:[\"house\",\"entry\",\"exit\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🚪\" src=\"1f6aa.png\"/>',fitzpatrick_scale:false,category:\"objects\"},bellhop_bell:{keywords:[\"service\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🛎\" src=\"1f6ce.png\"/>',fitzpatrick_scale:false,category:\"objects\"},teddy_bear:{keywords:[\"plush\",\"stuffed\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🧸\" src=\"1f9f8.png\"/>',fitzpatrick_scale:false,category:\"objects\"},framed_picture:{keywords:[\"photography\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🖼\" src=\"1f5bc.png\"/>',fitzpatrick_scale:false,category:\"objects\"},world_map:{keywords:[\"location\",\"direction\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🗺\" src=\"1f5fa.png\"/>',fitzpatrick_scale:false,category:\"objects\"},parasol_on_ground:{keywords:[\"weather\",\"summer\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"⛱\" src=\"26f1.png\"/>',fitzpatrick_scale:false,category:\"objects\"},moyai:{keywords:[\"rock\",\"easter island\",\"moai\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🗿\" src=\"1f5ff.png\"/>',fitzpatrick_scale:false,category:\"objects\"},shopping:{keywords:[\"mall\",\"buy\",\"purchase\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🛍\" src=\"1f6cd.png\"/>',fitzpatrick_scale:false,category:\"objects\"},shopping_cart:{keywords:[\"trolley\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🛒\" src=\"1f6d2.png\"/>',fitzpatrick_scale:false,category:\"objects\"},balloon:{keywords:[\"party\",\"celebration\",\"birthday\",\"circus\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🎈\" src=\"1f388.png\"/>',fitzpatrick_scale:false,category:\"objects\"},flags:{keywords:[\"fish\",\"japanese\",\"koinobori\",\"carp\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🎏\" src=\"1f38f.png\"/>',fitzpatrick_scale:false,category:\"objects\"},ribbon:{keywords:[\"decoration\",\"pink\",\"girl\",\"bowtie\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🎀\" src=\"1f380.png\"/>',fitzpatrick_scale:false,category:\"objects\"},gift:{keywords:[\"present\",\"birthday\",\"christmas\",\"xmas\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🎁\" src=\"1f381.png\"/>',fitzpatrick_scale:false,category:\"objects\"},confetti_ball:{keywords:[\"festival\",\"party\",\"birthday\",\"circus\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🎊\" src=\"1f38a.png\"/>',fitzpatrick_scale:false,category:\"objects\"},tada:{keywords:[\"party\",\"congratulations\",\"birthday\",\"magic\",\"circus\",\"celebration\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🎉\" src=\"1f389.png\"/>',fitzpatrick_scale:false,category:\"objects\"},dolls:{keywords:[\"japanese\",\"toy\",\"kimono\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🎎\" src=\"1f38e.png\"/>',fitzpatrick_scale:false,category:\"objects\"},wind_chime:{keywords:[\"nature\",\"ding\",\"spring\",\"bell\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🎐\" src=\"1f390.png\"/>',fitzpatrick_scale:false,category:\"objects\"},crossed_flags:{keywords:[\"japanese\",\"nation\",\"country\",\"border\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🎌\" src=\"1f38c.png\"/>',fitzpatrick_scale:false,category:\"objects\"},izakaya_lantern:{keywords:[\"light\",\"paper\",\"halloween\",\"spooky\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🏮\" src=\"1f3ee.png\"/>',fitzpatrick_scale:false,category:\"objects\"},red_envelope:{keywords:[\"gift\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🧧\" src=\"1f9e7.png\"/>',fitzpatrick_scale:false,category:\"objects\"},email:{keywords:[\"letter\",\"postal\",\"inbox\",\"communication\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"✉️\" src=\"2709.png\"/>',fitzpatrick_scale:false,category:\"objects\"},envelope_with_arrow:{keywords:[\"email\",\"communication\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"📩\" src=\"1f4e9.png\"/>',fitzpatrick_scale:false,category:\"objects\"},incoming_envelope:{keywords:[\"email\",\"inbox\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"📨\" src=\"1f4e8.png\"/>',fitzpatrick_scale:false,category:\"objects\"},\"e-mail\":{keywords:[\"communication\",\"inbox\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"📧\" src=\"1f4e7.png\"/>',fitzpatrick_scale:false,category:\"objects\"},love_letter:{keywords:[\"email\",\"like\",\"affection\",\"envelope\",\"valentines\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"💌\" src=\"1f48c.png\"/>',fitzpatrick_scale:false,category:\"objects\"},postbox:{keywords:[\"email\",\"letter\",\"envelope\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"📮\" src=\"1f4ee.png\"/>',fitzpatrick_scale:false,category:\"objects\"},mailbox_closed:{keywords:[\"email\",\"communication\",\"inbox\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"📪\" src=\"1f4ea.png\"/>',fitzpatrick_scale:false,category:\"objects\"},mailbox:{keywords:[\"email\",\"inbox\",\"communication\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"📫\" src=\"1f4eb.png\"/>',fitzpatrick_scale:false,category:\"objects\"},mailbox_with_mail:{keywords:[\"email\",\"inbox\",\"communication\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"📬\" src=\"1f4ec.png\"/>',fitzpatrick_scale:false,category:\"objects\"},mailbox_with_no_mail:{keywords:[\"email\",\"inbox\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"📭\" src=\"1f4ed.png\"/>',fitzpatrick_scale:false,category:\"objects\"},package:{keywords:[\"mail\",\"gift\",\"cardboard\",\"box\",\"moving\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"📦\" src=\"1f4e6.png\"/>',fitzpatrick_scale:false,category:\"objects\"},postal_horn:{keywords:[\"instrument\",\"music\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"📯\" src=\"1f4ef.png\"/>',fitzpatrick_scale:false,category:\"objects\"},inbox_tray:{keywords:[\"email\",\"documents\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"📥\" src=\"1f4e5.png\"/>',fitzpatrick_scale:false,category:\"objects\"},outbox_tray:{keywords:[\"inbox\",\"email\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"📤\" src=\"1f4e4.png\"/>',fitzpatrick_scale:false,category:\"objects\"},scroll:{keywords:[\"documents\",\"ancient\",\"history\",\"paper\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"📜\" src=\"1f4dc.png\"/>',fitzpatrick_scale:false,category:\"objects\"},page_with_curl:{keywords:[\"documents\",\"office\",\"paper\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"📃\" src=\"1f4c3.png\"/>',fitzpatrick_scale:false,category:\"objects\"},bookmark_tabs:{keywords:[\"favorite\",\"save\",\"order\",\"tidy\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"📑\" src=\"1f4d1.png\"/>',fitzpatrick_scale:false,category:\"objects\"},receipt:{keywords:[\"accounting\",\"expenses\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🧾\" src=\"1f9fe.png\"/>',fitzpatrick_scale:false,category:\"objects\"},bar_chart:{keywords:[\"graph\",\"presentation\",\"stats\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"📊\" src=\"1f4ca.png\"/>',fitzpatrick_scale:false,category:\"objects\"},chart_with_upwards_trend:{keywords:[\"graph\",\"presentation\",\"stats\",\"recovery\",\"business\",\"economics\",\"money\",\"sales\",\"good\",\"success\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"📈\" src=\"1f4c8.png\"/>',fitzpatrick_scale:false,category:\"objects\"},chart_with_downwards_trend:{keywords:[\"graph\",\"presentation\",\"stats\",\"recession\",\"business\",\"economics\",\"money\",\"sales\",\"bad\",\"failure\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"📉\" src=\"1f4c9.png\"/>',fitzpatrick_scale:false,category:\"objects\"},page_facing_up:{keywords:[\"documents\",\"office\",\"paper\",\"information\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"📄\" src=\"1f4c4.png\"/>',fitzpatrick_scale:false,category:\"objects\"},date:{keywords:[\"calendar\",\"schedule\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"📅\" src=\"1f4c5.png\"/>',fitzpatrick_scale:false,category:\"objects\"},calendar:{keywords:[\"schedule\",\"date\",\"planning\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"📆\" src=\"1f4c6.png\"/>',fitzpatrick_scale:false,category:\"objects\"},spiral_calendar:{keywords:[\"date\",\"schedule\",\"planning\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🗓\" src=\"1f5d3.png\"/>',fitzpatrick_scale:false,category:\"objects\"},card_index:{keywords:[\"business\",\"stationery\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"📇\" src=\"1f4c7.png\"/>',fitzpatrick_scale:false,category:\"objects\"},card_file_box:{keywords:[\"business\",\"stationery\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🗃\" src=\"1f5c3.png\"/>',fitzpatrick_scale:false,category:\"objects\"},ballot_box:{keywords:[\"election\",\"vote\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🗳\" src=\"1f5f3.png\"/>',fitzpatrick_scale:false,category:\"objects\"},file_cabinet:{keywords:[\"filing\",\"organizing\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🗄\" src=\"1f5c4.png\"/>',fitzpatrick_scale:false,category:\"objects\"},clipboard:{keywords:[\"stationery\",\"documents\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"📋\" src=\"1f4cb.png\"/>',fitzpatrick_scale:false,category:\"objects\"},spiral_notepad:{keywords:[\"memo\",\"stationery\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🗒\" src=\"1f5d2.png\"/>',fitzpatrick_scale:false,category:\"objects\"},file_folder:{keywords:[\"documents\",\"business\",\"office\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"📁\" src=\"1f4c1.png\"/>',fitzpatrick_scale:false,category:\"objects\"},open_file_folder:{keywords:[\"documents\",\"load\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"📂\" src=\"1f4c2.png\"/>',fitzpatrick_scale:false,category:\"objects\"},card_index_dividers:{keywords:[\"organizing\",\"business\",\"stationery\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🗂\" src=\"1f5c2.png\"/>',fitzpatrick_scale:false,category:\"objects\"},newspaper_roll:{keywords:[\"press\",\"headline\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🗞\" src=\"1f5de.png\"/>',fitzpatrick_scale:false,category:\"objects\"},newspaper:{keywords:[\"press\",\"headline\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"📰\" src=\"1f4f0.png\"/>',fitzpatrick_scale:false,category:\"objects\"},notebook:{keywords:[\"stationery\",\"record\",\"notes\",\"paper\",\"study\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"📓\" src=\"1f4d3.png\"/>',fitzpatrick_scale:false,category:\"objects\"},closed_book:{keywords:[\"read\",\"library\",\"knowledge\",\"textbook\",\"learn\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"📕\" src=\"1f4d5.png\"/>',fitzpatrick_scale:false,category:\"objects\"},green_book:{keywords:[\"read\",\"library\",\"knowledge\",\"study\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"📗\" src=\"1f4d7.png\"/>',fitzpatrick_scale:false,category:\"objects\"},blue_book:{keywords:[\"read\",\"library\",\"knowledge\",\"learn\",\"study\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"📘\" src=\"1f4d8.png\"/>',fitzpatrick_scale:false,category:\"objects\"},orange_book:{keywords:[\"read\",\"library\",\"knowledge\",\"textbook\",\"study\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"📙\" src=\"1f4d9.png\"/>',fitzpatrick_scale:false,category:\"objects\"},notebook_with_decorative_cover:{keywords:[\"classroom\",\"notes\",\"record\",\"paper\",\"study\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"📔\" src=\"1f4d4.png\"/>',fitzpatrick_scale:false,category:\"objects\"},ledger:{keywords:[\"notes\",\"paper\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"📒\" src=\"1f4d2.png\"/>',fitzpatrick_scale:false,category:\"objects\"},books:{keywords:[\"literature\",\"library\",\"study\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"📚\" src=\"1f4da.png\"/>',fitzpatrick_scale:false,category:\"objects\"},open_book:{keywords:[\"book\",\"read\",\"library\",\"knowledge\",\"literature\",\"learn\",\"study\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"📖\" src=\"1f4d6.png\"/>',fitzpatrick_scale:false,category:\"objects\"},safety_pin:{keywords:[\"diaper\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🧷\" src=\"1f9f7.png\"/>',fitzpatrick_scale:false,category:\"objects\"},link:{keywords:[\"rings\",\"url\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🔗\" src=\"1f517.png\"/>',fitzpatrick_scale:false,category:\"objects\"},paperclip:{keywords:[\"documents\",\"stationery\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"📎\" src=\"1f4ce.png\"/>',fitzpatrick_scale:false,category:\"objects\"},paperclips:{keywords:[\"documents\",\"stationery\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🖇\" src=\"1f587.png\"/>',fitzpatrick_scale:false,category:\"objects\"},scissors:{keywords:[\"stationery\",\"cut\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"✂️\" src=\"2702.png\"/>',fitzpatrick_scale:false,category:\"objects\"},triangular_ruler:{keywords:[\"stationery\",\"math\",\"architect\",\"sketch\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"📐\" src=\"1f4d0.png\"/>',fitzpatrick_scale:false,category:\"objects\"},straight_ruler:{keywords:[\"stationery\",\"calculate\",\"length\",\"math\",\"school\",\"drawing\",\"architect\",\"sketch\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"📏\" src=\"1f4cf.png\"/>',fitzpatrick_scale:false,category:\"objects\"},abacus:{keywords:[\"calculation\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🧮\" src=\"1f9ee.png\"/>',fitzpatrick_scale:false,category:\"objects\"},pushpin:{keywords:[\"stationery\",\"mark\",\"here\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"📌\" src=\"1f4cc.png\"/>',fitzpatrick_scale:false,category:\"objects\"},round_pushpin:{keywords:[\"stationery\",\"location\",\"map\",\"here\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"📍\" src=\"1f4cd.png\"/>',fitzpatrick_scale:false,category:\"objects\"},triangular_flag_on_post:{keywords:[\"mark\",\"milestone\",\"place\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🚩\" src=\"1f6a9.png\"/>',fitzpatrick_scale:false,category:\"objects\"},white_flag:{keywords:[\"losing\",\"loser\",\"lost\",\"surrender\",\"give up\",\"fail\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🏳\" src=\"1f3f3.png\"/>',fitzpatrick_scale:false,category:\"objects\"},black_flag:{keywords:[\"pirate\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🏴\" src=\"1f3f4.png\"/>',fitzpatrick_scale:false,category:\"objects\"},rainbow_flag:{keywords:[\"flag\",\"rainbow\",\"pride\",\"gay\",\"lgbt\",\"glbt\",\"queer\",\"homosexual\",\"lesbian\",\"bisexual\",\"transgender\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🏳️‍🌈\" src=\"1f3f3-fe0f-200d-1f308.png\"/>',fitzpatrick_scale:false,category:\"objects\"},closed_lock_with_key:{keywords:[\"security\",\"privacy\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🔐\" src=\"1f510.png\"/>',fitzpatrick_scale:false,category:\"objects\"},lock:{keywords:[\"security\",\"password\",\"padlock\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🔒\" src=\"1f512.png\"/>',fitzpatrick_scale:false,category:\"objects\"},unlock:{keywords:[\"privacy\",\"security\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🔓\" src=\"1f513.png\"/>',fitzpatrick_scale:false,category:\"objects\"},lock_with_ink_pen:{keywords:[\"security\",\"secret\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🔏\" src=\"1f50f.png\"/>',fitzpatrick_scale:false,category:\"objects\"},pen:{keywords:[\"stationery\",\"writing\",\"write\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🖊\" src=\"1f58a.png\"/>',fitzpatrick_scale:false,category:\"objects\"},fountain_pen:{keywords:[\"stationery\",\"writing\",\"write\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🖋\" src=\"1f58b.png\"/>',fitzpatrick_scale:false,category:\"objects\"},black_nib:{keywords:[\"pen\",\"stationery\",\"writing\",\"write\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"✒️\" src=\"2712.png\"/>',fitzpatrick_scale:false,category:\"objects\"},memo:{keywords:[\"write\",\"documents\",\"stationery\",\"pencil\",\"paper\",\"writing\",\"legal\",\"exam\",\"quiz\",\"test\",\"study\",\"compose\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"📝\" src=\"1f4dd.png\"/>',fitzpatrick_scale:false,category:\"objects\"},pencil2:{keywords:[\"stationery\",\"write\",\"paper\",\"writing\",\"school\",\"study\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"✏️\" src=\"270f.png\"/>',fitzpatrick_scale:false,category:\"objects\"},crayon:{keywords:[\"drawing\",\"creativity\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🖍\" src=\"1f58d.png\"/>',fitzpatrick_scale:false,category:\"objects\"},paintbrush:{keywords:[\"drawing\",\"creativity\",\"art\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🖌\" src=\"1f58c.png\"/>',fitzpatrick_scale:false,category:\"objects\"},mag:{keywords:[\"search\",\"zoom\",\"find\",\"detective\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🔍\" src=\"1f50d.png\"/>',fitzpatrick_scale:false,category:\"objects\"},mag_right:{keywords:[\"search\",\"zoom\",\"find\",\"detective\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🔎\" src=\"1f50e.png\"/>',fitzpatrick_scale:false,category:\"objects\"},heart:{keywords:[\"love\",\"like\",\"valentines\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"❤️\" src=\"2764.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},orange_heart:{keywords:[\"love\",\"like\",\"affection\",\"valentines\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🧡\" src=\"1f9e1.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},yellow_heart:{keywords:[\"love\",\"like\",\"affection\",\"valentines\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"💛\" src=\"1f49b.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},green_heart:{keywords:[\"love\",\"like\",\"affection\",\"valentines\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"💚\" src=\"1f49a.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},blue_heart:{keywords:[\"love\",\"like\",\"affection\",\"valentines\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"💙\" src=\"1f499.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},purple_heart:{keywords:[\"love\",\"like\",\"affection\",\"valentines\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"💜\" src=\"1f49c.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},black_heart:{keywords:[\"evil\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🖤\" src=\"1f5a4.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},broken_heart:{keywords:[\"sad\",\"sorry\",\"break\",\"heart\",\"heartbreak\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"💔\" src=\"1f494.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},heavy_heart_exclamation:{keywords:[\"decoration\",\"love\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"❣\" src=\"2763.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},two_hearts:{keywords:[\"love\",\"like\",\"affection\",\"valentines\",\"heart\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"💕\" src=\"1f495.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},revolving_hearts:{keywords:[\"love\",\"like\",\"affection\",\"valentines\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"💞\" src=\"1f49e.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},heartbeat:{keywords:[\"love\",\"like\",\"affection\",\"valentines\",\"pink\",\"heart\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"💓\" src=\"1f493.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},heartpulse:{keywords:[\"like\",\"love\",\"affection\",\"valentines\",\"pink\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"💗\" src=\"1f497.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},sparkling_heart:{keywords:[\"love\",\"like\",\"affection\",\"valentines\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"💖\" src=\"1f496.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},cupid:{keywords:[\"love\",\"like\",\"heart\",\"affection\",\"valentines\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"💘\" src=\"1f498.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},gift_heart:{keywords:[\"love\",\"valentines\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"💝\" src=\"1f49d.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},heart_decoration:{keywords:[\"purple-square\",\"love\",\"like\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"💟\" src=\"1f49f.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},peace_symbol:{keywords:[\"hippie\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"☮\" src=\"262e.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},latin_cross:{keywords:[\"christianity\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"✝\" src=\"271d.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},star_and_crescent:{keywords:[\"islam\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"☪\" src=\"262a.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},om:{keywords:[\"hinduism\",\"buddhism\",\"sikhism\",\"jainism\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🕉\" src=\"1f549.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},wheel_of_dharma:{keywords:[\"hinduism\",\"buddhism\",\"sikhism\",\"jainism\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"☸\" src=\"2638.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},star_of_david:{keywords:[\"judaism\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"✡\" src=\"2721.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},six_pointed_star:{keywords:[\"purple-square\",\"religion\",\"jewish\",\"hexagram\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🔯\" src=\"1f52f.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},menorah:{keywords:[\"hanukkah\",\"candles\",\"jewish\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🕎\" src=\"1f54e.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},yin_yang:{keywords:[\"balance\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"☯\" src=\"262f.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},orthodox_cross:{keywords:[\"suppedaneum\",\"religion\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"☦\" src=\"2626.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},place_of_worship:{keywords:[\"religion\",\"church\",\"temple\",\"prayer\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🛐\" src=\"1f6d0.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},ophiuchus:{keywords:[\"sign\",\"purple-square\",\"constellation\",\"astrology\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"⛎\" src=\"26ce.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},aries:{keywords:[\"sign\",\"purple-square\",\"zodiac\",\"astrology\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"♈\" src=\"2648.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},taurus:{keywords:[\"purple-square\",\"sign\",\"zodiac\",\"astrology\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"♉\" src=\"2649.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},gemini:{keywords:[\"sign\",\"zodiac\",\"purple-square\",\"astrology\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"♊\" src=\"264a.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},cancer:{keywords:[\"sign\",\"zodiac\",\"purple-square\",\"astrology\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"♋\" src=\"264b.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},leo:{keywords:[\"sign\",\"purple-square\",\"zodiac\",\"astrology\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"♌\" src=\"264c.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},virgo:{keywords:[\"sign\",\"zodiac\",\"purple-square\",\"astrology\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"♍\" src=\"264d.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},libra:{keywords:[\"sign\",\"purple-square\",\"zodiac\",\"astrology\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"♎\" src=\"264e.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},scorpius:{keywords:[\"sign\",\"zodiac\",\"purple-square\",\"astrology\",\"scorpio\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"♏\" src=\"264f.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},sagittarius:{keywords:[\"sign\",\"zodiac\",\"purple-square\",\"astrology\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"♐\" src=\"2650.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},capricorn:{keywords:[\"sign\",\"zodiac\",\"purple-square\",\"astrology\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"♑\" src=\"2651.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},aquarius:{keywords:[\"sign\",\"purple-square\",\"zodiac\",\"astrology\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"♒\" src=\"2652.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},pisces:{keywords:[\"purple-square\",\"sign\",\"zodiac\",\"astrology\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"♓\" src=\"2653.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},id:{keywords:[\"purple-square\",\"words\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🆔\" src=\"1f194.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},atom_symbol:{keywords:[\"science\",\"physics\",\"chemistry\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"⚛\" src=\"269b.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},u7a7a:{keywords:[\"kanji\",\"japanese\",\"chinese\",\"empty\",\"sky\",\"blue-square\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🈳\" src=\"1f233.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},u5272:{keywords:[\"cut\",\"divide\",\"chinese\",\"kanji\",\"pink-square\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🈹\" src=\"1f239.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},radioactive:{keywords:[\"nuclear\",\"danger\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"☢\" src=\"2622.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},biohazard:{keywords:[\"danger\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"☣\" src=\"2623.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},mobile_phone_off:{keywords:[\"mute\",\"orange-square\",\"silence\",\"quiet\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"📴\" src=\"1f4f4.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},vibration_mode:{keywords:[\"orange-square\",\"phone\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"📳\" src=\"1f4f3.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},u6709:{keywords:[\"orange-square\",\"chinese\",\"have\",\"kanji\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🈶\" src=\"1f236.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},u7121:{keywords:[\"nothing\",\"chinese\",\"kanji\",\"japanese\",\"orange-square\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🈚\" src=\"1f21a.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},u7533:{keywords:[\"chinese\",\"japanese\",\"kanji\",\"orange-square\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🈸\" src=\"1f238.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},u55b6:{keywords:[\"japanese\",\"opening hours\",\"orange-square\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🈺\" src=\"1f23a.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},u6708:{keywords:[\"chinese\",\"month\",\"moon\",\"japanese\",\"orange-square\",\"kanji\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🈷️\" src=\"1f237.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},eight_pointed_black_star:{keywords:[\"orange-square\",\"shape\",\"polygon\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"✴️\" src=\"2734.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},vs:{keywords:[\"words\",\"orange-square\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🆚\" src=\"1f19a.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},accept:{keywords:[\"ok\",\"good\",\"chinese\",\"kanji\",\"agree\",\"yes\",\"orange-circle\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🉑\" src=\"1f251.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},white_flower:{keywords:[\"japanese\",\"spring\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"💮\" src=\"1f4ae.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},ideograph_advantage:{keywords:[\"chinese\",\"kanji\",\"obtain\",\"get\",\"circle\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🉐\" src=\"1f250.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},secret:{keywords:[\"privacy\",\"chinese\",\"sshh\",\"kanji\",\"red-circle\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"㊙️\" src=\"3299.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},congratulations:{keywords:[\"chinese\",\"kanji\",\"japanese\",\"red-circle\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"㊗️\" src=\"3297.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},u5408:{keywords:[\"japanese\",\"chinese\",\"join\",\"kanji\",\"red-square\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🈴\" src=\"1f234.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},u6e80:{keywords:[\"full\",\"chinese\",\"japanese\",\"red-square\",\"kanji\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🈵\" src=\"1f235.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},u7981:{keywords:[\"kanji\",\"japanese\",\"chinese\",\"forbidden\",\"limit\",\"restricted\",\"red-square\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🈲\" src=\"1f232.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},a:{keywords:[\"red-square\",\"alphabet\",\"letter\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🅰️\" src=\"1f170.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},b:{keywords:[\"red-square\",\"alphabet\",\"letter\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🅱️\" src=\"1f171.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},ab:{keywords:[\"red-square\",\"alphabet\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🆎\" src=\"1f18e.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},cl:{keywords:[\"alphabet\",\"words\",\"red-square\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🆑\" src=\"1f191.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},o2:{keywords:[\"alphabet\",\"red-square\",\"letter\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🅾️\" src=\"1f17e.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},sos:{keywords:[\"help\",\"red-square\",\"words\",\"emergency\",\"911\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🆘\" src=\"1f198.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},no_entry:{keywords:[\"limit\",\"security\",\"privacy\",\"bad\",\"denied\",\"stop\",\"circle\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"⛔\" src=\"26d4.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},name_badge:{keywords:[\"fire\",\"forbid\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"📛\" src=\"1f4db.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},no_entry_sign:{keywords:[\"forbid\",\"stop\",\"limit\",\"denied\",\"disallow\",\"circle\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🚫\" src=\"1f6ab.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},x:{keywords:[\"no\",\"delete\",\"remove\",\"cancel\",\"red\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"❌\" src=\"274c.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},o:{keywords:[\"circle\",\"round\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"⭕\" src=\"2b55.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},stop_sign:{keywords:[\"stop\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🛑\" src=\"1f6d1.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},anger:{keywords:[\"angry\",\"mad\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"💢\" src=\"1f4a2.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},hotsprings:{keywords:[\"bath\",\"warm\",\"relax\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"♨️\" src=\"2668.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},no_pedestrians:{keywords:[\"rules\",\"crossing\",\"walking\",\"circle\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🚷\" src=\"1f6b7.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},do_not_litter:{keywords:[\"trash\",\"bin\",\"garbage\",\"circle\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🚯\" src=\"1f6af.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},no_bicycles:{keywords:[\"cyclist\",\"prohibited\",\"circle\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🚳\" src=\"1f6b3.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},\"non-potable_water\":{keywords:[\"drink\",\"faucet\",\"tap\",\"circle\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🚱\" src=\"1f6b1.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},underage:{keywords:[\"18\",\"drink\",\"pub\",\"night\",\"minor\",\"circle\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🔞\" src=\"1f51e.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},no_mobile_phones:{keywords:[\"iphone\",\"mute\",\"circle\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"📵\" src=\"1f4f5.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},exclamation:{keywords:[\"heavy_exclamation_mark\",\"danger\",\"surprise\",\"punctuation\",\"wow\",\"warning\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"❗\" src=\"2757.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},grey_exclamation:{keywords:[\"surprise\",\"punctuation\",\"gray\",\"wow\",\"warning\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"❕\" src=\"2755.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},question:{keywords:[\"doubt\",\"confused\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"❓\" src=\"2753.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},grey_question:{keywords:[\"doubts\",\"gray\",\"huh\",\"confused\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"❔\" src=\"2754.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},bangbang:{keywords:[\"exclamation\",\"surprise\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"‼️\" src=\"203c.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},interrobang:{keywords:[\"wat\",\"punctuation\",\"surprise\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"⁉️\" src=\"2049.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},low_brightness:{keywords:[\"sun\",\"afternoon\",\"warm\",\"summer\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🔅\" src=\"1f505.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},high_brightness:{keywords:[\"sun\",\"light\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🔆\" src=\"1f506.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},trident:{keywords:[\"weapon\",\"spear\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🔱\" src=\"1f531.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},fleur_de_lis:{keywords:[\"decorative\",\"scout\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"⚜\" src=\"269c.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},part_alternation_mark:{keywords:[\"graph\",\"presentation\",\"stats\",\"business\",\"economics\",\"bad\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"〽️\" src=\"303d.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},warning:{keywords:[\"exclamation\",\"wip\",\"alert\",\"error\",\"problem\",\"issue\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"⚠️\" src=\"26a0.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},children_crossing:{keywords:[\"school\",\"warning\",\"danger\",\"sign\",\"driving\",\"yellow-diamond\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🚸\" src=\"1f6b8.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},beginner:{keywords:[\"badge\",\"shield\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🔰\" src=\"1f530.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},recycle:{keywords:[\"arrow\",\"environment\",\"garbage\",\"trash\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"♻️\" src=\"267b.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},u6307:{keywords:[\"chinese\",\"point\",\"green-square\",\"kanji\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🈯\" src=\"1f22f.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},chart:{keywords:[\"green-square\",\"graph\",\"presentation\",\"stats\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"💹\" src=\"1f4b9.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},sparkle:{keywords:[\"stars\",\"green-square\",\"awesome\",\"good\",\"fireworks\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"❇️\" src=\"2747.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},eight_spoked_asterisk:{keywords:[\"star\",\"sparkle\",\"green-square\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"✳️\" src=\"2733.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},negative_squared_cross_mark:{keywords:[\"x\",\"green-square\",\"no\",\"deny\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"❎\" src=\"274e.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},white_check_mark:{keywords:[\"green-square\",\"ok\",\"agree\",\"vote\",\"election\",\"answer\",\"tick\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"✅\" src=\"2705.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},diamond_shape_with_a_dot_inside:{keywords:[\"jewel\",\"blue\",\"gem\",\"crystal\",\"fancy\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"💠\" src=\"1f4a0.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},cyclone:{keywords:[\"weather\",\"swirl\",\"blue\",\"cloud\",\"vortex\",\"spiral\",\"whirlpool\",\"spin\",\"tornado\",\"hurricane\",\"typhoon\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🌀\" src=\"1f300.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},loop:{keywords:[\"tape\",\"cassette\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"➿\" src=\"27bf.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},globe_with_meridians:{keywords:[\"earth\",\"international\",\"world\",\"internet\",\"interweb\",\"i18n\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🌐\" src=\"1f310.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},m:{keywords:[\"alphabet\",\"blue-circle\",\"letter\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"Ⓜ️\" src=\"24c2.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},atm:{keywords:[\"money\",\"sales\",\"cash\",\"blue-square\",\"payment\",\"bank\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🏧\" src=\"1f3e7.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},sa:{keywords:[\"japanese\",\"blue-square\",\"katakana\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🈂️\" src=\"1f202.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},passport_control:{keywords:[\"custom\",\"blue-square\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🛂\" src=\"1f6c2.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},customs:{keywords:[\"passport\",\"border\",\"blue-square\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🛃\" src=\"1f6c3.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},baggage_claim:{keywords:[\"blue-square\",\"airport\",\"transport\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🛄\" src=\"1f6c4.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},left_luggage:{keywords:[\"blue-square\",\"travel\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🛅\" src=\"1f6c5.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},wheelchair:{keywords:[\"blue-square\",\"disabled\",\"a11y\",\"accessibility\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"♿\" src=\"267f.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},no_smoking:{keywords:[\"cigarette\",\"blue-square\",\"smell\",\"smoke\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🚭\" src=\"1f6ad.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},wc:{keywords:[\"toilet\",\"restroom\",\"blue-square\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🚾\" src=\"1f6be.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},parking:{keywords:[\"cars\",\"blue-square\",\"alphabet\",\"letter\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🅿️\" src=\"1f17f.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},potable_water:{keywords:[\"blue-square\",\"liquid\",\"restroom\",\"cleaning\",\"faucet\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🚰\" src=\"1f6b0.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},mens:{keywords:[\"toilet\",\"restroom\",\"wc\",\"blue-square\",\"gender\",\"male\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🚹\" src=\"1f6b9.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},womens:{keywords:[\"purple-square\",\"woman\",\"female\",\"toilet\",\"loo\",\"restroom\",\"gender\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🚺\" src=\"1f6ba.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},baby_symbol:{keywords:[\"orange-square\",\"child\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🚼\" src=\"1f6bc.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},restroom:{keywords:[\"blue-square\",\"toilet\",\"refresh\",\"wc\",\"gender\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🚻\" src=\"1f6bb.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},put_litter_in_its_place:{keywords:[\"blue-square\",\"sign\",\"human\",\"info\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🚮\" src=\"1f6ae.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},cinema:{keywords:[\"blue-square\",\"record\",\"film\",\"movie\",\"curtain\",\"stage\",\"theater\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🎦\" src=\"1f3a6.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},signal_strength:{keywords:[\"blue-square\",\"reception\",\"phone\",\"internet\",\"connection\",\"wifi\",\"bluetooth\",\"bars\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"📶\" src=\"1f4f6.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},koko:{keywords:[\"blue-square\",\"here\",\"katakana\",\"japanese\",\"destination\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🈁\" src=\"1f201.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},ng:{keywords:[\"blue-square\",\"words\",\"shape\",\"icon\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🆖\" src=\"1f196.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},ok:{keywords:[\"good\",\"agree\",\"yes\",\"blue-square\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🆗\" src=\"1f197.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},up:{keywords:[\"blue-square\",\"above\",\"high\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🆙\" src=\"1f199.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},cool:{keywords:[\"words\",\"blue-square\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🆒\" src=\"1f192.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},new:{keywords:[\"blue-square\",\"words\",\"start\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🆕\" src=\"1f195.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},free:{keywords:[\"blue-square\",\"words\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🆓\" src=\"1f193.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},zero:{keywords:[\"0\",\"numbers\",\"blue-square\",\"null\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"0️⃣\" src=\"30-20e3.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},one:{keywords:[\"blue-square\",\"numbers\",\"1\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"1️⃣\" src=\"31-20e3.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},two:{keywords:[\"numbers\",\"2\",\"prime\",\"blue-square\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"2️⃣\" src=\"32-20e3.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},three:{keywords:[\"3\",\"numbers\",\"prime\",\"blue-square\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"3️⃣\" src=\"33-20e3.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},four:{keywords:[\"4\",\"numbers\",\"blue-square\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"4️⃣\" src=\"34-20e3.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},five:{keywords:[\"5\",\"numbers\",\"blue-square\",\"prime\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"5️⃣\" src=\"35-20e3.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},six:{keywords:[\"6\",\"numbers\",\"blue-square\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"6️⃣\" src=\"36-20e3.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},seven:{keywords:[\"7\",\"numbers\",\"blue-square\",\"prime\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"7️⃣\" src=\"37-20e3.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},eight:{keywords:[\"8\",\"blue-square\",\"numbers\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"8️⃣\" src=\"38-20e3.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},nine:{keywords:[\"blue-square\",\"numbers\",\"9\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"9️⃣\" src=\"39-20e3.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},keycap_ten:{keywords:[\"numbers\",\"10\",\"blue-square\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🔟\" src=\"1f51f.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},asterisk:{keywords:[\"star\",\"keycap\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"*⃣\" src=\"2a-20e3.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},eject_button:{keywords:[\"blue-square\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"⏏️\" src=\"23cf.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},arrow_forward:{keywords:[\"blue-square\",\"right\",\"direction\",\"play\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"▶️\" src=\"25b6.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},pause_button:{keywords:[\"pause\",\"blue-square\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"⏸\" src=\"23f8.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},next_track_button:{keywords:[\"forward\",\"next\",\"blue-square\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"⏭\" src=\"23ed.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},stop_button:{keywords:[\"blue-square\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"⏹\" src=\"23f9.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},record_button:{keywords:[\"blue-square\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"⏺\" src=\"23fa.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},play_or_pause_button:{keywords:[\"blue-square\",\"play\",\"pause\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"⏯\" src=\"23ef.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},previous_track_button:{keywords:[\"backward\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"⏮\" src=\"23ee.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},fast_forward:{keywords:[\"blue-square\",\"play\",\"speed\",\"continue\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"⏩\" src=\"23e9.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},rewind:{keywords:[\"play\",\"blue-square\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"⏪\" src=\"23ea.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},twisted_rightwards_arrows:{keywords:[\"blue-square\",\"shuffle\",\"music\",\"random\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🔀\" src=\"1f500.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},repeat:{keywords:[\"loop\",\"record\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🔁\" src=\"1f501.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},repeat_one:{keywords:[\"blue-square\",\"loop\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🔂\" src=\"1f502.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},arrow_backward:{keywords:[\"blue-square\",\"left\",\"direction\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"◀️\" src=\"25c0.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},arrow_up_small:{keywords:[\"blue-square\",\"triangle\",\"direction\",\"point\",\"forward\",\"top\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🔼\" src=\"1f53c.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},arrow_down_small:{keywords:[\"blue-square\",\"direction\",\"bottom\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🔽\" src=\"1f53d.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},arrow_double_up:{keywords:[\"blue-square\",\"direction\",\"top\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"⏫\" src=\"23eb.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},arrow_double_down:{keywords:[\"blue-square\",\"direction\",\"bottom\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"⏬\" src=\"23ec.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},arrow_right:{keywords:[\"blue-square\",\"next\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"➡️\" src=\"27a1.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},arrow_left:{keywords:[\"blue-square\",\"previous\",\"back\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"⬅️\" src=\"2b05.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},arrow_up:{keywords:[\"blue-square\",\"continue\",\"top\",\"direction\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"⬆️\" src=\"2b06.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},arrow_down:{keywords:[\"blue-square\",\"direction\",\"bottom\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"⬇️\" src=\"2b07.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},arrow_upper_right:{keywords:[\"blue-square\",\"point\",\"direction\",\"diagonal\",\"northeast\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"↗️\" src=\"2197.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},arrow_lower_right:{keywords:[\"blue-square\",\"direction\",\"diagonal\",\"southeast\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"↘️\" src=\"2198.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},arrow_lower_left:{keywords:[\"blue-square\",\"direction\",\"diagonal\",\"southwest\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"↙️\" src=\"2199.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},arrow_upper_left:{keywords:[\"blue-square\",\"point\",\"direction\",\"diagonal\",\"northwest\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"↖️\" src=\"2196.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},arrow_up_down:{keywords:[\"blue-square\",\"direction\",\"way\",\"vertical\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"↕️\" src=\"2195.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},left_right_arrow:{keywords:[\"shape\",\"direction\",\"horizontal\",\"sideways\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"↔️\" src=\"2194.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},arrows_counterclockwise:{keywords:[\"blue-square\",\"sync\",\"cycle\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🔄\" src=\"1f504.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},arrow_right_hook:{keywords:[\"blue-square\",\"return\",\"rotate\",\"direction\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"↪️\" src=\"21aa.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},leftwards_arrow_with_hook:{keywords:[\"back\",\"return\",\"blue-square\",\"undo\",\"enter\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"↩️\" src=\"21a9.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},arrow_heading_up:{keywords:[\"blue-square\",\"direction\",\"top\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"⤴️\" src=\"2934.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},arrow_heading_down:{keywords:[\"blue-square\",\"direction\",\"bottom\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"⤵️\" src=\"2935.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},hash:{keywords:[\"symbol\",\"blue-square\",\"twitter\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"#️⃣\" src=\"23-20e3.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},information_source:{keywords:[\"blue-square\",\"alphabet\",\"letter\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"ℹ️\" src=\"2139.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},abc:{keywords:[\"blue-square\",\"alphabet\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🔤\" src=\"1f524.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},abcd:{keywords:[\"blue-square\",\"alphabet\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🔡\" src=\"1f521.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},capital_abcd:{keywords:[\"alphabet\",\"words\",\"blue-square\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🔠\" src=\"1f520.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},symbols:{keywords:[\"blue-square\",\"music\",\"note\",\"ampersand\",\"percent\",\"glyphs\",\"characters\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🔣\" src=\"1f523.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},musical_note:{keywords:[\"score\",\"tone\",\"sound\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🎵\" src=\"1f3b5.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},notes:{keywords:[\"music\",\"score\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🎶\" src=\"1f3b6.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},wavy_dash:{keywords:[\"draw\",\"line\",\"moustache\",\"mustache\",\"squiggle\",\"scribble\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"〰️\" src=\"3030.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},curly_loop:{keywords:[\"scribble\",\"draw\",\"shape\",\"squiggle\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"➰\" src=\"27b0.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},heavy_check_mark:{keywords:[\"ok\",\"nike\",\"answer\",\"yes\",\"tick\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"✔️\" src=\"2714.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},arrows_clockwise:{keywords:[\"sync\",\"cycle\",\"round\",\"repeat\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🔃\" src=\"1f503.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},heavy_plus_sign:{keywords:[\"math\",\"calculation\",\"addition\",\"more\",\"increase\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"➕\" src=\"2795.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},heavy_minus_sign:{keywords:[\"math\",\"calculation\",\"subtract\",\"less\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"➖\" src=\"2796.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},heavy_division_sign:{keywords:[\"divide\",\"math\",\"calculation\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"➗\" src=\"2797.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},heavy_multiplication_x:{keywords:[\"math\",\"calculation\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"✖️\" src=\"2716.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},infinity:{keywords:[\"forever\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"♾\" src=\"267e.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},heavy_dollar_sign:{keywords:[\"money\",\"sales\",\"payment\",\"currency\",\"buck\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"💲\" src=\"1f4b2.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},currency_exchange:{keywords:[\"money\",\"sales\",\"dollar\",\"travel\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"💱\" src=\"1f4b1.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},copyright:{keywords:[\"ip\",\"license\",\"circle\",\"law\",\"legal\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"©️\" src=\"a9.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},registered:{keywords:[\"alphabet\",\"circle\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"®️\" src=\"ae.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},tm:{keywords:[\"trademark\",\"brand\",\"law\",\"legal\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"™️\" src=\"2122.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},end:{keywords:[\"words\",\"arrow\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🔚\" src=\"1f51a.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},back:{keywords:[\"arrow\",\"words\",\"return\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🔙\" src=\"1f519.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},on:{keywords:[\"arrow\",\"words\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🔛\" src=\"1f51b.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},top:{keywords:[\"words\",\"blue-square\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🔝\" src=\"1f51d.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},soon:{keywords:[\"arrow\",\"words\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🔜\" src=\"1f51c.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},ballot_box_with_check:{keywords:[\"ok\",\"agree\",\"confirm\",\"black-square\",\"vote\",\"election\",\"yes\",\"tick\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"☑️\" src=\"2611.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},radio_button:{keywords:[\"input\",\"old\",\"music\",\"circle\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🔘\" src=\"1f518.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},white_circle:{keywords:[\"shape\",\"round\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"⚪\" src=\"26aa.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},black_circle:{keywords:[\"shape\",\"button\",\"round\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"⚫\" src=\"26ab.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},red_circle:{keywords:[\"shape\",\"error\",\"danger\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🔴\" src=\"1f534.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},large_blue_circle:{keywords:[\"shape\",\"icon\",\"button\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🔵\" src=\"1f535.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},small_orange_diamond:{keywords:[\"shape\",\"jewel\",\"gem\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🔸\" src=\"1f538.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},small_blue_diamond:{keywords:[\"shape\",\"jewel\",\"gem\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🔹\" src=\"1f539.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},large_orange_diamond:{keywords:[\"shape\",\"jewel\",\"gem\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🔶\" src=\"1f536.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},large_blue_diamond:{keywords:[\"shape\",\"jewel\",\"gem\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🔷\" src=\"1f537.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},small_red_triangle:{keywords:[\"shape\",\"direction\",\"up\",\"top\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🔺\" src=\"1f53a.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},black_small_square:{keywords:[\"shape\",\"icon\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"▪️\" src=\"25aa.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},white_small_square:{keywords:[\"shape\",\"icon\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"▫️\" src=\"25ab.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},black_large_square:{keywords:[\"shape\",\"icon\",\"button\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"⬛\" src=\"2b1b.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},white_large_square:{keywords:[\"shape\",\"icon\",\"stone\",\"button\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"⬜\" src=\"2b1c.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},small_red_triangle_down:{keywords:[\"shape\",\"direction\",\"bottom\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🔻\" src=\"1f53b.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},black_medium_square:{keywords:[\"shape\",\"button\",\"icon\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"◼️\" src=\"25fc.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},white_medium_square:{keywords:[\"shape\",\"stone\",\"icon\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"◻️\" src=\"25fb.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},black_medium_small_square:{keywords:[\"icon\",\"shape\",\"button\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"◾\" src=\"25fe.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},white_medium_small_square:{keywords:[\"shape\",\"stone\",\"icon\",\"button\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"◽\" src=\"25fd.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},black_square_button:{keywords:[\"shape\",\"input\",\"frame\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🔲\" src=\"1f532.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},white_square_button:{keywords:[\"shape\",\"input\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🔳\" src=\"1f533.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},speaker:{keywords:[\"sound\",\"volume\",\"silence\",\"broadcast\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🔈\" src=\"1f508.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},sound:{keywords:[\"volume\",\"speaker\",\"broadcast\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🔉\" src=\"1f509.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},loud_sound:{keywords:[\"volume\",\"noise\",\"noisy\",\"speaker\",\"broadcast\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🔊\" src=\"1f50a.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},mute:{keywords:[\"sound\",\"volume\",\"silence\",\"quiet\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🔇\" src=\"1f507.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},mega:{keywords:[\"sound\",\"speaker\",\"volume\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"📣\" src=\"1f4e3.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},loudspeaker:{keywords:[\"volume\",\"sound\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"📢\" src=\"1f4e2.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},bell:{keywords:[\"sound\",\"notification\",\"christmas\",\"xmas\",\"chime\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🔔\" src=\"1f514.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},no_bell:{keywords:[\"sound\",\"volume\",\"mute\",\"quiet\",\"silent\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🔕\" src=\"1f515.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},black_joker:{keywords:[\"poker\",\"cards\",\"game\",\"play\",\"magic\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🃏\" src=\"1f0cf.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},mahjong:{keywords:[\"game\",\"play\",\"chinese\",\"kanji\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🀄\" src=\"1f004.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},spades:{keywords:[\"poker\",\"cards\",\"suits\",\"magic\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"♠️\" src=\"2660.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},clubs:{keywords:[\"poker\",\"cards\",\"magic\",\"suits\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"♣️\" src=\"2663.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},hearts:{keywords:[\"poker\",\"cards\",\"magic\",\"suits\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"♥️\" src=\"2665.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},diamonds:{keywords:[\"poker\",\"cards\",\"magic\",\"suits\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"♦️\" src=\"2666.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},flower_playing_cards:{keywords:[\"game\",\"sunset\",\"red\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🎴\" src=\"1f3b4.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},thought_balloon:{keywords:[\"bubble\",\"cloud\",\"speech\",\"thinking\",\"dream\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"💭\" src=\"1f4ad.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},right_anger_bubble:{keywords:[\"caption\",\"speech\",\"thinking\",\"mad\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🗯\" src=\"1f5ef.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},speech_balloon:{keywords:[\"bubble\",\"words\",\"message\",\"talk\",\"chatting\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"💬\" src=\"1f4ac.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},left_speech_bubble:{keywords:[\"words\",\"message\",\"talk\",\"chatting\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🗨\" src=\"1f5e8.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},clock1:{keywords:[\"time\",\"late\",\"early\",\"schedule\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🕐\" src=\"1f550.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},clock2:{keywords:[\"time\",\"late\",\"early\",\"schedule\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🕑\" src=\"1f551.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},clock3:{keywords:[\"time\",\"late\",\"early\",\"schedule\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🕒\" src=\"1f552.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},clock4:{keywords:[\"time\",\"late\",\"early\",\"schedule\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🕓\" src=\"1f553.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},clock5:{keywords:[\"time\",\"late\",\"early\",\"schedule\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🕔\" src=\"1f554.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},clock6:{keywords:[\"time\",\"late\",\"early\",\"schedule\",\"dawn\",\"dusk\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🕕\" src=\"1f555.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},clock7:{keywords:[\"time\",\"late\",\"early\",\"schedule\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🕖\" src=\"1f556.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},clock8:{keywords:[\"time\",\"late\",\"early\",\"schedule\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🕗\" src=\"1f557.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},clock9:{keywords:[\"time\",\"late\",\"early\",\"schedule\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🕘\" src=\"1f558.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},clock10:{keywords:[\"time\",\"late\",\"early\",\"schedule\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🕙\" src=\"1f559.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},clock11:{keywords:[\"time\",\"late\",\"early\",\"schedule\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🕚\" src=\"1f55a.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},clock12:{keywords:[\"time\",\"noon\",\"midnight\",\"midday\",\"late\",\"early\",\"schedule\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🕛\" src=\"1f55b.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},clock130:{keywords:[\"time\",\"late\",\"early\",\"schedule\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🕜\" src=\"1f55c.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},clock230:{keywords:[\"time\",\"late\",\"early\",\"schedule\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🕝\" src=\"1f55d.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},clock330:{keywords:[\"time\",\"late\",\"early\",\"schedule\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🕞\" src=\"1f55e.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},clock430:{keywords:[\"time\",\"late\",\"early\",\"schedule\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🕟\" src=\"1f55f.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},clock530:{keywords:[\"time\",\"late\",\"early\",\"schedule\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🕠\" src=\"1f560.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},clock630:{keywords:[\"time\",\"late\",\"early\",\"schedule\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🕡\" src=\"1f561.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},clock730:{keywords:[\"time\",\"late\",\"early\",\"schedule\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🕢\" src=\"1f562.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},clock830:{keywords:[\"time\",\"late\",\"early\",\"schedule\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🕣\" src=\"1f563.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},clock930:{keywords:[\"time\",\"late\",\"early\",\"schedule\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🕤\" src=\"1f564.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},clock1030:{keywords:[\"time\",\"late\",\"early\",\"schedule\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🕥\" src=\"1f565.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},clock1130:{keywords:[\"time\",\"late\",\"early\",\"schedule\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🕦\" src=\"1f566.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},clock1230:{keywords:[\"time\",\"late\",\"early\",\"schedule\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🕧\" src=\"1f567.png\"/>',fitzpatrick_scale:false,category:\"symbols\"},afghanistan:{keywords:[\"af\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇦🇫\" src=\"1f1e6-1f1eb.png\"/>',fitzpatrick_scale:false,category:\"flags\"},aland_islands:{keywords:[\"Åland\",\"islands\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇦🇽\" src=\"1f1e6-1f1fd.png\"/>',fitzpatrick_scale:false,category:\"flags\"},albania:{keywords:[\"al\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇦🇱\" src=\"1f1e6-1f1f1.png\"/>',fitzpatrick_scale:false,category:\"flags\"},algeria:{keywords:[\"dz\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇩🇿\" src=\"1f1e9-1f1ff.png\"/>',fitzpatrick_scale:false,category:\"flags\"},american_samoa:{keywords:[\"american\",\"ws\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇦🇸\" src=\"1f1e6-1f1f8.png\"/>',fitzpatrick_scale:false,category:\"flags\"},andorra:{keywords:[\"ad\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇦🇩\" src=\"1f1e6-1f1e9.png\"/>',fitzpatrick_scale:false,category:\"flags\"},angola:{keywords:[\"ao\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇦🇴\" src=\"1f1e6-1f1f4.png\"/>',fitzpatrick_scale:false,category:\"flags\"},anguilla:{keywords:[\"ai\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇦🇮\" src=\"1f1e6-1f1ee.png\"/>',fitzpatrick_scale:false,category:\"flags\"},antarctica:{keywords:[\"aq\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇦🇶\" src=\"1f1e6-1f1f6.png\"/>',fitzpatrick_scale:false,category:\"flags\"},antigua_barbuda:{keywords:[\"antigua\",\"barbuda\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇦🇬\" src=\"1f1e6-1f1ec.png\"/>',fitzpatrick_scale:false,category:\"flags\"},argentina:{keywords:[\"ar\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇦🇷\" src=\"1f1e6-1f1f7.png\"/>',fitzpatrick_scale:false,category:\"flags\"},armenia:{keywords:[\"am\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇦🇲\" src=\"1f1e6-1f1f2.png\"/>',fitzpatrick_scale:false,category:\"flags\"},aruba:{keywords:[\"aw\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇦🇼\" src=\"1f1e6-1f1fc.png\"/>',fitzpatrick_scale:false,category:\"flags\"},australia:{keywords:[\"au\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇦🇺\" src=\"1f1e6-1f1fa.png\"/>',fitzpatrick_scale:false,category:\"flags\"},austria:{keywords:[\"at\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇦🇹\" src=\"1f1e6-1f1f9.png\"/>',fitzpatrick_scale:false,category:\"flags\"},azerbaijan:{keywords:[\"az\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇦🇿\" src=\"1f1e6-1f1ff.png\"/>',fitzpatrick_scale:false,category:\"flags\"},bahamas:{keywords:[\"bs\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇧🇸\" src=\"1f1e7-1f1f8.png\"/>',fitzpatrick_scale:false,category:\"flags\"},bahrain:{keywords:[\"bh\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇧🇭\" src=\"1f1e7-1f1ed.png\"/>',fitzpatrick_scale:false,category:\"flags\"},bangladesh:{keywords:[\"bd\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇧🇩\" src=\"1f1e7-1f1e9.png\"/>',fitzpatrick_scale:false,category:\"flags\"},barbados:{keywords:[\"bb\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇧🇧\" src=\"1f1e7-1f1e7.png\"/>',fitzpatrick_scale:false,category:\"flags\"},belarus:{keywords:[\"by\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇧🇾\" src=\"1f1e7-1f1fe.png\"/>',fitzpatrick_scale:false,category:\"flags\"},belgium:{keywords:[\"be\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇧🇪\" src=\"1f1e7-1f1ea.png\"/>',fitzpatrick_scale:false,category:\"flags\"},belize:{keywords:[\"bz\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇧🇿\" src=\"1f1e7-1f1ff.png\"/>',fitzpatrick_scale:false,category:\"flags\"},benin:{keywords:[\"bj\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇧🇯\" src=\"1f1e7-1f1ef.png\"/>',fitzpatrick_scale:false,category:\"flags\"},bermuda:{keywords:[\"bm\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇧🇲\" src=\"1f1e7-1f1f2.png\"/>',fitzpatrick_scale:false,category:\"flags\"},bhutan:{keywords:[\"bt\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇧🇹\" src=\"1f1e7-1f1f9.png\"/>',fitzpatrick_scale:false,category:\"flags\"},bolivia:{keywords:[\"bo\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇧🇴\" src=\"1f1e7-1f1f4.png\"/>',fitzpatrick_scale:false,category:\"flags\"},caribbean_netherlands:{keywords:[\"bonaire\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇧🇶\" src=\"1f1e7-1f1f6.png\"/>',fitzpatrick_scale:false,category:\"flags\"},bosnia_herzegovina:{keywords:[\"bosnia\",\"herzegovina\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇧🇦\" src=\"1f1e7-1f1e6.png\"/>',fitzpatrick_scale:false,category:\"flags\"},botswana:{keywords:[\"bw\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇧🇼\" src=\"1f1e7-1f1fc.png\"/>',fitzpatrick_scale:false,category:\"flags\"},brazil:{keywords:[\"br\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇧🇷\" src=\"1f1e7-1f1f7.png\"/>',fitzpatrick_scale:false,category:\"flags\"},british_indian_ocean_territory:{keywords:[\"british\",\"indian\",\"ocean\",\"territory\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇮🇴\" src=\"1f1ee-1f1f4.png\"/>',fitzpatrick_scale:false,category:\"flags\"},british_virgin_islands:{keywords:[\"british\",\"virgin\",\"islands\",\"bvi\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇻🇬\" src=\"1f1fb-1f1ec.png\"/>',fitzpatrick_scale:false,category:\"flags\"},brunei:{keywords:[\"bn\",\"darussalam\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇧🇳\" src=\"1f1e7-1f1f3.png\"/>',fitzpatrick_scale:false,category:\"flags\"},bulgaria:{keywords:[\"bg\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇧🇬\" src=\"1f1e7-1f1ec.png\"/>',fitzpatrick_scale:false,category:\"flags\"},burkina_faso:{keywords:[\"burkina\",\"faso\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇧🇫\" src=\"1f1e7-1f1eb.png\"/>',fitzpatrick_scale:false,category:\"flags\"},burundi:{keywords:[\"bi\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇧🇮\" src=\"1f1e7-1f1ee.png\"/>',fitzpatrick_scale:false,category:\"flags\"},cape_verde:{keywords:[\"cabo\",\"verde\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇨🇻\" src=\"1f1e8-1f1fb.png\"/>',fitzpatrick_scale:false,category:\"flags\"},cambodia:{keywords:[\"kh\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇰🇭\" src=\"1f1f0-1f1ed.png\"/>',fitzpatrick_scale:false,category:\"flags\"},cameroon:{keywords:[\"cm\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇨🇲\" src=\"1f1e8-1f1f2.png\"/>',fitzpatrick_scale:false,category:\"flags\"},canada:{keywords:[\"ca\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇨🇦\" src=\"1f1e8-1f1e6.png\"/>',fitzpatrick_scale:false,category:\"flags\"},canary_islands:{keywords:[\"canary\",\"islands\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇮🇨\" src=\"1f1ee-1f1e8.png\"/>',fitzpatrick_scale:false,category:\"flags\"},cayman_islands:{keywords:[\"cayman\",\"islands\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇰🇾\" src=\"1f1f0-1f1fe.png\"/>',fitzpatrick_scale:false,category:\"flags\"},central_african_republic:{keywords:[\"central\",\"african\",\"republic\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇨🇫\" src=\"1f1e8-1f1eb.png\"/>',fitzpatrick_scale:false,category:\"flags\"},chad:{keywords:[\"td\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇹🇩\" src=\"1f1f9-1f1e9.png\"/>',fitzpatrick_scale:false,category:\"flags\"},chile:{keywords:[\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇨🇱\" src=\"1f1e8-1f1f1.png\"/>',fitzpatrick_scale:false,category:\"flags\"},cn:{keywords:[\"china\",\"chinese\",\"prc\",\"flag\",\"country\",\"nation\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇨🇳\" src=\"1f1e8-1f1f3.png\"/>',fitzpatrick_scale:false,category:\"flags\"},christmas_island:{keywords:[\"christmas\",\"island\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇨🇽\" src=\"1f1e8-1f1fd.png\"/>',fitzpatrick_scale:false,category:\"flags\"},cocos_islands:{keywords:[\"cocos\",\"keeling\",\"islands\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇨🇨\" src=\"1f1e8-1f1e8.png\"/>',fitzpatrick_scale:false,category:\"flags\"},colombia:{keywords:[\"co\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇨🇴\" src=\"1f1e8-1f1f4.png\"/>',fitzpatrick_scale:false,category:\"flags\"},comoros:{keywords:[\"km\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇰🇲\" src=\"1f1f0-1f1f2.png\"/>',fitzpatrick_scale:false,category:\"flags\"},congo_brazzaville:{keywords:[\"congo\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇨🇬\" src=\"1f1e8-1f1ec.png\"/>',fitzpatrick_scale:false,category:\"flags\"},congo_kinshasa:{keywords:[\"congo\",\"democratic\",\"republic\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇨🇩\" src=\"1f1e8-1f1e9.png\"/>',fitzpatrick_scale:false,category:\"flags\"},cook_islands:{keywords:[\"cook\",\"islands\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇨🇰\" src=\"1f1e8-1f1f0.png\"/>',fitzpatrick_scale:false,category:\"flags\"},costa_rica:{keywords:[\"costa\",\"rica\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇨🇷\" src=\"1f1e8-1f1f7.png\"/>',fitzpatrick_scale:false,category:\"flags\"},croatia:{keywords:[\"hr\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇭🇷\" src=\"1f1ed-1f1f7.png\"/>',fitzpatrick_scale:false,category:\"flags\"},cuba:{keywords:[\"cu\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇨🇺\" src=\"1f1e8-1f1fa.png\"/>',fitzpatrick_scale:false,category:\"flags\"},curacao:{keywords:[\"curaçao\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇨🇼\" src=\"1f1e8-1f1fc.png\"/>',fitzpatrick_scale:false,category:\"flags\"},cyprus:{keywords:[\"cy\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇨🇾\" src=\"1f1e8-1f1fe.png\"/>',fitzpatrick_scale:false,category:\"flags\"},czech_republic:{keywords:[\"cz\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇨🇿\" src=\"1f1e8-1f1ff.png\"/>',fitzpatrick_scale:false,category:\"flags\"},denmark:{keywords:[\"dk\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇩🇰\" src=\"1f1e9-1f1f0.png\"/>',fitzpatrick_scale:false,category:\"flags\"},djibouti:{keywords:[\"dj\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇩🇯\" src=\"1f1e9-1f1ef.png\"/>',fitzpatrick_scale:false,category:\"flags\"},dominica:{keywords:[\"dm\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇩🇲\" src=\"1f1e9-1f1f2.png\"/>',fitzpatrick_scale:false,category:\"flags\"},dominican_republic:{keywords:[\"dominican\",\"republic\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇩🇴\" src=\"1f1e9-1f1f4.png\"/>',fitzpatrick_scale:false,category:\"flags\"},ecuador:{keywords:[\"ec\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇪🇨\" src=\"1f1ea-1f1e8.png\"/>',fitzpatrick_scale:false,category:\"flags\"},egypt:{keywords:[\"eg\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇪🇬\" src=\"1f1ea-1f1ec.png\"/>',fitzpatrick_scale:false,category:\"flags\"},el_salvador:{keywords:[\"el\",\"salvador\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇸🇻\" src=\"1f1f8-1f1fb.png\"/>',fitzpatrick_scale:false,category:\"flags\"},equatorial_guinea:{keywords:[\"equatorial\",\"gn\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇬🇶\" src=\"1f1ec-1f1f6.png\"/>',fitzpatrick_scale:false,category:\"flags\"},eritrea:{keywords:[\"er\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇪🇷\" src=\"1f1ea-1f1f7.png\"/>',fitzpatrick_scale:false,category:\"flags\"},estonia:{keywords:[\"ee\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇪🇪\" src=\"1f1ea-1f1ea.png\"/>',fitzpatrick_scale:false,category:\"flags\"},ethiopia:{keywords:[\"et\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇪🇹\" src=\"1f1ea-1f1f9.png\"/>',fitzpatrick_scale:false,category:\"flags\"},eu:{keywords:[\"european\",\"union\",\"flag\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇪🇺\" src=\"1f1ea-1f1fa.png\"/>',fitzpatrick_scale:false,category:\"flags\"},falkland_islands:{keywords:[\"falkland\",\"islands\",\"malvinas\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇫🇰\" src=\"1f1eb-1f1f0.png\"/>',fitzpatrick_scale:false,category:\"flags\"},faroe_islands:{keywords:[\"faroe\",\"islands\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇫🇴\" src=\"1f1eb-1f1f4.png\"/>',fitzpatrick_scale:false,category:\"flags\"},fiji:{keywords:[\"fj\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇫🇯\" src=\"1f1eb-1f1ef.png\"/>',fitzpatrick_scale:false,category:\"flags\"},finland:{keywords:[\"fi\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇫🇮\" src=\"1f1eb-1f1ee.png\"/>',fitzpatrick_scale:false,category:\"flags\"},fr:{keywords:[\"banner\",\"flag\",\"nation\",\"france\",\"french\",\"country\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇫🇷\" src=\"1f1eb-1f1f7.png\"/>',fitzpatrick_scale:false,category:\"flags\"},french_guiana:{keywords:[\"french\",\"guiana\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇬🇫\" src=\"1f1ec-1f1eb.png\"/>',fitzpatrick_scale:false,category:\"flags\"},french_polynesia:{keywords:[\"french\",\"polynesia\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇵🇫\" src=\"1f1f5-1f1eb.png\"/>',fitzpatrick_scale:false,category:\"flags\"},french_southern_territories:{keywords:[\"french\",\"southern\",\"territories\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇹🇫\" src=\"1f1f9-1f1eb.png\"/>',fitzpatrick_scale:false,category:\"flags\"},gabon:{keywords:[\"ga\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇬🇦\" src=\"1f1ec-1f1e6.png\"/>',fitzpatrick_scale:false,category:\"flags\"},gambia:{keywords:[\"gm\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇬🇲\" src=\"1f1ec-1f1f2.png\"/>',fitzpatrick_scale:false,category:\"flags\"},georgia:{keywords:[\"ge\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇬🇪\" src=\"1f1ec-1f1ea.png\"/>',fitzpatrick_scale:false,category:\"flags\"},de:{keywords:[\"german\",\"nation\",\"flag\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇩🇪\" src=\"1f1e9-1f1ea.png\"/>',fitzpatrick_scale:false,category:\"flags\"},ghana:{keywords:[\"gh\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇬🇭\" src=\"1f1ec-1f1ed.png\"/>',fitzpatrick_scale:false,category:\"flags\"},gibraltar:{keywords:[\"gi\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇬🇮\" src=\"1f1ec-1f1ee.png\"/>',fitzpatrick_scale:false,category:\"flags\"},greece:{keywords:[\"gr\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇬🇷\" src=\"1f1ec-1f1f7.png\"/>',fitzpatrick_scale:false,category:\"flags\"},greenland:{keywords:[\"gl\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇬🇱\" src=\"1f1ec-1f1f1.png\"/>',fitzpatrick_scale:false,category:\"flags\"},grenada:{keywords:[\"gd\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇬🇩\" src=\"1f1ec-1f1e9.png\"/>',fitzpatrick_scale:false,category:\"flags\"},guadeloupe:{keywords:[\"gp\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇬🇵\" src=\"1f1ec-1f1f5.png\"/>',fitzpatrick_scale:false,category:\"flags\"},guam:{keywords:[\"gu\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇬🇺\" src=\"1f1ec-1f1fa.png\"/>',fitzpatrick_scale:false,category:\"flags\"},guatemala:{keywords:[\"gt\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇬🇹\" src=\"1f1ec-1f1f9.png\"/>',fitzpatrick_scale:false,category:\"flags\"},guernsey:{keywords:[\"gg\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇬🇬\" src=\"1f1ec-1f1ec.png\"/>',fitzpatrick_scale:false,category:\"flags\"},guinea:{keywords:[\"gn\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇬🇳\" src=\"1f1ec-1f1f3.png\"/>',fitzpatrick_scale:false,category:\"flags\"},guinea_bissau:{keywords:[\"gw\",\"bissau\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇬🇼\" src=\"1f1ec-1f1fc.png\"/>',fitzpatrick_scale:false,category:\"flags\"},guyana:{keywords:[\"gy\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇬🇾\" src=\"1f1ec-1f1fe.png\"/>',fitzpatrick_scale:false,category:\"flags\"},haiti:{keywords:[\"ht\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇭🇹\" src=\"1f1ed-1f1f9.png\"/>',fitzpatrick_scale:false,category:\"flags\"},honduras:{keywords:[\"hn\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇭🇳\" src=\"1f1ed-1f1f3.png\"/>',fitzpatrick_scale:false,category:\"flags\"},hong_kong:{keywords:[\"hong\",\"kong\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇭🇰\" src=\"1f1ed-1f1f0.png\"/>',fitzpatrick_scale:false,category:\"flags\"},hungary:{keywords:[\"hu\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇭🇺\" src=\"1f1ed-1f1fa.png\"/>',fitzpatrick_scale:false,category:\"flags\"},iceland:{keywords:[\"is\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇮🇸\" src=\"1f1ee-1f1f8.png\"/>',fitzpatrick_scale:false,category:\"flags\"},india:{keywords:[\"in\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇮🇳\" src=\"1f1ee-1f1f3.png\"/>',fitzpatrick_scale:false,category:\"flags\"},indonesia:{keywords:[\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇮🇩\" src=\"1f1ee-1f1e9.png\"/>',fitzpatrick_scale:false,category:\"flags\"},iran:{keywords:[\"iran,\",\"islamic\",\"republic\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇮🇷\" src=\"1f1ee-1f1f7.png\"/>',fitzpatrick_scale:false,category:\"flags\"},iraq:{keywords:[\"iq\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇮🇶\" src=\"1f1ee-1f1f6.png\"/>',fitzpatrick_scale:false,category:\"flags\"},ireland:{keywords:[\"ie\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇮🇪\" src=\"1f1ee-1f1ea.png\"/>',fitzpatrick_scale:false,category:\"flags\"},isle_of_man:{keywords:[\"isle\",\"man\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇮🇲\" src=\"1f1ee-1f1f2.png\"/>',fitzpatrick_scale:false,category:\"flags\"},israel:{keywords:[\"il\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇮🇱\" src=\"1f1ee-1f1f1.png\"/>',fitzpatrick_scale:false,category:\"flags\"},it:{keywords:[\"italy\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇮🇹\" src=\"1f1ee-1f1f9.png\"/>',fitzpatrick_scale:false,category:\"flags\"},cote_divoire:{keywords:[\"ivory\",\"coast\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇨🇮\" src=\"1f1e8-1f1ee.png\"/>',fitzpatrick_scale:false,category:\"flags\"},jamaica:{keywords:[\"jm\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇯🇲\" src=\"1f1ef-1f1f2.png\"/>',fitzpatrick_scale:false,category:\"flags\"},jp:{keywords:[\"japanese\",\"nation\",\"flag\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇯🇵\" src=\"1f1ef-1f1f5.png\"/>',fitzpatrick_scale:false,category:\"flags\"},jersey:{keywords:[\"je\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇯🇪\" src=\"1f1ef-1f1ea.png\"/>',fitzpatrick_scale:false,category:\"flags\"},jordan:{keywords:[\"jo\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇯🇴\" src=\"1f1ef-1f1f4.png\"/>',fitzpatrick_scale:false,category:\"flags\"},kazakhstan:{keywords:[\"kz\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇰🇿\" src=\"1f1f0-1f1ff.png\"/>',fitzpatrick_scale:false,category:\"flags\"},kenya:{keywords:[\"ke\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇰🇪\" src=\"1f1f0-1f1ea.png\"/>',fitzpatrick_scale:false,category:\"flags\"},kiribati:{keywords:[\"ki\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇰🇮\" src=\"1f1f0-1f1ee.png\"/>',fitzpatrick_scale:false,category:\"flags\"},kosovo:{keywords:[\"xk\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇽🇰\" src=\"1f1fd-1f1f0.png\"/>',fitzpatrick_scale:false,category:\"flags\"},kuwait:{keywords:[\"kw\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇰🇼\" src=\"1f1f0-1f1fc.png\"/>',fitzpatrick_scale:false,category:\"flags\"},kyrgyzstan:{keywords:[\"kg\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇰🇬\" src=\"1f1f0-1f1ec.png\"/>',fitzpatrick_scale:false,category:\"flags\"},laos:{keywords:[\"lao\",\"democratic\",\"republic\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇱🇦\" src=\"1f1f1-1f1e6.png\"/>',fitzpatrick_scale:false,category:\"flags\"},latvia:{keywords:[\"lv\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇱🇻\" src=\"1f1f1-1f1fb.png\"/>',fitzpatrick_scale:false,category:\"flags\"},lebanon:{keywords:[\"lb\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇱🇧\" src=\"1f1f1-1f1e7.png\"/>',fitzpatrick_scale:false,category:\"flags\"},lesotho:{keywords:[\"ls\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇱🇸\" src=\"1f1f1-1f1f8.png\"/>',fitzpatrick_scale:false,category:\"flags\"},liberia:{keywords:[\"lr\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇱🇷\" src=\"1f1f1-1f1f7.png\"/>',fitzpatrick_scale:false,category:\"flags\"},libya:{keywords:[\"ly\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇱🇾\" src=\"1f1f1-1f1fe.png\"/>',fitzpatrick_scale:false,category:\"flags\"},liechtenstein:{keywords:[\"li\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇱🇮\" src=\"1f1f1-1f1ee.png\"/>',fitzpatrick_scale:false,category:\"flags\"},lithuania:{keywords:[\"lt\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇱🇹\" src=\"1f1f1-1f1f9.png\"/>',fitzpatrick_scale:false,category:\"flags\"},luxembourg:{keywords:[\"lu\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇱🇺\" src=\"1f1f1-1f1fa.png\"/>',fitzpatrick_scale:false,category:\"flags\"},macau:{keywords:[\"macao\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇲🇴\" src=\"1f1f2-1f1f4.png\"/>',fitzpatrick_scale:false,category:\"flags\"},macedonia:{keywords:[\"macedonia,\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇲🇰\" src=\"1f1f2-1f1f0.png\"/>',fitzpatrick_scale:false,category:\"flags\"},madagascar:{keywords:[\"mg\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇲🇬\" src=\"1f1f2-1f1ec.png\"/>',fitzpatrick_scale:false,category:\"flags\"},malawi:{keywords:[\"mw\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇲🇼\" src=\"1f1f2-1f1fc.png\"/>',fitzpatrick_scale:false,category:\"flags\"},malaysia:{keywords:[\"my\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇲🇾\" src=\"1f1f2-1f1fe.png\"/>',fitzpatrick_scale:false,category:\"flags\"},maldives:{keywords:[\"mv\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇲🇻\" src=\"1f1f2-1f1fb.png\"/>',fitzpatrick_scale:false,category:\"flags\"},mali:{keywords:[\"ml\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇲🇱\" src=\"1f1f2-1f1f1.png\"/>',fitzpatrick_scale:false,category:\"flags\"},malta:{keywords:[\"mt\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇲🇹\" src=\"1f1f2-1f1f9.png\"/>',fitzpatrick_scale:false,category:\"flags\"},marshall_islands:{keywords:[\"marshall\",\"islands\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇲🇭\" src=\"1f1f2-1f1ed.png\"/>',fitzpatrick_scale:false,category:\"flags\"},martinique:{keywords:[\"mq\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇲🇶\" src=\"1f1f2-1f1f6.png\"/>',fitzpatrick_scale:false,category:\"flags\"},mauritania:{keywords:[\"mr\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇲🇷\" src=\"1f1f2-1f1f7.png\"/>',fitzpatrick_scale:false,category:\"flags\"},mauritius:{keywords:[\"mu\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇲🇺\" src=\"1f1f2-1f1fa.png\"/>',fitzpatrick_scale:false,category:\"flags\"},mayotte:{keywords:[\"yt\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇾🇹\" src=\"1f1fe-1f1f9.png\"/>',fitzpatrick_scale:false,category:\"flags\"},mexico:{keywords:[\"mx\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇲🇽\" src=\"1f1f2-1f1fd.png\"/>',fitzpatrick_scale:false,category:\"flags\"},micronesia:{keywords:[\"micronesia,\",\"federated\",\"states\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇫🇲\" src=\"1f1eb-1f1f2.png\"/>',fitzpatrick_scale:false,category:\"flags\"},moldova:{keywords:[\"moldova,\",\"republic\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇲🇩\" src=\"1f1f2-1f1e9.png\"/>',fitzpatrick_scale:false,category:\"flags\"},monaco:{keywords:[\"mc\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇲🇨\" src=\"1f1f2-1f1e8.png\"/>',fitzpatrick_scale:false,category:\"flags\"},mongolia:{keywords:[\"mn\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇲🇳\" src=\"1f1f2-1f1f3.png\"/>',fitzpatrick_scale:false,category:\"flags\"},montenegro:{keywords:[\"me\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇲🇪\" src=\"1f1f2-1f1ea.png\"/>',fitzpatrick_scale:false,category:\"flags\"},montserrat:{keywords:[\"ms\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇲🇸\" src=\"1f1f2-1f1f8.png\"/>',fitzpatrick_scale:false,category:\"flags\"},morocco:{keywords:[\"ma\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇲🇦\" src=\"1f1f2-1f1e6.png\"/>',fitzpatrick_scale:false,category:\"flags\"},mozambique:{keywords:[\"mz\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇲🇿\" src=\"1f1f2-1f1ff.png\"/>',fitzpatrick_scale:false,category:\"flags\"},myanmar:{keywords:[\"mm\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇲🇲\" src=\"1f1f2-1f1f2.png\"/>',fitzpatrick_scale:false,category:\"flags\"},namibia:{keywords:[\"na\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇳🇦\" src=\"1f1f3-1f1e6.png\"/>',fitzpatrick_scale:false,category:\"flags\"},nauru:{keywords:[\"nr\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇳🇷\" src=\"1f1f3-1f1f7.png\"/>',fitzpatrick_scale:false,category:\"flags\"},nepal:{keywords:[\"np\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇳🇵\" src=\"1f1f3-1f1f5.png\"/>',fitzpatrick_scale:false,category:\"flags\"},netherlands:{keywords:[\"nl\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇳🇱\" src=\"1f1f3-1f1f1.png\"/>',fitzpatrick_scale:false,category:\"flags\"},new_caledonia:{keywords:[\"new\",\"caledonia\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇳🇨\" src=\"1f1f3-1f1e8.png\"/>',fitzpatrick_scale:false,category:\"flags\"},new_zealand:{keywords:[\"new\",\"zealand\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇳🇿\" src=\"1f1f3-1f1ff.png\"/>',fitzpatrick_scale:false,category:\"flags\"},nicaragua:{keywords:[\"ni\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇳🇮\" src=\"1f1f3-1f1ee.png\"/>',fitzpatrick_scale:false,category:\"flags\"},niger:{keywords:[\"ne\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇳🇪\" src=\"1f1f3-1f1ea.png\"/>',fitzpatrick_scale:false,category:\"flags\"},nigeria:{keywords:[\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇳🇬\" src=\"1f1f3-1f1ec.png\"/>',fitzpatrick_scale:false,category:\"flags\"},niue:{keywords:[\"nu\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇳🇺\" src=\"1f1f3-1f1fa.png\"/>',fitzpatrick_scale:false,category:\"flags\"},norfolk_island:{keywords:[\"norfolk\",\"island\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇳🇫\" src=\"1f1f3-1f1eb.png\"/>',fitzpatrick_scale:false,category:\"flags\"},northern_mariana_islands:{keywords:[\"northern\",\"mariana\",\"islands\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇲🇵\" src=\"1f1f2-1f1f5.png\"/>',fitzpatrick_scale:false,category:\"flags\"},north_korea:{keywords:[\"north\",\"korea\",\"nation\",\"flag\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇰🇵\" src=\"1f1f0-1f1f5.png\"/>',fitzpatrick_scale:false,category:\"flags\"},norway:{keywords:[\"no\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇳🇴\" src=\"1f1f3-1f1f4.png\"/>',fitzpatrick_scale:false,category:\"flags\"},oman:{keywords:[\"om_symbol\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇴🇲\" src=\"1f1f4-1f1f2.png\"/>',fitzpatrick_scale:false,category:\"flags\"},pakistan:{keywords:[\"pk\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇵🇰\" src=\"1f1f5-1f1f0.png\"/>',fitzpatrick_scale:false,category:\"flags\"},palau:{keywords:[\"pw\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇵🇼\" src=\"1f1f5-1f1fc.png\"/>',fitzpatrick_scale:false,category:\"flags\"},palestinian_territories:{keywords:[\"palestine\",\"palestinian\",\"territories\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇵🇸\" src=\"1f1f5-1f1f8.png\"/>',fitzpatrick_scale:false,category:\"flags\"},panama:{keywords:[\"pa\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇵🇦\" src=\"1f1f5-1f1e6.png\"/>',fitzpatrick_scale:false,category:\"flags\"},papua_new_guinea:{keywords:[\"papua\",\"new\",\"guinea\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇵🇬\" src=\"1f1f5-1f1ec.png\"/>',fitzpatrick_scale:false,category:\"flags\"},paraguay:{keywords:[\"py\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇵🇾\" src=\"1f1f5-1f1fe.png\"/>',fitzpatrick_scale:false,category:\"flags\"},peru:{keywords:[\"pe\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇵🇪\" src=\"1f1f5-1f1ea.png\"/>',fitzpatrick_scale:false,category:\"flags\"},philippines:{keywords:[\"ph\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇵🇭\" src=\"1f1f5-1f1ed.png\"/>',fitzpatrick_scale:false,category:\"flags\"},pitcairn_islands:{keywords:[\"pitcairn\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇵🇳\" src=\"1f1f5-1f1f3.png\"/>',fitzpatrick_scale:false,category:\"flags\"},poland:{keywords:[\"pl\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇵🇱\" src=\"1f1f5-1f1f1.png\"/>',fitzpatrick_scale:false,category:\"flags\"},portugal:{keywords:[\"pt\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇵🇹\" src=\"1f1f5-1f1f9.png\"/>',fitzpatrick_scale:false,category:\"flags\"},puerto_rico:{keywords:[\"puerto\",\"rico\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇵🇷\" src=\"1f1f5-1f1f7.png\"/>',fitzpatrick_scale:false,category:\"flags\"},qatar:{keywords:[\"qa\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇶🇦\" src=\"1f1f6-1f1e6.png\"/>',fitzpatrick_scale:false,category:\"flags\"},reunion:{keywords:[\"réunion\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇷🇪\" src=\"1f1f7-1f1ea.png\"/>',fitzpatrick_scale:false,category:\"flags\"},romania:{keywords:[\"ro\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇷🇴\" src=\"1f1f7-1f1f4.png\"/>',fitzpatrick_scale:false,category:\"flags\"},ru:{keywords:[\"russian\",\"federation\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇷🇺\" src=\"1f1f7-1f1fa.png\"/>',fitzpatrick_scale:false,category:\"flags\"},rwanda:{keywords:[\"rw\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇷🇼\" src=\"1f1f7-1f1fc.png\"/>',fitzpatrick_scale:false,category:\"flags\"},st_barthelemy:{keywords:[\"saint\",\"barthélemy\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇧🇱\" src=\"1f1e7-1f1f1.png\"/>',fitzpatrick_scale:false,category:\"flags\"},st_helena:{keywords:[\"saint\",\"helena\",\"ascension\",\"tristan\",\"cunha\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇸🇭\" src=\"1f1f8-1f1ed.png\"/>',fitzpatrick_scale:false,category:\"flags\"},st_kitts_nevis:{keywords:[\"saint\",\"kitts\",\"nevis\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇰🇳\" src=\"1f1f0-1f1f3.png\"/>',fitzpatrick_scale:false,category:\"flags\"},st_lucia:{keywords:[\"saint\",\"lucia\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇱🇨\" src=\"1f1f1-1f1e8.png\"/>',fitzpatrick_scale:false,category:\"flags\"},st_pierre_miquelon:{keywords:[\"saint\",\"pierre\",\"miquelon\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇵🇲\" src=\"1f1f5-1f1f2.png\"/>',fitzpatrick_scale:false,category:\"flags\"},st_vincent_grenadines:{keywords:[\"saint\",\"vincent\",\"grenadines\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇻🇨\" src=\"1f1fb-1f1e8.png\"/>',fitzpatrick_scale:false,category:\"flags\"},samoa:{keywords:[\"ws\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇼🇸\" src=\"1f1fc-1f1f8.png\"/>',fitzpatrick_scale:false,category:\"flags\"},san_marino:{keywords:[\"san\",\"marino\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇸🇲\" src=\"1f1f8-1f1f2.png\"/>',fitzpatrick_scale:false,category:\"flags\"},sao_tome_principe:{keywords:[\"sao\",\"tome\",\"principe\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇸🇹\" src=\"1f1f8-1f1f9.png\"/>',fitzpatrick_scale:false,category:\"flags\"},saudi_arabia:{keywords:[\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇸🇦\" src=\"1f1f8-1f1e6.png\"/>',fitzpatrick_scale:false,category:\"flags\"},senegal:{keywords:[\"sn\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇸🇳\" src=\"1f1f8-1f1f3.png\"/>',fitzpatrick_scale:false,category:\"flags\"},serbia:{keywords:[\"rs\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇷🇸\" src=\"1f1f7-1f1f8.png\"/>',fitzpatrick_scale:false,category:\"flags\"},seychelles:{keywords:[\"sc\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇸🇨\" src=\"1f1f8-1f1e8.png\"/>',fitzpatrick_scale:false,category:\"flags\"},sierra_leone:{keywords:[\"sierra\",\"leone\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇸🇱\" src=\"1f1f8-1f1f1.png\"/>',fitzpatrick_scale:false,category:\"flags\"},singapore:{keywords:[\"sg\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇸🇬\" src=\"1f1f8-1f1ec.png\"/>',fitzpatrick_scale:false,category:\"flags\"},sint_maarten:{keywords:[\"sint\",\"maarten\",\"dutch\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇸🇽\" src=\"1f1f8-1f1fd.png\"/>',fitzpatrick_scale:false,category:\"flags\"},slovakia:{keywords:[\"sk\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇸🇰\" src=\"1f1f8-1f1f0.png\"/>',fitzpatrick_scale:false,category:\"flags\"},slovenia:{keywords:[\"si\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇸🇮\" src=\"1f1f8-1f1ee.png\"/>',fitzpatrick_scale:false,category:\"flags\"},solomon_islands:{keywords:[\"solomon\",\"islands\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇸🇧\" src=\"1f1f8-1f1e7.png\"/>',fitzpatrick_scale:false,category:\"flags\"},somalia:{keywords:[\"so\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇸🇴\" src=\"1f1f8-1f1f4.png\"/>',fitzpatrick_scale:false,category:\"flags\"},south_africa:{keywords:[\"south\",\"africa\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇿🇦\" src=\"1f1ff-1f1e6.png\"/>',fitzpatrick_scale:false,category:\"flags\"},south_georgia_south_sandwich_islands:{keywords:[\"south\",\"georgia\",\"sandwich\",\"islands\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇬🇸\" src=\"1f1ec-1f1f8.png\"/>',fitzpatrick_scale:false,category:\"flags\"},kr:{keywords:[\"south\",\"korea\",\"nation\",\"flag\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇰🇷\" src=\"1f1f0-1f1f7.png\"/>',fitzpatrick_scale:false,category:\"flags\"},south_sudan:{keywords:[\"south\",\"sd\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇸🇸\" src=\"1f1f8-1f1f8.png\"/>',fitzpatrick_scale:false,category:\"flags\"},es:{keywords:[\"spain\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇪🇸\" src=\"1f1ea-1f1f8.png\"/>',fitzpatrick_scale:false,category:\"flags\"},sri_lanka:{keywords:[\"sri\",\"lanka\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇱🇰\" src=\"1f1f1-1f1f0.png\"/>',fitzpatrick_scale:false,category:\"flags\"},sudan:{keywords:[\"sd\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇸🇩\" src=\"1f1f8-1f1e9.png\"/>',fitzpatrick_scale:false,category:\"flags\"},suriname:{keywords:[\"sr\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇸🇷\" src=\"1f1f8-1f1f7.png\"/>',fitzpatrick_scale:false,category:\"flags\"},swaziland:{keywords:[\"sz\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇸🇿\" src=\"1f1f8-1f1ff.png\"/>',fitzpatrick_scale:false,category:\"flags\"},sweden:{keywords:[\"se\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇸🇪\" src=\"1f1f8-1f1ea.png\"/>',fitzpatrick_scale:false,category:\"flags\"},switzerland:{keywords:[\"ch\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇨🇭\" src=\"1f1e8-1f1ed.png\"/>',fitzpatrick_scale:false,category:\"flags\"},syria:{keywords:[\"syrian\",\"arab\",\"republic\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇸🇾\" src=\"1f1f8-1f1fe.png\"/>',fitzpatrick_scale:false,category:\"flags\"},taiwan:{keywords:[\"tw\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇹🇼\" src=\"1f1f9-1f1fc.png\"/>',fitzpatrick_scale:false,category:\"flags\"},tajikistan:{keywords:[\"tj\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇹🇯\" src=\"1f1f9-1f1ef.png\"/>',fitzpatrick_scale:false,category:\"flags\"},tanzania:{keywords:[\"tanzania,\",\"united\",\"republic\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇹🇿\" src=\"1f1f9-1f1ff.png\"/>',fitzpatrick_scale:false,category:\"flags\"},thailand:{keywords:[\"th\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇹🇭\" src=\"1f1f9-1f1ed.png\"/>',fitzpatrick_scale:false,category:\"flags\"},timor_leste:{keywords:[\"timor\",\"leste\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇹🇱\" src=\"1f1f9-1f1f1.png\"/>',fitzpatrick_scale:false,category:\"flags\"},togo:{keywords:[\"tg\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇹🇬\" src=\"1f1f9-1f1ec.png\"/>',fitzpatrick_scale:false,category:\"flags\"},tokelau:{keywords:[\"tk\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇹🇰\" src=\"1f1f9-1f1f0.png\"/>',fitzpatrick_scale:false,category:\"flags\"},tonga:{keywords:[\"to\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇹🇴\" src=\"1f1f9-1f1f4.png\"/>',fitzpatrick_scale:false,category:\"flags\"},trinidad_tobago:{keywords:[\"trinidad\",\"tobago\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇹🇹\" src=\"1f1f9-1f1f9.png\"/>',fitzpatrick_scale:false,category:\"flags\"},tunisia:{keywords:[\"tn\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇹🇳\" src=\"1f1f9-1f1f3.png\"/>',fitzpatrick_scale:false,category:\"flags\"},tr:{keywords:[\"turkey\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇹🇷\" src=\"1f1f9-1f1f7.png\"/>',fitzpatrick_scale:false,category:\"flags\"},turkmenistan:{keywords:[\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇹🇲\" src=\"1f1f9-1f1f2.png\"/>',fitzpatrick_scale:false,category:\"flags\"},turks_caicos_islands:{keywords:[\"turks\",\"caicos\",\"islands\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇹🇨\" src=\"1f1f9-1f1e8.png\"/>',fitzpatrick_scale:false,category:\"flags\"},tuvalu:{keywords:[\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇹🇻\" src=\"1f1f9-1f1fb.png\"/>',fitzpatrick_scale:false,category:\"flags\"},uganda:{keywords:[\"ug\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇺🇬\" src=\"1f1fa-1f1ec.png\"/>',fitzpatrick_scale:false,category:\"flags\"},ukraine:{keywords:[\"ua\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇺🇦\" src=\"1f1fa-1f1e6.png\"/>',fitzpatrick_scale:false,category:\"flags\"},united_arab_emirates:{keywords:[\"united\",\"arab\",\"emirates\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇦🇪\" src=\"1f1e6-1f1ea.png\"/>',fitzpatrick_scale:false,category:\"flags\"},uk:{keywords:[\"united\",\"kingdom\",\"great\",\"britain\",\"northern\",\"ireland\",\"flag\",\"nation\",\"country\",\"banner\",\"british\",\"UK\",\"english\",\"england\",\"union jack\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇬🇧\" src=\"1f1ec-1f1e7.png\"/>',fitzpatrick_scale:false,category:\"flags\"},england:{keywords:[\"flag\",\"english\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🏴󠁧󠁢󠁥󠁮󠁧󠁿\" src=\"1f3f4-e0067-e0062-e0065-e006e-e0067-e007f.png\"/>',fitzpatrick_scale:false,category:\"flags\"},scotland:{keywords:[\"flag\",\"scottish\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🏴󠁧󠁢󠁳󠁣󠁴󠁿\" src=\"1f3f4-e0067-e0062-e0073-e0063-e0074-e007f.png\"/>',fitzpatrick_scale:false,category:\"flags\"},wales:{keywords:[\"flag\",\"welsh\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🏴󠁧󠁢󠁷󠁬󠁳󠁿\" src=\"1f3f4-e0067-e0062-e0077-e006c-e0073-e007f.png\"/>',fitzpatrick_scale:false,category:\"flags\"},us:{keywords:[\"united\",\"states\",\"america\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇺🇸\" src=\"1f1fa-1f1f8.png\"/>',fitzpatrick_scale:false,category:\"flags\"},us_virgin_islands:{keywords:[\"virgin\",\"islands\",\"us\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇻🇮\" src=\"1f1fb-1f1ee.png\"/>',fitzpatrick_scale:false,category:\"flags\"},uruguay:{keywords:[\"uy\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇺🇾\" src=\"1f1fa-1f1fe.png\"/>',fitzpatrick_scale:false,category:\"flags\"},uzbekistan:{keywords:[\"uz\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇺🇿\" src=\"1f1fa-1f1ff.png\"/>',fitzpatrick_scale:false,category:\"flags\"},vanuatu:{keywords:[\"vu\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇻🇺\" src=\"1f1fb-1f1fa.png\"/>',fitzpatrick_scale:false,category:\"flags\"},vatican_city:{keywords:[\"vatican\",\"city\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇻🇦\" src=\"1f1fb-1f1e6.png\"/>',fitzpatrick_scale:false,category:\"flags\"},venezuela:{keywords:[\"ve\",\"bolivarian\",\"republic\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇻🇪\" src=\"1f1fb-1f1ea.png\"/>',fitzpatrick_scale:false,category:\"flags\"},vietnam:{keywords:[\"viet\",\"nam\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇻🇳\" src=\"1f1fb-1f1f3.png\"/>',fitzpatrick_scale:false,category:\"flags\"},wallis_futuna:{keywords:[\"wallis\",\"futuna\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇼🇫\" src=\"1f1fc-1f1eb.png\"/>',fitzpatrick_scale:false,category:\"flags\"},western_sahara:{keywords:[\"western\",\"sahara\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇪🇭\" src=\"1f1ea-1f1ed.png\"/>',fitzpatrick_scale:false,category:\"flags\"},yemen:{keywords:[\"ye\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇾🇪\" src=\"1f1fe-1f1ea.png\"/>',fitzpatrick_scale:false,category:\"flags\"},zambia:{keywords:[\"zm\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇿🇲\" src=\"1f1ff-1f1f2.png\"/>',fitzpatrick_scale:false,category:\"flags\"},zimbabwe:{keywords:[\"zw\",\"flag\",\"nation\",\"country\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇿🇼\" src=\"1f1ff-1f1fc.png\"/>',fitzpatrick_scale:false,category:\"flags\"},united_nations:{keywords:[\"un\",\"flag\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🇺🇳\" src=\"1f1fa-1f1f3.png\"/>',fitzpatrick_scale:false,category:\"flags\"},pirate_flag:{keywords:[\"skull\",\"crossbones\",\"flag\",\"banner\"],char:'<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"🏴‍☠️\" src=\"1f3f4-200d-2620-fe0f.png\"/>',fitzpatrick_scale:false,category:\"flags\"}});"
  },
  {
    "path": "apps/web-antd/public/tinymce/plugins/emoticons/js/emojis.js",
    "content": "window.tinymce.Resource.add(\"tinymce.plugins.emoticons\",{grinning:{keywords:[\"face\",\"smile\",\"happy\",\"joy\",\":D\",\"grin\"],char:\"😀\",fitzpatrick_scale:false,category:\"people\"},grimacing:{keywords:[\"face\",\"grimace\",\"teeth\"],char:\"😬\",fitzpatrick_scale:false,category:\"people\"},grin:{keywords:[\"face\",\"happy\",\"smile\",\"joy\",\"kawaii\"],char:\"😁\",fitzpatrick_scale:false,category:\"people\"},joy:{keywords:[\"face\",\"cry\",\"tears\",\"weep\",\"happy\",\"happytears\",\"haha\"],char:\"😂\",fitzpatrick_scale:false,category:\"people\"},rofl:{keywords:[\"face\",\"rolling\",\"floor\",\"laughing\",\"lol\",\"haha\"],char:\"🤣\",fitzpatrick_scale:false,category:\"people\"},partying:{keywords:[\"face\",\"celebration\",\"woohoo\"],char:\"🥳\",fitzpatrick_scale:false,category:\"people\"},smiley:{keywords:[\"face\",\"happy\",\"joy\",\"haha\",\":D\",\":)\",\"smile\",\"funny\"],char:\"😃\",fitzpatrick_scale:false,category:\"people\"},smile:{keywords:[\"face\",\"happy\",\"joy\",\"funny\",\"haha\",\"laugh\",\"like\",\":D\",\":)\"],char:\"😄\",fitzpatrick_scale:false,category:\"people\"},sweat_smile:{keywords:[\"face\",\"hot\",\"happy\",\"laugh\",\"sweat\",\"smile\",\"relief\"],char:\"😅\",fitzpatrick_scale:false,category:\"people\"},laughing:{keywords:[\"happy\",\"joy\",\"lol\",\"satisfied\",\"haha\",\"face\",\"glad\",\"XD\",\"laugh\"],char:\"😆\",fitzpatrick_scale:false,category:\"people\"},innocent:{keywords:[\"face\",\"angel\",\"heaven\",\"halo\"],char:\"😇\",fitzpatrick_scale:false,category:\"people\"},wink:{keywords:[\"face\",\"happy\",\"mischievous\",\"secret\",\";)\",\"smile\",\"eye\"],char:\"😉\",fitzpatrick_scale:false,category:\"people\"},blush:{keywords:[\"face\",\"smile\",\"happy\",\"flushed\",\"crush\",\"embarrassed\",\"shy\",\"joy\"],char:\"😊\",fitzpatrick_scale:false,category:\"people\"},slightly_smiling_face:{keywords:[\"face\",\"smile\"],char:\"🙂\",fitzpatrick_scale:false,category:\"people\"},upside_down_face:{keywords:[\"face\",\"flipped\",\"silly\",\"smile\"],char:\"🙃\",fitzpatrick_scale:false,category:\"people\"},relaxed:{keywords:[\"face\",\"blush\",\"massage\",\"happiness\"],char:\"☺️\",fitzpatrick_scale:false,category:\"people\"},yum:{keywords:[\"happy\",\"joy\",\"tongue\",\"smile\",\"face\",\"silly\",\"yummy\",\"nom\",\"delicious\",\"savouring\"],char:\"😋\",fitzpatrick_scale:false,category:\"people\"},relieved:{keywords:[\"face\",\"relaxed\",\"phew\",\"massage\",\"happiness\"],char:\"😌\",fitzpatrick_scale:false,category:\"people\"},heart_eyes:{keywords:[\"face\",\"love\",\"like\",\"affection\",\"valentines\",\"infatuation\",\"crush\",\"heart\"],char:\"😍\",fitzpatrick_scale:false,category:\"people\"},smiling_face_with_three_hearts:{keywords:[\"face\",\"love\",\"like\",\"affection\",\"valentines\",\"infatuation\",\"crush\",\"hearts\",\"adore\"],char:\"🥰\",fitzpatrick_scale:false,category:\"people\"},kissing_heart:{keywords:[\"face\",\"love\",\"like\",\"affection\",\"valentines\",\"infatuation\",\"kiss\"],char:\"😘\",fitzpatrick_scale:false,category:\"people\"},kissing:{keywords:[\"love\",\"like\",\"face\",\"3\",\"valentines\",\"infatuation\",\"kiss\"],char:\"😗\",fitzpatrick_scale:false,category:\"people\"},kissing_smiling_eyes:{keywords:[\"face\",\"affection\",\"valentines\",\"infatuation\",\"kiss\"],char:\"😙\",fitzpatrick_scale:false,category:\"people\"},kissing_closed_eyes:{keywords:[\"face\",\"love\",\"like\",\"affection\",\"valentines\",\"infatuation\",\"kiss\"],char:\"😚\",fitzpatrick_scale:false,category:\"people\"},stuck_out_tongue_winking_eye:{keywords:[\"face\",\"prank\",\"childish\",\"playful\",\"mischievous\",\"smile\",\"wink\",\"tongue\"],char:\"😜\",fitzpatrick_scale:false,category:\"people\"},zany:{keywords:[\"face\",\"goofy\",\"crazy\"],char:\"🤪\",fitzpatrick_scale:false,category:\"people\"},raised_eyebrow:{keywords:[\"face\",\"distrust\",\"scepticism\",\"disapproval\",\"disbelief\",\"surprise\"],char:\"🤨\",fitzpatrick_scale:false,category:\"people\"},monocle:{keywords:[\"face\",\"stuffy\",\"wealthy\"],char:\"🧐\",fitzpatrick_scale:false,category:\"people\"},stuck_out_tongue_closed_eyes:{keywords:[\"face\",\"prank\",\"playful\",\"mischievous\",\"smile\",\"tongue\"],char:\"😝\",fitzpatrick_scale:false,category:\"people\"},stuck_out_tongue:{keywords:[\"face\",\"prank\",\"childish\",\"playful\",\"mischievous\",\"smile\",\"tongue\"],char:\"😛\",fitzpatrick_scale:false,category:\"people\"},money_mouth_face:{keywords:[\"face\",\"rich\",\"dollar\",\"money\"],char:\"🤑\",fitzpatrick_scale:false,category:\"people\"},nerd_face:{keywords:[\"face\",\"nerdy\",\"geek\",\"dork\"],char:\"🤓\",fitzpatrick_scale:false,category:\"people\"},sunglasses:{keywords:[\"face\",\"cool\",\"smile\",\"summer\",\"beach\",\"sunglass\"],char:\"😎\",fitzpatrick_scale:false,category:\"people\"},star_struck:{keywords:[\"face\",\"smile\",\"starry\",\"eyes\",\"grinning\"],char:\"🤩\",fitzpatrick_scale:false,category:\"people\"},clown_face:{keywords:[\"face\"],char:\"🤡\",fitzpatrick_scale:false,category:\"people\"},cowboy_hat_face:{keywords:[\"face\",\"cowgirl\",\"hat\"],char:\"🤠\",fitzpatrick_scale:false,category:\"people\"},hugs:{keywords:[\"face\",\"smile\",\"hug\"],char:\"🤗\",fitzpatrick_scale:false,category:\"people\"},smirk:{keywords:[\"face\",\"smile\",\"mean\",\"prank\",\"smug\",\"sarcasm\"],char:\"😏\",fitzpatrick_scale:false,category:\"people\"},no_mouth:{keywords:[\"face\",\"hellokitty\"],char:\"😶\",fitzpatrick_scale:false,category:\"people\"},neutral_face:{keywords:[\"indifference\",\"meh\",\":|\",\"neutral\"],char:\"😐\",fitzpatrick_scale:false,category:\"people\"},expressionless:{keywords:[\"face\",\"indifferent\",\"-_-\",\"meh\",\"deadpan\"],char:\"😑\",fitzpatrick_scale:false,category:\"people\"},unamused:{keywords:[\"indifference\",\"bored\",\"straight face\",\"serious\",\"sarcasm\",\"unimpressed\",\"skeptical\",\"dubious\",\"side_eye\"],char:\"😒\",fitzpatrick_scale:false,category:\"people\"},roll_eyes:{keywords:[\"face\",\"eyeroll\",\"frustrated\"],char:\"🙄\",fitzpatrick_scale:false,category:\"people\"},thinking:{keywords:[\"face\",\"hmmm\",\"think\",\"consider\"],char:\"🤔\",fitzpatrick_scale:false,category:\"people\"},lying_face:{keywords:[\"face\",\"lie\",\"pinocchio\"],char:\"🤥\",fitzpatrick_scale:false,category:\"people\"},hand_over_mouth:{keywords:[\"face\",\"whoops\",\"shock\",\"surprise\"],char:\"🤭\",fitzpatrick_scale:false,category:\"people\"},shushing:{keywords:[\"face\",\"quiet\",\"shhh\"],char:\"🤫\",fitzpatrick_scale:false,category:\"people\"},symbols_over_mouth:{keywords:[\"face\",\"swearing\",\"cursing\",\"cussing\",\"profanity\",\"expletive\"],char:\"🤬\",fitzpatrick_scale:false,category:\"people\"},exploding_head:{keywords:[\"face\",\"shocked\",\"mind\",\"blown\"],char:\"🤯\",fitzpatrick_scale:false,category:\"people\"},flushed:{keywords:[\"face\",\"blush\",\"shy\",\"flattered\"],char:\"😳\",fitzpatrick_scale:false,category:\"people\"},disappointed:{keywords:[\"face\",\"sad\",\"upset\",\"depressed\",\":(\"],char:\"😞\",fitzpatrick_scale:false,category:\"people\"},worried:{keywords:[\"face\",\"concern\",\"nervous\",\":(\"],char:\"😟\",fitzpatrick_scale:false,category:\"people\"},angry:{keywords:[\"mad\",\"face\",\"annoyed\",\"frustrated\"],char:\"😠\",fitzpatrick_scale:false,category:\"people\"},rage:{keywords:[\"angry\",\"mad\",\"hate\",\"despise\"],char:\"😡\",fitzpatrick_scale:false,category:\"people\"},pensive:{keywords:[\"face\",\"sad\",\"depressed\",\"upset\"],char:\"😔\",fitzpatrick_scale:false,category:\"people\"},confused:{keywords:[\"face\",\"indifference\",\"huh\",\"weird\",\"hmmm\",\":/\"],char:\"😕\",fitzpatrick_scale:false,category:\"people\"},slightly_frowning_face:{keywords:[\"face\",\"frowning\",\"disappointed\",\"sad\",\"upset\"],char:\"🙁\",fitzpatrick_scale:false,category:\"people\"},frowning_face:{keywords:[\"face\",\"sad\",\"upset\",\"frown\"],char:\"☹\",fitzpatrick_scale:false,category:\"people\"},persevere:{keywords:[\"face\",\"sick\",\"no\",\"upset\",\"oops\"],char:\"😣\",fitzpatrick_scale:false,category:\"people\"},confounded:{keywords:[\"face\",\"confused\",\"sick\",\"unwell\",\"oops\",\":S\"],char:\"😖\",fitzpatrick_scale:false,category:\"people\"},tired_face:{keywords:[\"sick\",\"whine\",\"upset\",\"frustrated\"],char:\"😫\",fitzpatrick_scale:false,category:\"people\"},weary:{keywords:[\"face\",\"tired\",\"sleepy\",\"sad\",\"frustrated\",\"upset\"],char:\"😩\",fitzpatrick_scale:false,category:\"people\"},pleading:{keywords:[\"face\",\"begging\",\"mercy\"],char:\"🥺\",fitzpatrick_scale:false,category:\"people\"},triumph:{keywords:[\"face\",\"gas\",\"phew\",\"proud\",\"pride\"],char:\"😤\",fitzpatrick_scale:false,category:\"people\"},open_mouth:{keywords:[\"face\",\"surprise\",\"impressed\",\"wow\",\"whoa\",\":O\"],char:\"😮\",fitzpatrick_scale:false,category:\"people\"},scream:{keywords:[\"face\",\"munch\",\"scared\",\"omg\"],char:\"😱\",fitzpatrick_scale:false,category:\"people\"},fearful:{keywords:[\"face\",\"scared\",\"terrified\",\"nervous\",\"oops\",\"huh\"],char:\"😨\",fitzpatrick_scale:false,category:\"people\"},cold_sweat:{keywords:[\"face\",\"nervous\",\"sweat\"],char:\"😰\",fitzpatrick_scale:false,category:\"people\"},hushed:{keywords:[\"face\",\"woo\",\"shh\"],char:\"😯\",fitzpatrick_scale:false,category:\"people\"},frowning:{keywords:[\"face\",\"aw\",\"what\"],char:\"😦\",fitzpatrick_scale:false,category:\"people\"},anguished:{keywords:[\"face\",\"stunned\",\"nervous\"],char:\"😧\",fitzpatrick_scale:false,category:\"people\"},cry:{keywords:[\"face\",\"tears\",\"sad\",\"depressed\",\"upset\",\":'(\"],char:\"😢\",fitzpatrick_scale:false,category:\"people\"},disappointed_relieved:{keywords:[\"face\",\"phew\",\"sweat\",\"nervous\"],char:\"😥\",fitzpatrick_scale:false,category:\"people\"},drooling_face:{keywords:[\"face\"],char:\"🤤\",fitzpatrick_scale:false,category:\"people\"},sleepy:{keywords:[\"face\",\"tired\",\"rest\",\"nap\"],char:\"😪\",fitzpatrick_scale:false,category:\"people\"},sweat:{keywords:[\"face\",\"hot\",\"sad\",\"tired\",\"exercise\"],char:\"😓\",fitzpatrick_scale:false,category:\"people\"},hot:{keywords:[\"face\",\"feverish\",\"heat\",\"red\",\"sweating\"],char:\"🥵\",fitzpatrick_scale:false,category:\"people\"},cold:{keywords:[\"face\",\"blue\",\"freezing\",\"frozen\",\"frostbite\",\"icicles\"],char:\"🥶\",fitzpatrick_scale:false,category:\"people\"},sob:{keywords:[\"face\",\"cry\",\"tears\",\"sad\",\"upset\",\"depressed\"],char:\"😭\",fitzpatrick_scale:false,category:\"people\"},dizzy_face:{keywords:[\"spent\",\"unconscious\",\"xox\",\"dizzy\"],char:\"😵\",fitzpatrick_scale:false,category:\"people\"},astonished:{keywords:[\"face\",\"xox\",\"surprised\",\"poisoned\"],char:\"😲\",fitzpatrick_scale:false,category:\"people\"},zipper_mouth_face:{keywords:[\"face\",\"sealed\",\"zipper\",\"secret\"],char:\"🤐\",fitzpatrick_scale:false,category:\"people\"},nauseated_face:{keywords:[\"face\",\"vomit\",\"gross\",\"green\",\"sick\",\"throw up\",\"ill\"],char:\"🤢\",fitzpatrick_scale:false,category:\"people\"},sneezing_face:{keywords:[\"face\",\"gesundheit\",\"sneeze\",\"sick\",\"allergy\"],char:\"🤧\",fitzpatrick_scale:false,category:\"people\"},vomiting:{keywords:[\"face\",\"sick\"],char:\"🤮\",fitzpatrick_scale:false,category:\"people\"},mask:{keywords:[\"face\",\"sick\",\"ill\",\"disease\"],char:\"😷\",fitzpatrick_scale:false,category:\"people\"},face_with_thermometer:{keywords:[\"sick\",\"temperature\",\"thermometer\",\"cold\",\"fever\"],char:\"🤒\",fitzpatrick_scale:false,category:\"people\"},face_with_head_bandage:{keywords:[\"injured\",\"clumsy\",\"bandage\",\"hurt\"],char:\"🤕\",fitzpatrick_scale:false,category:\"people\"},woozy:{keywords:[\"face\",\"dizzy\",\"intoxicated\",\"tipsy\",\"wavy\"],char:\"🥴\",fitzpatrick_scale:false,category:\"people\"},sleeping:{keywords:[\"face\",\"tired\",\"sleepy\",\"night\",\"zzz\"],char:\"😴\",fitzpatrick_scale:false,category:\"people\"},zzz:{keywords:[\"sleepy\",\"tired\",\"dream\"],char:\"💤\",fitzpatrick_scale:false,category:\"people\"},poop:{keywords:[\"hankey\",\"shitface\",\"fail\",\"turd\",\"shit\"],char:\"💩\",fitzpatrick_scale:false,category:\"people\"},smiling_imp:{keywords:[\"devil\",\"horns\"],char:\"😈\",fitzpatrick_scale:false,category:\"people\"},imp:{keywords:[\"devil\",\"angry\",\"horns\"],char:\"👿\",fitzpatrick_scale:false,category:\"people\"},japanese_ogre:{keywords:[\"monster\",\"red\",\"mask\",\"halloween\",\"scary\",\"creepy\",\"devil\",\"demon\",\"japanese\",\"ogre\"],char:\"👹\",fitzpatrick_scale:false,category:\"people\"},japanese_goblin:{keywords:[\"red\",\"evil\",\"mask\",\"monster\",\"scary\",\"creepy\",\"japanese\",\"goblin\"],char:\"👺\",fitzpatrick_scale:false,category:\"people\"},skull:{keywords:[\"dead\",\"skeleton\",\"creepy\",\"death\"],char:\"💀\",fitzpatrick_scale:false,category:\"people\"},ghost:{keywords:[\"halloween\",\"spooky\",\"scary\"],char:\"👻\",fitzpatrick_scale:false,category:\"people\"},alien:{keywords:[\"UFO\",\"paul\",\"weird\",\"outer_space\"],char:\"👽\",fitzpatrick_scale:false,category:\"people\"},robot:{keywords:[\"computer\",\"machine\",\"bot\"],char:\"🤖\",fitzpatrick_scale:false,category:\"people\"},smiley_cat:{keywords:[\"animal\",\"cats\",\"happy\",\"smile\"],char:\"😺\",fitzpatrick_scale:false,category:\"people\"},smile_cat:{keywords:[\"animal\",\"cats\",\"smile\"],char:\"😸\",fitzpatrick_scale:false,category:\"people\"},joy_cat:{keywords:[\"animal\",\"cats\",\"haha\",\"happy\",\"tears\"],char:\"😹\",fitzpatrick_scale:false,category:\"people\"},heart_eyes_cat:{keywords:[\"animal\",\"love\",\"like\",\"affection\",\"cats\",\"valentines\",\"heart\"],char:\"😻\",fitzpatrick_scale:false,category:\"people\"},smirk_cat:{keywords:[\"animal\",\"cats\",\"smirk\"],char:\"😼\",fitzpatrick_scale:false,category:\"people\"},kissing_cat:{keywords:[\"animal\",\"cats\",\"kiss\"],char:\"😽\",fitzpatrick_scale:false,category:\"people\"},scream_cat:{keywords:[\"animal\",\"cats\",\"munch\",\"scared\",\"scream\"],char:\"🙀\",fitzpatrick_scale:false,category:\"people\"},crying_cat_face:{keywords:[\"animal\",\"tears\",\"weep\",\"sad\",\"cats\",\"upset\",\"cry\"],char:\"😿\",fitzpatrick_scale:false,category:\"people\"},pouting_cat:{keywords:[\"animal\",\"cats\"],char:\"😾\",fitzpatrick_scale:false,category:\"people\"},palms_up:{keywords:[\"hands\",\"gesture\",\"cupped\",\"prayer\"],char:\"🤲\",fitzpatrick_scale:true,category:\"people\"},raised_hands:{keywords:[\"gesture\",\"hooray\",\"yea\",\"celebration\",\"hands\"],char:\"🙌\",fitzpatrick_scale:true,category:\"people\"},clap:{keywords:[\"hands\",\"praise\",\"applause\",\"congrats\",\"yay\"],char:\"👏\",fitzpatrick_scale:true,category:\"people\"},wave:{keywords:[\"hands\",\"gesture\",\"goodbye\",\"solong\",\"farewell\",\"hello\",\"hi\",\"palm\"],char:\"👋\",fitzpatrick_scale:true,category:\"people\"},call_me_hand:{keywords:[\"hands\",\"gesture\"],char:\"🤙\",fitzpatrick_scale:true,category:\"people\"},\"+1\":{keywords:[\"thumbsup\",\"yes\",\"awesome\",\"good\",\"agree\",\"accept\",\"cool\",\"hand\",\"like\"],char:\"👍\",fitzpatrick_scale:true,category:\"people\"},\"-1\":{keywords:[\"thumbsdown\",\"no\",\"dislike\",\"hand\"],char:\"👎\",fitzpatrick_scale:true,category:\"people\"},facepunch:{keywords:[\"angry\",\"violence\",\"fist\",\"hit\",\"attack\",\"hand\"],char:\"👊\",fitzpatrick_scale:true,category:\"people\"},fist:{keywords:[\"fingers\",\"hand\",\"grasp\"],char:\"✊\",fitzpatrick_scale:true,category:\"people\"},fist_left:{keywords:[\"hand\",\"fistbump\"],char:\"🤛\",fitzpatrick_scale:true,category:\"people\"},fist_right:{keywords:[\"hand\",\"fistbump\"],char:\"🤜\",fitzpatrick_scale:true,category:\"people\"},v:{keywords:[\"fingers\",\"ohyeah\",\"hand\",\"peace\",\"victory\",\"two\"],char:\"✌\",fitzpatrick_scale:true,category:\"people\"},ok_hand:{keywords:[\"fingers\",\"limbs\",\"perfect\",\"ok\",\"okay\"],char:\"👌\",fitzpatrick_scale:true,category:\"people\"},raised_hand:{keywords:[\"fingers\",\"stop\",\"highfive\",\"palm\",\"ban\"],char:\"✋\",fitzpatrick_scale:true,category:\"people\"},raised_back_of_hand:{keywords:[\"fingers\",\"raised\",\"backhand\"],char:\"🤚\",fitzpatrick_scale:true,category:\"people\"},open_hands:{keywords:[\"fingers\",\"butterfly\",\"hands\",\"open\"],char:\"👐\",fitzpatrick_scale:true,category:\"people\"},muscle:{keywords:[\"arm\",\"flex\",\"hand\",\"summer\",\"strong\",\"biceps\"],char:\"💪\",fitzpatrick_scale:true,category:\"people\"},pray:{keywords:[\"please\",\"hope\",\"wish\",\"namaste\",\"highfive\"],char:\"🙏\",fitzpatrick_scale:true,category:\"people\"},foot:{keywords:[\"kick\",\"stomp\"],char:\"🦶\",fitzpatrick_scale:true,category:\"people\"},leg:{keywords:[\"kick\",\"limb\"],char:\"🦵\",fitzpatrick_scale:true,category:\"people\"},handshake:{keywords:[\"agreement\",\"shake\"],char:\"🤝\",fitzpatrick_scale:false,category:\"people\"},point_up:{keywords:[\"hand\",\"fingers\",\"direction\",\"up\"],char:\"☝\",fitzpatrick_scale:true,category:\"people\"},point_up_2:{keywords:[\"fingers\",\"hand\",\"direction\",\"up\"],char:\"👆\",fitzpatrick_scale:true,category:\"people\"},point_down:{keywords:[\"fingers\",\"hand\",\"direction\",\"down\"],char:\"👇\",fitzpatrick_scale:true,category:\"people\"},point_left:{keywords:[\"direction\",\"fingers\",\"hand\",\"left\"],char:\"👈\",fitzpatrick_scale:true,category:\"people\"},point_right:{keywords:[\"fingers\",\"hand\",\"direction\",\"right\"],char:\"👉\",fitzpatrick_scale:true,category:\"people\"},fu:{keywords:[\"hand\",\"fingers\",\"rude\",\"middle\",\"flipping\"],char:\"🖕\",fitzpatrick_scale:true,category:\"people\"},raised_hand_with_fingers_splayed:{keywords:[\"hand\",\"fingers\",\"palm\"],char:\"🖐\",fitzpatrick_scale:true,category:\"people\"},love_you:{keywords:[\"hand\",\"fingers\",\"gesture\"],char:\"🤟\",fitzpatrick_scale:true,category:\"people\"},metal:{keywords:[\"hand\",\"fingers\",\"evil_eye\",\"sign_of_horns\",\"rock_on\"],char:\"🤘\",fitzpatrick_scale:true,category:\"people\"},crossed_fingers:{keywords:[\"good\",\"lucky\"],char:\"🤞\",fitzpatrick_scale:true,category:\"people\"},vulcan_salute:{keywords:[\"hand\",\"fingers\",\"spock\",\"star trek\"],char:\"🖖\",fitzpatrick_scale:true,category:\"people\"},writing_hand:{keywords:[\"lower_left_ballpoint_pen\",\"stationery\",\"write\",\"compose\"],char:\"✍\",fitzpatrick_scale:true,category:\"people\"},selfie:{keywords:[\"camera\",\"phone\"],char:\"🤳\",fitzpatrick_scale:true,category:\"people\"},nail_care:{keywords:[\"beauty\",\"manicure\",\"finger\",\"fashion\",\"nail\"],char:\"💅\",fitzpatrick_scale:true,category:\"people\"},lips:{keywords:[\"mouth\",\"kiss\"],char:\"👄\",fitzpatrick_scale:false,category:\"people\"},tooth:{keywords:[\"teeth\",\"dentist\"],char:\"🦷\",fitzpatrick_scale:false,category:\"people\"},tongue:{keywords:[\"mouth\",\"playful\"],char:\"👅\",fitzpatrick_scale:false,category:\"people\"},ear:{keywords:[\"face\",\"hear\",\"sound\",\"listen\"],char:\"👂\",fitzpatrick_scale:true,category:\"people\"},nose:{keywords:[\"smell\",\"sniff\"],char:\"👃\",fitzpatrick_scale:true,category:\"people\"},eye:{keywords:[\"face\",\"look\",\"see\",\"watch\",\"stare\"],char:\"👁\",fitzpatrick_scale:false,category:\"people\"},eyes:{keywords:[\"look\",\"watch\",\"stalk\",\"peek\",\"see\"],char:\"👀\",fitzpatrick_scale:false,category:\"people\"},brain:{keywords:[\"smart\",\"intelligent\"],char:\"🧠\",fitzpatrick_scale:false,category:\"people\"},bust_in_silhouette:{keywords:[\"user\",\"person\",\"human\"],char:\"👤\",fitzpatrick_scale:false,category:\"people\"},busts_in_silhouette:{keywords:[\"user\",\"person\",\"human\",\"group\",\"team\"],char:\"👥\",fitzpatrick_scale:false,category:\"people\"},speaking_head:{keywords:[\"user\",\"person\",\"human\",\"sing\",\"say\",\"talk\"],char:\"🗣\",fitzpatrick_scale:false,category:\"people\"},baby:{keywords:[\"child\",\"boy\",\"girl\",\"toddler\"],char:\"👶\",fitzpatrick_scale:true,category:\"people\"},child:{keywords:[\"gender-neutral\",\"young\"],char:\"🧒\",fitzpatrick_scale:true,category:\"people\"},boy:{keywords:[\"man\",\"male\",\"guy\",\"teenager\"],char:\"👦\",fitzpatrick_scale:true,category:\"people\"},girl:{keywords:[\"female\",\"woman\",\"teenager\"],char:\"👧\",fitzpatrick_scale:true,category:\"people\"},adult:{keywords:[\"gender-neutral\",\"person\"],char:\"🧑\",fitzpatrick_scale:true,category:\"people\"},man:{keywords:[\"mustache\",\"father\",\"dad\",\"guy\",\"classy\",\"sir\",\"moustache\"],char:\"👨\",fitzpatrick_scale:true,category:\"people\"},woman:{keywords:[\"female\",\"girls\",\"lady\"],char:\"👩\",fitzpatrick_scale:true,category:\"people\"},blonde_woman:{keywords:[\"woman\",\"female\",\"girl\",\"blonde\",\"person\"],char:\"👱‍♀️\",fitzpatrick_scale:true,category:\"people\"},blonde_man:{keywords:[\"man\",\"male\",\"boy\",\"blonde\",\"guy\",\"person\"],char:\"👱\",fitzpatrick_scale:true,category:\"people\"},bearded_person:{keywords:[\"person\",\"bewhiskered\"],char:\"🧔\",fitzpatrick_scale:true,category:\"people\"},older_adult:{keywords:[\"human\",\"elder\",\"senior\",\"gender-neutral\"],char:\"🧓\",fitzpatrick_scale:true,category:\"people\"},older_man:{keywords:[\"human\",\"male\",\"men\",\"old\",\"elder\",\"senior\"],char:\"👴\",fitzpatrick_scale:true,category:\"people\"},older_woman:{keywords:[\"human\",\"female\",\"women\",\"lady\",\"old\",\"elder\",\"senior\"],char:\"👵\",fitzpatrick_scale:true,category:\"people\"},man_with_gua_pi_mao:{keywords:[\"male\",\"boy\",\"chinese\"],char:\"👲\",fitzpatrick_scale:true,category:\"people\"},woman_with_headscarf:{keywords:[\"female\",\"hijab\",\"mantilla\",\"tichel\"],char:\"🧕\",fitzpatrick_scale:true,category:\"people\"},woman_with_turban:{keywords:[\"female\",\"indian\",\"hinduism\",\"arabs\",\"woman\"],char:\"👳‍♀️\",fitzpatrick_scale:true,category:\"people\"},man_with_turban:{keywords:[\"male\",\"indian\",\"hinduism\",\"arabs\"],char:\"👳\",fitzpatrick_scale:true,category:\"people\"},policewoman:{keywords:[\"woman\",\"police\",\"law\",\"legal\",\"enforcement\",\"arrest\",\"911\",\"female\"],char:\"👮‍♀️\",fitzpatrick_scale:true,category:\"people\"},policeman:{keywords:[\"man\",\"police\",\"law\",\"legal\",\"enforcement\",\"arrest\",\"911\"],char:\"👮\",fitzpatrick_scale:true,category:\"people\"},construction_worker_woman:{keywords:[\"female\",\"human\",\"wip\",\"build\",\"construction\",\"worker\",\"labor\",\"woman\"],char:\"👷‍♀️\",fitzpatrick_scale:true,category:\"people\"},construction_worker_man:{keywords:[\"male\",\"human\",\"wip\",\"guy\",\"build\",\"construction\",\"worker\",\"labor\"],char:\"👷\",fitzpatrick_scale:true,category:\"people\"},guardswoman:{keywords:[\"uk\",\"gb\",\"british\",\"female\",\"royal\",\"woman\"],char:\"💂‍♀️\",fitzpatrick_scale:true,category:\"people\"},guardsman:{keywords:[\"uk\",\"gb\",\"british\",\"male\",\"guy\",\"royal\"],char:\"💂\",fitzpatrick_scale:true,category:\"people\"},female_detective:{keywords:[\"human\",\"spy\",\"detective\",\"female\",\"woman\"],char:\"🕵️‍♀️\",fitzpatrick_scale:true,category:\"people\"},male_detective:{keywords:[\"human\",\"spy\",\"detective\"],char:\"🕵\",fitzpatrick_scale:true,category:\"people\"},woman_health_worker:{keywords:[\"doctor\",\"nurse\",\"therapist\",\"healthcare\",\"woman\",\"human\"],char:\"👩‍⚕️\",fitzpatrick_scale:true,category:\"people\"},man_health_worker:{keywords:[\"doctor\",\"nurse\",\"therapist\",\"healthcare\",\"man\",\"human\"],char:\"👨‍⚕️\",fitzpatrick_scale:true,category:\"people\"},woman_farmer:{keywords:[\"rancher\",\"gardener\",\"woman\",\"human\"],char:\"👩‍🌾\",fitzpatrick_scale:true,category:\"people\"},man_farmer:{keywords:[\"rancher\",\"gardener\",\"man\",\"human\"],char:\"👨‍🌾\",fitzpatrick_scale:true,category:\"people\"},woman_cook:{keywords:[\"chef\",\"woman\",\"human\"],char:\"👩‍🍳\",fitzpatrick_scale:true,category:\"people\"},man_cook:{keywords:[\"chef\",\"man\",\"human\"],char:\"👨‍🍳\",fitzpatrick_scale:true,category:\"people\"},woman_student:{keywords:[\"graduate\",\"woman\",\"human\"],char:\"👩‍🎓\",fitzpatrick_scale:true,category:\"people\"},man_student:{keywords:[\"graduate\",\"man\",\"human\"],char:\"👨‍🎓\",fitzpatrick_scale:true,category:\"people\"},woman_singer:{keywords:[\"rockstar\",\"entertainer\",\"woman\",\"human\"],char:\"👩‍🎤\",fitzpatrick_scale:true,category:\"people\"},man_singer:{keywords:[\"rockstar\",\"entertainer\",\"man\",\"human\"],char:\"👨‍🎤\",fitzpatrick_scale:true,category:\"people\"},woman_teacher:{keywords:[\"instructor\",\"professor\",\"woman\",\"human\"],char:\"👩‍🏫\",fitzpatrick_scale:true,category:\"people\"},man_teacher:{keywords:[\"instructor\",\"professor\",\"man\",\"human\"],char:\"👨‍🏫\",fitzpatrick_scale:true,category:\"people\"},woman_factory_worker:{keywords:[\"assembly\",\"industrial\",\"woman\",\"human\"],char:\"👩‍🏭\",fitzpatrick_scale:true,category:\"people\"},man_factory_worker:{keywords:[\"assembly\",\"industrial\",\"man\",\"human\"],char:\"👨‍🏭\",fitzpatrick_scale:true,category:\"people\"},woman_technologist:{keywords:[\"coder\",\"developer\",\"engineer\",\"programmer\",\"software\",\"woman\",\"human\",\"laptop\",\"computer\"],char:\"👩‍💻\",fitzpatrick_scale:true,category:\"people\"},man_technologist:{keywords:[\"coder\",\"developer\",\"engineer\",\"programmer\",\"software\",\"man\",\"human\",\"laptop\",\"computer\"],char:\"👨‍💻\",fitzpatrick_scale:true,category:\"people\"},woman_office_worker:{keywords:[\"business\",\"manager\",\"woman\",\"human\"],char:\"👩‍💼\",fitzpatrick_scale:true,category:\"people\"},man_office_worker:{keywords:[\"business\",\"manager\",\"man\",\"human\"],char:\"👨‍💼\",fitzpatrick_scale:true,category:\"people\"},woman_mechanic:{keywords:[\"plumber\",\"woman\",\"human\",\"wrench\"],char:\"👩‍🔧\",fitzpatrick_scale:true,category:\"people\"},man_mechanic:{keywords:[\"plumber\",\"man\",\"human\",\"wrench\"],char:\"👨‍🔧\",fitzpatrick_scale:true,category:\"people\"},woman_scientist:{keywords:[\"biologist\",\"chemist\",\"engineer\",\"physicist\",\"woman\",\"human\"],char:\"👩‍🔬\",fitzpatrick_scale:true,category:\"people\"},man_scientist:{keywords:[\"biologist\",\"chemist\",\"engineer\",\"physicist\",\"man\",\"human\"],char:\"👨‍🔬\",fitzpatrick_scale:true,category:\"people\"},woman_artist:{keywords:[\"painter\",\"woman\",\"human\"],char:\"👩‍🎨\",fitzpatrick_scale:true,category:\"people\"},man_artist:{keywords:[\"painter\",\"man\",\"human\"],char:\"👨‍🎨\",fitzpatrick_scale:true,category:\"people\"},woman_firefighter:{keywords:[\"fireman\",\"woman\",\"human\"],char:\"👩‍🚒\",fitzpatrick_scale:true,category:\"people\"},man_firefighter:{keywords:[\"fireman\",\"man\",\"human\"],char:\"👨‍🚒\",fitzpatrick_scale:true,category:\"people\"},woman_pilot:{keywords:[\"aviator\",\"plane\",\"woman\",\"human\"],char:\"👩‍✈️\",fitzpatrick_scale:true,category:\"people\"},man_pilot:{keywords:[\"aviator\",\"plane\",\"man\",\"human\"],char:\"👨‍✈️\",fitzpatrick_scale:true,category:\"people\"},woman_astronaut:{keywords:[\"space\",\"rocket\",\"woman\",\"human\"],char:\"👩‍🚀\",fitzpatrick_scale:true,category:\"people\"},man_astronaut:{keywords:[\"space\",\"rocket\",\"man\",\"human\"],char:\"👨‍🚀\",fitzpatrick_scale:true,category:\"people\"},woman_judge:{keywords:[\"justice\",\"court\",\"woman\",\"human\"],char:\"👩‍⚖️\",fitzpatrick_scale:true,category:\"people\"},man_judge:{keywords:[\"justice\",\"court\",\"man\",\"human\"],char:\"👨‍⚖️\",fitzpatrick_scale:true,category:\"people\"},woman_superhero:{keywords:[\"woman\",\"female\",\"good\",\"heroine\",\"superpowers\"],char:\"🦸‍♀️\",fitzpatrick_scale:true,category:\"people\"},man_superhero:{keywords:[\"man\",\"male\",\"good\",\"hero\",\"superpowers\"],char:\"🦸‍♂️\",fitzpatrick_scale:true,category:\"people\"},woman_supervillain:{keywords:[\"woman\",\"female\",\"evil\",\"bad\",\"criminal\",\"heroine\",\"superpowers\"],char:\"🦹‍♀️\",fitzpatrick_scale:true,category:\"people\"},man_supervillain:{keywords:[\"man\",\"male\",\"evil\",\"bad\",\"criminal\",\"hero\",\"superpowers\"],char:\"🦹‍♂️\",fitzpatrick_scale:true,category:\"people\"},mrs_claus:{keywords:[\"woman\",\"female\",\"xmas\",\"mother christmas\"],char:\"🤶\",fitzpatrick_scale:true,category:\"people\"},santa:{keywords:[\"festival\",\"man\",\"male\",\"xmas\",\"father christmas\"],char:\"🎅\",fitzpatrick_scale:true,category:\"people\"},sorceress:{keywords:[\"woman\",\"female\",\"mage\",\"witch\"],char:\"🧙‍♀️\",fitzpatrick_scale:true,category:\"people\"},wizard:{keywords:[\"man\",\"male\",\"mage\",\"sorcerer\"],char:\"🧙‍♂️\",fitzpatrick_scale:true,category:\"people\"},woman_elf:{keywords:[\"woman\",\"female\"],char:\"🧝‍♀️\",fitzpatrick_scale:true,category:\"people\"},man_elf:{keywords:[\"man\",\"male\"],char:\"🧝‍♂️\",fitzpatrick_scale:true,category:\"people\"},woman_vampire:{keywords:[\"woman\",\"female\"],char:\"🧛‍♀️\",fitzpatrick_scale:true,category:\"people\"},man_vampire:{keywords:[\"man\",\"male\",\"dracula\"],char:\"🧛‍♂️\",fitzpatrick_scale:true,category:\"people\"},woman_zombie:{keywords:[\"woman\",\"female\",\"undead\",\"walking dead\"],char:\"🧟‍♀️\",fitzpatrick_scale:false,category:\"people\"},man_zombie:{keywords:[\"man\",\"male\",\"dracula\",\"undead\",\"walking dead\"],char:\"🧟‍♂️\",fitzpatrick_scale:false,category:\"people\"},woman_genie:{keywords:[\"woman\",\"female\"],char:\"🧞‍♀️\",fitzpatrick_scale:false,category:\"people\"},man_genie:{keywords:[\"man\",\"male\"],char:\"🧞‍♂️\",fitzpatrick_scale:false,category:\"people\"},mermaid:{keywords:[\"woman\",\"female\",\"merwoman\",\"ariel\"],char:\"🧜‍♀️\",fitzpatrick_scale:true,category:\"people\"},merman:{keywords:[\"man\",\"male\",\"triton\"],char:\"🧜‍♂️\",fitzpatrick_scale:true,category:\"people\"},woman_fairy:{keywords:[\"woman\",\"female\"],char:\"🧚‍♀️\",fitzpatrick_scale:true,category:\"people\"},man_fairy:{keywords:[\"man\",\"male\"],char:\"🧚‍♂️\",fitzpatrick_scale:true,category:\"people\"},angel:{keywords:[\"heaven\",\"wings\",\"halo\"],char:\"👼\",fitzpatrick_scale:true,category:\"people\"},pregnant_woman:{keywords:[\"baby\"],char:\"🤰\",fitzpatrick_scale:true,category:\"people\"},breastfeeding:{keywords:[\"nursing\",\"baby\"],char:\"🤱\",fitzpatrick_scale:true,category:\"people\"},princess:{keywords:[\"girl\",\"woman\",\"female\",\"blond\",\"crown\",\"royal\",\"queen\"],char:\"👸\",fitzpatrick_scale:true,category:\"people\"},prince:{keywords:[\"boy\",\"man\",\"male\",\"crown\",\"royal\",\"king\"],char:\"🤴\",fitzpatrick_scale:true,category:\"people\"},bride_with_veil:{keywords:[\"couple\",\"marriage\",\"wedding\",\"woman\",\"bride\"],char:\"👰\",fitzpatrick_scale:true,category:\"people\"},man_in_tuxedo:{keywords:[\"couple\",\"marriage\",\"wedding\",\"groom\"],char:\"🤵\",fitzpatrick_scale:true,category:\"people\"},running_woman:{keywords:[\"woman\",\"walking\",\"exercise\",\"race\",\"running\",\"female\"],char:\"🏃‍♀️\",fitzpatrick_scale:true,category:\"people\"},running_man:{keywords:[\"man\",\"walking\",\"exercise\",\"race\",\"running\"],char:\"🏃\",fitzpatrick_scale:true,category:\"people\"},walking_woman:{keywords:[\"human\",\"feet\",\"steps\",\"woman\",\"female\"],char:\"🚶‍♀️\",fitzpatrick_scale:true,category:\"people\"},walking_man:{keywords:[\"human\",\"feet\",\"steps\"],char:\"🚶\",fitzpatrick_scale:true,category:\"people\"},dancer:{keywords:[\"female\",\"girl\",\"woman\",\"fun\"],char:\"💃\",fitzpatrick_scale:true,category:\"people\"},man_dancing:{keywords:[\"male\",\"boy\",\"fun\",\"dancer\"],char:\"🕺\",fitzpatrick_scale:true,category:\"people\"},dancing_women:{keywords:[\"female\",\"bunny\",\"women\",\"girls\"],char:\"👯\",fitzpatrick_scale:false,category:\"people\"},dancing_men:{keywords:[\"male\",\"bunny\",\"men\",\"boys\"],char:\"👯‍♂️\",fitzpatrick_scale:false,category:\"people\"},couple:{keywords:[\"pair\",\"people\",\"human\",\"love\",\"date\",\"dating\",\"like\",\"affection\",\"valentines\",\"marriage\"],char:\"👫\",fitzpatrick_scale:false,category:\"people\"},two_men_holding_hands:{keywords:[\"pair\",\"couple\",\"love\",\"like\",\"bromance\",\"friendship\",\"people\",\"human\"],char:\"👬\",fitzpatrick_scale:false,category:\"people\"},two_women_holding_hands:{keywords:[\"pair\",\"friendship\",\"couple\",\"love\",\"like\",\"female\",\"people\",\"human\"],char:\"👭\",fitzpatrick_scale:false,category:\"people\"},bowing_woman:{keywords:[\"woman\",\"female\",\"girl\"],char:\"🙇‍♀️\",fitzpatrick_scale:true,category:\"people\"},bowing_man:{keywords:[\"man\",\"male\",\"boy\"],char:\"🙇\",fitzpatrick_scale:true,category:\"people\"},man_facepalming:{keywords:[\"man\",\"male\",\"boy\",\"disbelief\"],char:\"🤦‍♂️\",fitzpatrick_scale:true,category:\"people\"},woman_facepalming:{keywords:[\"woman\",\"female\",\"girl\",\"disbelief\"],char:\"🤦‍♀️\",fitzpatrick_scale:true,category:\"people\"},woman_shrugging:{keywords:[\"woman\",\"female\",\"girl\",\"confused\",\"indifferent\",\"doubt\"],char:\"🤷\",fitzpatrick_scale:true,category:\"people\"},man_shrugging:{keywords:[\"man\",\"male\",\"boy\",\"confused\",\"indifferent\",\"doubt\"],char:\"🤷‍♂️\",fitzpatrick_scale:true,category:\"people\"},tipping_hand_woman:{keywords:[\"female\",\"girl\",\"woman\",\"human\",\"information\"],char:\"💁\",fitzpatrick_scale:true,category:\"people\"},tipping_hand_man:{keywords:[\"male\",\"boy\",\"man\",\"human\",\"information\"],char:\"💁‍♂️\",fitzpatrick_scale:true,category:\"people\"},no_good_woman:{keywords:[\"female\",\"girl\",\"woman\",\"nope\"],char:\"🙅\",fitzpatrick_scale:true,category:\"people\"},no_good_man:{keywords:[\"male\",\"boy\",\"man\",\"nope\"],char:\"🙅‍♂️\",fitzpatrick_scale:true,category:\"people\"},ok_woman:{keywords:[\"women\",\"girl\",\"female\",\"pink\",\"human\",\"woman\"],char:\"🙆\",fitzpatrick_scale:true,category:\"people\"},ok_man:{keywords:[\"men\",\"boy\",\"male\",\"blue\",\"human\",\"man\"],char:\"🙆‍♂️\",fitzpatrick_scale:true,category:\"people\"},raising_hand_woman:{keywords:[\"female\",\"girl\",\"woman\"],char:\"🙋\",fitzpatrick_scale:true,category:\"people\"},raising_hand_man:{keywords:[\"male\",\"boy\",\"man\"],char:\"🙋‍♂️\",fitzpatrick_scale:true,category:\"people\"},pouting_woman:{keywords:[\"female\",\"girl\",\"woman\"],char:\"🙎\",fitzpatrick_scale:true,category:\"people\"},pouting_man:{keywords:[\"male\",\"boy\",\"man\"],char:\"🙎‍♂️\",fitzpatrick_scale:true,category:\"people\"},frowning_woman:{keywords:[\"female\",\"girl\",\"woman\",\"sad\",\"depressed\",\"discouraged\",\"unhappy\"],char:\"🙍\",fitzpatrick_scale:true,category:\"people\"},frowning_man:{keywords:[\"male\",\"boy\",\"man\",\"sad\",\"depressed\",\"discouraged\",\"unhappy\"],char:\"🙍‍♂️\",fitzpatrick_scale:true,category:\"people\"},haircut_woman:{keywords:[\"female\",\"girl\",\"woman\"],char:\"💇\",fitzpatrick_scale:true,category:\"people\"},haircut_man:{keywords:[\"male\",\"boy\",\"man\"],char:\"💇‍♂️\",fitzpatrick_scale:true,category:\"people\"},massage_woman:{keywords:[\"female\",\"girl\",\"woman\",\"head\"],char:\"💆\",fitzpatrick_scale:true,category:\"people\"},massage_man:{keywords:[\"male\",\"boy\",\"man\",\"head\"],char:\"💆‍♂️\",fitzpatrick_scale:true,category:\"people\"},woman_in_steamy_room:{keywords:[\"female\",\"woman\",\"spa\",\"steamroom\",\"sauna\"],char:\"🧖‍♀️\",fitzpatrick_scale:true,category:\"people\"},man_in_steamy_room:{keywords:[\"male\",\"man\",\"spa\",\"steamroom\",\"sauna\"],char:\"🧖‍♂️\",fitzpatrick_scale:true,category:\"people\"},couple_with_heart_woman_man:{keywords:[\"pair\",\"love\",\"like\",\"affection\",\"human\",\"dating\",\"valentines\",\"marriage\"],char:\"💑\",fitzpatrick_scale:false,category:\"people\"},couple_with_heart_woman_woman:{keywords:[\"pair\",\"love\",\"like\",\"affection\",\"human\",\"dating\",\"valentines\",\"marriage\"],char:\"👩‍❤️‍👩\",fitzpatrick_scale:false,category:\"people\"},couple_with_heart_man_man:{keywords:[\"pair\",\"love\",\"like\",\"affection\",\"human\",\"dating\",\"valentines\",\"marriage\"],char:\"👨‍❤️‍👨\",fitzpatrick_scale:false,category:\"people\"},couplekiss_man_woman:{keywords:[\"pair\",\"valentines\",\"love\",\"like\",\"dating\",\"marriage\"],char:\"💏\",fitzpatrick_scale:false,category:\"people\"},couplekiss_woman_woman:{keywords:[\"pair\",\"valentines\",\"love\",\"like\",\"dating\",\"marriage\"],char:\"👩‍❤️‍💋‍👩\",fitzpatrick_scale:false,category:\"people\"},couplekiss_man_man:{keywords:[\"pair\",\"valentines\",\"love\",\"like\",\"dating\",\"marriage\"],char:\"👨‍❤️‍💋‍👨\",fitzpatrick_scale:false,category:\"people\"},family_man_woman_boy:{keywords:[\"home\",\"parents\",\"child\",\"mom\",\"dad\",\"father\",\"mother\",\"people\",\"human\"],char:\"👪\",fitzpatrick_scale:false,category:\"people\"},family_man_woman_girl:{keywords:[\"home\",\"parents\",\"people\",\"human\",\"child\"],char:\"👨‍👩‍👧\",fitzpatrick_scale:false,category:\"people\"},family_man_woman_girl_boy:{keywords:[\"home\",\"parents\",\"people\",\"human\",\"children\"],char:\"👨‍👩‍👧‍👦\",fitzpatrick_scale:false,category:\"people\"},family_man_woman_boy_boy:{keywords:[\"home\",\"parents\",\"people\",\"human\",\"children\"],char:\"👨‍👩‍👦‍👦\",fitzpatrick_scale:false,category:\"people\"},family_man_woman_girl_girl:{keywords:[\"home\",\"parents\",\"people\",\"human\",\"children\"],char:\"👨‍👩‍👧‍👧\",fitzpatrick_scale:false,category:\"people\"},family_woman_woman_boy:{keywords:[\"home\",\"parents\",\"people\",\"human\",\"children\"],char:\"👩‍👩‍👦\",fitzpatrick_scale:false,category:\"people\"},family_woman_woman_girl:{keywords:[\"home\",\"parents\",\"people\",\"human\",\"children\"],char:\"👩‍👩‍👧\",fitzpatrick_scale:false,category:\"people\"},family_woman_woman_girl_boy:{keywords:[\"home\",\"parents\",\"people\",\"human\",\"children\"],char:\"👩‍👩‍👧‍👦\",fitzpatrick_scale:false,category:\"people\"},family_woman_woman_boy_boy:{keywords:[\"home\",\"parents\",\"people\",\"human\",\"children\"],char:\"👩‍👩‍👦‍👦\",fitzpatrick_scale:false,category:\"people\"},family_woman_woman_girl_girl:{keywords:[\"home\",\"parents\",\"people\",\"human\",\"children\"],char:\"👩‍👩‍👧‍👧\",fitzpatrick_scale:false,category:\"people\"},family_man_man_boy:{keywords:[\"home\",\"parents\",\"people\",\"human\",\"children\"],char:\"👨‍👨‍👦\",fitzpatrick_scale:false,category:\"people\"},family_man_man_girl:{keywords:[\"home\",\"parents\",\"people\",\"human\",\"children\"],char:\"👨‍👨‍👧\",fitzpatrick_scale:false,category:\"people\"},family_man_man_girl_boy:{keywords:[\"home\",\"parents\",\"people\",\"human\",\"children\"],char:\"👨‍👨‍👧‍👦\",fitzpatrick_scale:false,category:\"people\"},family_man_man_boy_boy:{keywords:[\"home\",\"parents\",\"people\",\"human\",\"children\"],char:\"👨‍👨‍👦‍👦\",fitzpatrick_scale:false,category:\"people\"},family_man_man_girl_girl:{keywords:[\"home\",\"parents\",\"people\",\"human\",\"children\"],char:\"👨‍👨‍👧‍👧\",fitzpatrick_scale:false,category:\"people\"},family_woman_boy:{keywords:[\"home\",\"parent\",\"people\",\"human\",\"child\"],char:\"👩‍👦\",fitzpatrick_scale:false,category:\"people\"},family_woman_girl:{keywords:[\"home\",\"parent\",\"people\",\"human\",\"child\"],char:\"👩‍👧\",fitzpatrick_scale:false,category:\"people\"},family_woman_girl_boy:{keywords:[\"home\",\"parent\",\"people\",\"human\",\"children\"],char:\"👩‍👧‍👦\",fitzpatrick_scale:false,category:\"people\"},family_woman_boy_boy:{keywords:[\"home\",\"parent\",\"people\",\"human\",\"children\"],char:\"👩‍👦‍👦\",fitzpatrick_scale:false,category:\"people\"},family_woman_girl_girl:{keywords:[\"home\",\"parent\",\"people\",\"human\",\"children\"],char:\"👩‍👧‍👧\",fitzpatrick_scale:false,category:\"people\"},family_man_boy:{keywords:[\"home\",\"parent\",\"people\",\"human\",\"child\"],char:\"👨‍👦\",fitzpatrick_scale:false,category:\"people\"},family_man_girl:{keywords:[\"home\",\"parent\",\"people\",\"human\",\"child\"],char:\"👨‍👧\",fitzpatrick_scale:false,category:\"people\"},family_man_girl_boy:{keywords:[\"home\",\"parent\",\"people\",\"human\",\"children\"],char:\"👨‍👧‍👦\",fitzpatrick_scale:false,category:\"people\"},family_man_boy_boy:{keywords:[\"home\",\"parent\",\"people\",\"human\",\"children\"],char:\"👨‍👦‍👦\",fitzpatrick_scale:false,category:\"people\"},family_man_girl_girl:{keywords:[\"home\",\"parent\",\"people\",\"human\",\"children\"],char:\"👨‍👧‍👧\",fitzpatrick_scale:false,category:\"people\"},yarn:{keywords:[\"ball\",\"crochet\",\"knit\"],char:\"🧶\",fitzpatrick_scale:false,category:\"people\"},thread:{keywords:[\"needle\",\"sewing\",\"spool\",\"string\"],char:\"🧵\",fitzpatrick_scale:false,category:\"people\"},coat:{keywords:[\"jacket\"],char:\"🧥\",fitzpatrick_scale:false,category:\"people\"},labcoat:{keywords:[\"doctor\",\"experiment\",\"scientist\",\"chemist\"],char:\"🥼\",fitzpatrick_scale:false,category:\"people\"},womans_clothes:{keywords:[\"fashion\",\"shopping_bags\",\"female\"],char:\"👚\",fitzpatrick_scale:false,category:\"people\"},tshirt:{keywords:[\"fashion\",\"cloth\",\"casual\",\"shirt\",\"tee\"],char:\"👕\",fitzpatrick_scale:false,category:\"people\"},jeans:{keywords:[\"fashion\",\"shopping\"],char:\"👖\",fitzpatrick_scale:false,category:\"people\"},necktie:{keywords:[\"shirt\",\"suitup\",\"formal\",\"fashion\",\"cloth\",\"business\"],char:\"👔\",fitzpatrick_scale:false,category:\"people\"},dress:{keywords:[\"clothes\",\"fashion\",\"shopping\"],char:\"👗\",fitzpatrick_scale:false,category:\"people\"},bikini:{keywords:[\"swimming\",\"female\",\"woman\",\"girl\",\"fashion\",\"beach\",\"summer\"],char:\"👙\",fitzpatrick_scale:false,category:\"people\"},kimono:{keywords:[\"dress\",\"fashion\",\"women\",\"female\",\"japanese\"],char:\"👘\",fitzpatrick_scale:false,category:\"people\"},lipstick:{keywords:[\"female\",\"girl\",\"fashion\",\"woman\"],char:\"💄\",fitzpatrick_scale:false,category:\"people\"},kiss:{keywords:[\"face\",\"lips\",\"love\",\"like\",\"affection\",\"valentines\"],char:\"💋\",fitzpatrick_scale:false,category:\"people\"},footprints:{keywords:[\"feet\",\"tracking\",\"walking\",\"beach\"],char:\"👣\",fitzpatrick_scale:false,category:\"people\"},flat_shoe:{keywords:[\"ballet\",\"slip-on\",\"slipper\"],char:\"🥿\",fitzpatrick_scale:false,category:\"people\"},high_heel:{keywords:[\"fashion\",\"shoes\",\"female\",\"pumps\",\"stiletto\"],char:\"👠\",fitzpatrick_scale:false,category:\"people\"},sandal:{keywords:[\"shoes\",\"fashion\",\"flip flops\"],char:\"👡\",fitzpatrick_scale:false,category:\"people\"},boot:{keywords:[\"shoes\",\"fashion\"],char:\"👢\",fitzpatrick_scale:false,category:\"people\"},mans_shoe:{keywords:[\"fashion\",\"male\"],char:\"👞\",fitzpatrick_scale:false,category:\"people\"},athletic_shoe:{keywords:[\"shoes\",\"sports\",\"sneakers\"],char:\"👟\",fitzpatrick_scale:false,category:\"people\"},hiking_boot:{keywords:[\"backpacking\",\"camping\",\"hiking\"],char:\"🥾\",fitzpatrick_scale:false,category:\"people\"},socks:{keywords:[\"stockings\",\"clothes\"],char:\"🧦\",fitzpatrick_scale:false,category:\"people\"},gloves:{keywords:[\"hands\",\"winter\",\"clothes\"],char:\"🧤\",fitzpatrick_scale:false,category:\"people\"},scarf:{keywords:[\"neck\",\"winter\",\"clothes\"],char:\"🧣\",fitzpatrick_scale:false,category:\"people\"},womans_hat:{keywords:[\"fashion\",\"accessories\",\"female\",\"lady\",\"spring\"],char:\"👒\",fitzpatrick_scale:false,category:\"people\"},tophat:{keywords:[\"magic\",\"gentleman\",\"classy\",\"circus\"],char:\"🎩\",fitzpatrick_scale:false,category:\"people\"},billed_hat:{keywords:[\"cap\",\"baseball\"],char:\"🧢\",fitzpatrick_scale:false,category:\"people\"},rescue_worker_helmet:{keywords:[\"construction\",\"build\"],char:\"⛑\",fitzpatrick_scale:false,category:\"people\"},mortar_board:{keywords:[\"school\",\"college\",\"degree\",\"university\",\"graduation\",\"cap\",\"hat\",\"legal\",\"learn\",\"education\"],char:\"🎓\",fitzpatrick_scale:false,category:\"people\"},crown:{keywords:[\"king\",\"kod\",\"leader\",\"royalty\",\"lord\"],char:\"👑\",fitzpatrick_scale:false,category:\"people\"},school_satchel:{keywords:[\"student\",\"education\",\"bag\",\"backpack\"],char:\"🎒\",fitzpatrick_scale:false,category:\"people\"},luggage:{keywords:[\"packing\",\"travel\"],char:\"🧳\",fitzpatrick_scale:false,category:\"people\"},pouch:{keywords:[\"bag\",\"accessories\",\"shopping\"],char:\"👝\",fitzpatrick_scale:false,category:\"people\"},purse:{keywords:[\"fashion\",\"accessories\",\"money\",\"sales\",\"shopping\"],char:\"👛\",fitzpatrick_scale:false,category:\"people\"},handbag:{keywords:[\"fashion\",\"accessory\",\"accessories\",\"shopping\"],char:\"👜\",fitzpatrick_scale:false,category:\"people\"},briefcase:{keywords:[\"business\",\"documents\",\"work\",\"law\",\"legal\",\"job\",\"career\"],char:\"💼\",fitzpatrick_scale:false,category:\"people\"},eyeglasses:{keywords:[\"fashion\",\"accessories\",\"eyesight\",\"nerdy\",\"dork\",\"geek\"],char:\"👓\",fitzpatrick_scale:false,category:\"people\"},dark_sunglasses:{keywords:[\"face\",\"cool\",\"accessories\"],char:\"🕶\",fitzpatrick_scale:false,category:\"people\"},goggles:{keywords:[\"eyes\",\"protection\",\"safety\"],char:\"🥽\",fitzpatrick_scale:false,category:\"people\"},ring:{keywords:[\"wedding\",\"propose\",\"marriage\",\"valentines\",\"diamond\",\"fashion\",\"jewelry\",\"gem\",\"engagement\"],char:\"💍\",fitzpatrick_scale:false,category:\"people\"},closed_umbrella:{keywords:[\"weather\",\"rain\",\"drizzle\"],char:\"🌂\",fitzpatrick_scale:false,category:\"people\"},dog:{keywords:[\"animal\",\"friend\",\"nature\",\"woof\",\"puppy\",\"pet\",\"faithful\"],char:\"🐶\",fitzpatrick_scale:false,category:\"animals_and_nature\"},cat:{keywords:[\"animal\",\"meow\",\"nature\",\"pet\",\"kitten\"],char:\"🐱\",fitzpatrick_scale:false,category:\"animals_and_nature\"},mouse:{keywords:[\"animal\",\"nature\",\"cheese_wedge\",\"rodent\"],char:\"🐭\",fitzpatrick_scale:false,category:\"animals_and_nature\"},hamster:{keywords:[\"animal\",\"nature\"],char:\"🐹\",fitzpatrick_scale:false,category:\"animals_and_nature\"},rabbit:{keywords:[\"animal\",\"nature\",\"pet\",\"spring\",\"magic\",\"bunny\"],char:\"🐰\",fitzpatrick_scale:false,category:\"animals_and_nature\"},fox_face:{keywords:[\"animal\",\"nature\",\"face\"],char:\"🦊\",fitzpatrick_scale:false,category:\"animals_and_nature\"},bear:{keywords:[\"animal\",\"nature\",\"wild\"],char:\"🐻\",fitzpatrick_scale:false,category:\"animals_and_nature\"},panda_face:{keywords:[\"animal\",\"nature\",\"panda\"],char:\"🐼\",fitzpatrick_scale:false,category:\"animals_and_nature\"},koala:{keywords:[\"animal\",\"nature\"],char:\"🐨\",fitzpatrick_scale:false,category:\"animals_and_nature\"},tiger:{keywords:[\"animal\",\"cat\",\"danger\",\"wild\",\"nature\",\"roar\"],char:\"🐯\",fitzpatrick_scale:false,category:\"animals_and_nature\"},lion:{keywords:[\"animal\",\"nature\"],char:\"🦁\",fitzpatrick_scale:false,category:\"animals_and_nature\"},cow:{keywords:[\"beef\",\"ox\",\"animal\",\"nature\",\"moo\",\"milk\"],char:\"🐮\",fitzpatrick_scale:false,category:\"animals_and_nature\"},pig:{keywords:[\"animal\",\"oink\",\"nature\"],char:\"🐷\",fitzpatrick_scale:false,category:\"animals_and_nature\"},pig_nose:{keywords:[\"animal\",\"oink\"],char:\"🐽\",fitzpatrick_scale:false,category:\"animals_and_nature\"},frog:{keywords:[\"animal\",\"nature\",\"croak\",\"toad\"],char:\"🐸\",fitzpatrick_scale:false,category:\"animals_and_nature\"},squid:{keywords:[\"animal\",\"nature\",\"ocean\",\"sea\"],char:\"🦑\",fitzpatrick_scale:false,category:\"animals_and_nature\"},octopus:{keywords:[\"animal\",\"creature\",\"ocean\",\"sea\",\"nature\",\"beach\"],char:\"🐙\",fitzpatrick_scale:false,category:\"animals_and_nature\"},shrimp:{keywords:[\"animal\",\"ocean\",\"nature\",\"seafood\"],char:\"🦐\",fitzpatrick_scale:false,category:\"animals_and_nature\"},monkey_face:{keywords:[\"animal\",\"nature\",\"circus\"],char:\"🐵\",fitzpatrick_scale:false,category:\"animals_and_nature\"},gorilla:{keywords:[\"animal\",\"nature\",\"circus\"],char:\"🦍\",fitzpatrick_scale:false,category:\"animals_and_nature\"},see_no_evil:{keywords:[\"monkey\",\"animal\",\"nature\",\"haha\"],char:\"🙈\",fitzpatrick_scale:false,category:\"animals_and_nature\"},hear_no_evil:{keywords:[\"animal\",\"monkey\",\"nature\"],char:\"🙉\",fitzpatrick_scale:false,category:\"animals_and_nature\"},speak_no_evil:{keywords:[\"monkey\",\"animal\",\"nature\",\"omg\"],char:\"🙊\",fitzpatrick_scale:false,category:\"animals_and_nature\"},monkey:{keywords:[\"animal\",\"nature\",\"banana\",\"circus\"],char:\"🐒\",fitzpatrick_scale:false,category:\"animals_and_nature\"},chicken:{keywords:[\"animal\",\"cluck\",\"nature\",\"bird\"],char:\"🐔\",fitzpatrick_scale:false,category:\"animals_and_nature\"},penguin:{keywords:[\"animal\",\"nature\"],char:\"🐧\",fitzpatrick_scale:false,category:\"animals_and_nature\"},bird:{keywords:[\"animal\",\"nature\",\"fly\",\"tweet\",\"spring\"],char:\"🐦\",fitzpatrick_scale:false,category:\"animals_and_nature\"},baby_chick:{keywords:[\"animal\",\"chicken\",\"bird\"],char:\"🐤\",fitzpatrick_scale:false,category:\"animals_and_nature\"},hatching_chick:{keywords:[\"animal\",\"chicken\",\"egg\",\"born\",\"baby\",\"bird\"],char:\"🐣\",fitzpatrick_scale:false,category:\"animals_and_nature\"},hatched_chick:{keywords:[\"animal\",\"chicken\",\"baby\",\"bird\"],char:\"🐥\",fitzpatrick_scale:false,category:\"animals_and_nature\"},duck:{keywords:[\"animal\",\"nature\",\"bird\",\"mallard\"],char:\"🦆\",fitzpatrick_scale:false,category:\"animals_and_nature\"},eagle:{keywords:[\"animal\",\"nature\",\"bird\"],char:\"🦅\",fitzpatrick_scale:false,category:\"animals_and_nature\"},owl:{keywords:[\"animal\",\"nature\",\"bird\",\"hoot\"],char:\"🦉\",fitzpatrick_scale:false,category:\"animals_and_nature\"},bat:{keywords:[\"animal\",\"nature\",\"blind\",\"vampire\"],char:\"🦇\",fitzpatrick_scale:false,category:\"animals_and_nature\"},wolf:{keywords:[\"animal\",\"nature\",\"wild\"],char:\"🐺\",fitzpatrick_scale:false,category:\"animals_and_nature\"},boar:{keywords:[\"animal\",\"nature\"],char:\"🐗\",fitzpatrick_scale:false,category:\"animals_and_nature\"},horse:{keywords:[\"animal\",\"brown\",\"nature\"],char:\"🐴\",fitzpatrick_scale:false,category:\"animals_and_nature\"},unicorn:{keywords:[\"animal\",\"nature\",\"mystical\"],char:\"🦄\",fitzpatrick_scale:false,category:\"animals_and_nature\"},honeybee:{keywords:[\"animal\",\"insect\",\"nature\",\"bug\",\"spring\",\"honey\"],char:\"🐝\",fitzpatrick_scale:false,category:\"animals_and_nature\"},bug:{keywords:[\"animal\",\"insect\",\"nature\",\"worm\"],char:\"🐛\",fitzpatrick_scale:false,category:\"animals_and_nature\"},butterfly:{keywords:[\"animal\",\"insect\",\"nature\",\"caterpillar\"],char:\"🦋\",fitzpatrick_scale:false,category:\"animals_and_nature\"},snail:{keywords:[\"slow\",\"animal\",\"shell\"],char:\"🐌\",fitzpatrick_scale:false,category:\"animals_and_nature\"},beetle:{keywords:[\"animal\",\"insect\",\"nature\",\"ladybug\"],char:\"🐞\",fitzpatrick_scale:false,category:\"animals_and_nature\"},ant:{keywords:[\"animal\",\"insect\",\"nature\",\"bug\"],char:\"🐜\",fitzpatrick_scale:false,category:\"animals_and_nature\"},grasshopper:{keywords:[\"animal\",\"cricket\",\"chirp\"],char:\"🦗\",fitzpatrick_scale:false,category:\"animals_and_nature\"},spider:{keywords:[\"animal\",\"arachnid\"],char:\"🕷\",fitzpatrick_scale:false,category:\"animals_and_nature\"},scorpion:{keywords:[\"animal\",\"arachnid\"],char:\"🦂\",fitzpatrick_scale:false,category:\"animals_and_nature\"},crab:{keywords:[\"animal\",\"crustacean\"],char:\"🦀\",fitzpatrick_scale:false,category:\"animals_and_nature\"},snake:{keywords:[\"animal\",\"evil\",\"nature\",\"hiss\",\"python\"],char:\"🐍\",fitzpatrick_scale:false,category:\"animals_and_nature\"},lizard:{keywords:[\"animal\",\"nature\",\"reptile\"],char:\"🦎\",fitzpatrick_scale:false,category:\"animals_and_nature\"},\"t-rex\":{keywords:[\"animal\",\"nature\",\"dinosaur\",\"tyrannosaurus\",\"extinct\"],char:\"🦖\",fitzpatrick_scale:false,category:\"animals_and_nature\"},sauropod:{keywords:[\"animal\",\"nature\",\"dinosaur\",\"brachiosaurus\",\"brontosaurus\",\"diplodocus\",\"extinct\"],char:\"🦕\",fitzpatrick_scale:false,category:\"animals_and_nature\"},turtle:{keywords:[\"animal\",\"slow\",\"nature\",\"tortoise\"],char:\"🐢\",fitzpatrick_scale:false,category:\"animals_and_nature\"},tropical_fish:{keywords:[\"animal\",\"swim\",\"ocean\",\"beach\",\"nemo\"],char:\"🐠\",fitzpatrick_scale:false,category:\"animals_and_nature\"},fish:{keywords:[\"animal\",\"food\",\"nature\"],char:\"🐟\",fitzpatrick_scale:false,category:\"animals_and_nature\"},blowfish:{keywords:[\"animal\",\"nature\",\"food\",\"sea\",\"ocean\"],char:\"🐡\",fitzpatrick_scale:false,category:\"animals_and_nature\"},dolphin:{keywords:[\"animal\",\"nature\",\"fish\",\"sea\",\"ocean\",\"flipper\",\"fins\",\"beach\"],char:\"🐬\",fitzpatrick_scale:false,category:\"animals_and_nature\"},shark:{keywords:[\"animal\",\"nature\",\"fish\",\"sea\",\"ocean\",\"jaws\",\"fins\",\"beach\"],char:\"🦈\",fitzpatrick_scale:false,category:\"animals_and_nature\"},whale:{keywords:[\"animal\",\"nature\",\"sea\",\"ocean\"],char:\"🐳\",fitzpatrick_scale:false,category:\"animals_and_nature\"},whale2:{keywords:[\"animal\",\"nature\",\"sea\",\"ocean\"],char:\"🐋\",fitzpatrick_scale:false,category:\"animals_and_nature\"},crocodile:{keywords:[\"animal\",\"nature\",\"reptile\",\"lizard\",\"alligator\"],char:\"🐊\",fitzpatrick_scale:false,category:\"animals_and_nature\"},leopard:{keywords:[\"animal\",\"nature\"],char:\"🐆\",fitzpatrick_scale:false,category:\"animals_and_nature\"},zebra:{keywords:[\"animal\",\"nature\",\"stripes\",\"safari\"],char:\"🦓\",fitzpatrick_scale:false,category:\"animals_and_nature\"},tiger2:{keywords:[\"animal\",\"nature\",\"roar\"],char:\"🐅\",fitzpatrick_scale:false,category:\"animals_and_nature\"},water_buffalo:{keywords:[\"animal\",\"nature\",\"ox\",\"cow\"],char:\"🐃\",fitzpatrick_scale:false,category:\"animals_and_nature\"},ox:{keywords:[\"animal\",\"cow\",\"beef\"],char:\"🐂\",fitzpatrick_scale:false,category:\"animals_and_nature\"},cow2:{keywords:[\"beef\",\"ox\",\"animal\",\"nature\",\"moo\",\"milk\"],char:\"🐄\",fitzpatrick_scale:false,category:\"animals_and_nature\"},deer:{keywords:[\"animal\",\"nature\",\"horns\",\"venison\"],char:\"🦌\",fitzpatrick_scale:false,category:\"animals_and_nature\"},dromedary_camel:{keywords:[\"animal\",\"hot\",\"desert\",\"hump\"],char:\"🐪\",fitzpatrick_scale:false,category:\"animals_and_nature\"},camel:{keywords:[\"animal\",\"nature\",\"hot\",\"desert\",\"hump\"],char:\"🐫\",fitzpatrick_scale:false,category:\"animals_and_nature\"},giraffe:{keywords:[\"animal\",\"nature\",\"spots\",\"safari\"],char:\"🦒\",fitzpatrick_scale:false,category:\"animals_and_nature\"},elephant:{keywords:[\"animal\",\"nature\",\"nose\",\"th\",\"circus\"],char:\"🐘\",fitzpatrick_scale:false,category:\"animals_and_nature\"},rhinoceros:{keywords:[\"animal\",\"nature\",\"horn\"],char:\"🦏\",fitzpatrick_scale:false,category:\"animals_and_nature\"},goat:{keywords:[\"animal\",\"nature\"],char:\"🐐\",fitzpatrick_scale:false,category:\"animals_and_nature\"},ram:{keywords:[\"animal\",\"sheep\",\"nature\"],char:\"🐏\",fitzpatrick_scale:false,category:\"animals_and_nature\"},sheep:{keywords:[\"animal\",\"nature\",\"wool\",\"shipit\"],char:\"🐑\",fitzpatrick_scale:false,category:\"animals_and_nature\"},racehorse:{keywords:[\"animal\",\"gamble\",\"luck\"],char:\"🐎\",fitzpatrick_scale:false,category:\"animals_and_nature\"},pig2:{keywords:[\"animal\",\"nature\"],char:\"🐖\",fitzpatrick_scale:false,category:\"animals_and_nature\"},rat:{keywords:[\"animal\",\"mouse\",\"rodent\"],char:\"🐀\",fitzpatrick_scale:false,category:\"animals_and_nature\"},mouse2:{keywords:[\"animal\",\"nature\",\"rodent\"],char:\"🐁\",fitzpatrick_scale:false,category:\"animals_and_nature\"},rooster:{keywords:[\"animal\",\"nature\",\"chicken\"],char:\"🐓\",fitzpatrick_scale:false,category:\"animals_and_nature\"},turkey:{keywords:[\"animal\",\"bird\"],char:\"🦃\",fitzpatrick_scale:false,category:\"animals_and_nature\"},dove:{keywords:[\"animal\",\"bird\"],char:\"🕊\",fitzpatrick_scale:false,category:\"animals_and_nature\"},dog2:{keywords:[\"animal\",\"nature\",\"friend\",\"doge\",\"pet\",\"faithful\"],char:\"🐕\",fitzpatrick_scale:false,category:\"animals_and_nature\"},poodle:{keywords:[\"dog\",\"animal\",\"101\",\"nature\",\"pet\"],char:\"🐩\",fitzpatrick_scale:false,category:\"animals_and_nature\"},cat2:{keywords:[\"animal\",\"meow\",\"pet\",\"cats\"],char:\"🐈\",fitzpatrick_scale:false,category:\"animals_and_nature\"},rabbit2:{keywords:[\"animal\",\"nature\",\"pet\",\"magic\",\"spring\"],char:\"🐇\",fitzpatrick_scale:false,category:\"animals_and_nature\"},chipmunk:{keywords:[\"animal\",\"nature\",\"rodent\",\"squirrel\"],char:\"🐿\",fitzpatrick_scale:false,category:\"animals_and_nature\"},hedgehog:{keywords:[\"animal\",\"nature\",\"spiny\"],char:\"🦔\",fitzpatrick_scale:false,category:\"animals_and_nature\"},raccoon:{keywords:[\"animal\",\"nature\"],char:\"🦝\",fitzpatrick_scale:false,category:\"animals_and_nature\"},llama:{keywords:[\"animal\",\"nature\",\"alpaca\"],char:\"🦙\",fitzpatrick_scale:false,category:\"animals_and_nature\"},hippopotamus:{keywords:[\"animal\",\"nature\"],char:\"🦛\",fitzpatrick_scale:false,category:\"animals_and_nature\"},kangaroo:{keywords:[\"animal\",\"nature\",\"australia\",\"joey\",\"hop\",\"marsupial\"],char:\"🦘\",fitzpatrick_scale:false,category:\"animals_and_nature\"},badger:{keywords:[\"animal\",\"nature\",\"honey\"],char:\"🦡\",fitzpatrick_scale:false,category:\"animals_and_nature\"},swan:{keywords:[\"animal\",\"nature\",\"bird\"],char:\"🦢\",fitzpatrick_scale:false,category:\"animals_and_nature\"},peacock:{keywords:[\"animal\",\"nature\",\"peahen\",\"bird\"],char:\"🦚\",fitzpatrick_scale:false,category:\"animals_and_nature\"},parrot:{keywords:[\"animal\",\"nature\",\"bird\",\"pirate\",\"talk\"],char:\"🦜\",fitzpatrick_scale:false,category:\"animals_and_nature\"},lobster:{keywords:[\"animal\",\"nature\",\"bisque\",\"claws\",\"seafood\"],char:\"🦞\",fitzpatrick_scale:false,category:\"animals_and_nature\"},mosquito:{keywords:[\"animal\",\"nature\",\"insect\",\"malaria\"],char:\"🦟\",fitzpatrick_scale:false,category:\"animals_and_nature\"},paw_prints:{keywords:[\"animal\",\"tracking\",\"footprints\",\"dog\",\"cat\",\"pet\",\"feet\"],char:\"🐾\",fitzpatrick_scale:false,category:\"animals_and_nature\"},dragon:{keywords:[\"animal\",\"myth\",\"nature\",\"chinese\",\"green\"],char:\"🐉\",fitzpatrick_scale:false,category:\"animals_and_nature\"},dragon_face:{keywords:[\"animal\",\"myth\",\"nature\",\"chinese\",\"green\"],char:\"🐲\",fitzpatrick_scale:false,category:\"animals_and_nature\"},cactus:{keywords:[\"vegetable\",\"plant\",\"nature\"],char:\"🌵\",fitzpatrick_scale:false,category:\"animals_and_nature\"},christmas_tree:{keywords:[\"festival\",\"vacation\",\"december\",\"xmas\",\"celebration\"],char:\"🎄\",fitzpatrick_scale:false,category:\"animals_and_nature\"},evergreen_tree:{keywords:[\"plant\",\"nature\"],char:\"🌲\",fitzpatrick_scale:false,category:\"animals_and_nature\"},deciduous_tree:{keywords:[\"plant\",\"nature\"],char:\"🌳\",fitzpatrick_scale:false,category:\"animals_and_nature\"},palm_tree:{keywords:[\"plant\",\"vegetable\",\"nature\",\"summer\",\"beach\",\"mojito\",\"tropical\"],char:\"🌴\",fitzpatrick_scale:false,category:\"animals_and_nature\"},seedling:{keywords:[\"plant\",\"nature\",\"grass\",\"lawn\",\"spring\"],char:\"🌱\",fitzpatrick_scale:false,category:\"animals_and_nature\"},herb:{keywords:[\"vegetable\",\"plant\",\"medicine\",\"weed\",\"grass\",\"lawn\"],char:\"🌿\",fitzpatrick_scale:false,category:\"animals_and_nature\"},shamrock:{keywords:[\"vegetable\",\"plant\",\"nature\",\"irish\",\"clover\"],char:\"☘\",fitzpatrick_scale:false,category:\"animals_and_nature\"},four_leaf_clover:{keywords:[\"vegetable\",\"plant\",\"nature\",\"lucky\",\"irish\"],char:\"🍀\",fitzpatrick_scale:false,category:\"animals_and_nature\"},bamboo:{keywords:[\"plant\",\"nature\",\"vegetable\",\"panda\",\"pine_decoration\"],char:\"🎍\",fitzpatrick_scale:false,category:\"animals_and_nature\"},tanabata_tree:{keywords:[\"plant\",\"nature\",\"branch\",\"summer\"],char:\"🎋\",fitzpatrick_scale:false,category:\"animals_and_nature\"},leaves:{keywords:[\"nature\",\"plant\",\"tree\",\"vegetable\",\"grass\",\"lawn\",\"spring\"],char:\"🍃\",fitzpatrick_scale:false,category:\"animals_and_nature\"},fallen_leaf:{keywords:[\"nature\",\"plant\",\"vegetable\",\"leaves\"],char:\"🍂\",fitzpatrick_scale:false,category:\"animals_and_nature\"},maple_leaf:{keywords:[\"nature\",\"plant\",\"vegetable\",\"ca\",\"fall\"],char:\"🍁\",fitzpatrick_scale:false,category:\"animals_and_nature\"},ear_of_rice:{keywords:[\"nature\",\"plant\"],char:\"🌾\",fitzpatrick_scale:false,category:\"animals_and_nature\"},hibiscus:{keywords:[\"plant\",\"vegetable\",\"flowers\",\"beach\"],char:\"🌺\",fitzpatrick_scale:false,category:\"animals_and_nature\"},sunflower:{keywords:[\"nature\",\"plant\",\"fall\"],char:\"🌻\",fitzpatrick_scale:false,category:\"animals_and_nature\"},rose:{keywords:[\"flowers\",\"valentines\",\"love\",\"spring\"],char:\"🌹\",fitzpatrick_scale:false,category:\"animals_and_nature\"},wilted_flower:{keywords:[\"plant\",\"nature\",\"flower\"],char:\"🥀\",fitzpatrick_scale:false,category:\"animals_and_nature\"},tulip:{keywords:[\"flowers\",\"plant\",\"nature\",\"summer\",\"spring\"],char:\"🌷\",fitzpatrick_scale:false,category:\"animals_and_nature\"},blossom:{keywords:[\"nature\",\"flowers\",\"yellow\"],char:\"🌼\",fitzpatrick_scale:false,category:\"animals_and_nature\"},cherry_blossom:{keywords:[\"nature\",\"plant\",\"spring\",\"flower\"],char:\"🌸\",fitzpatrick_scale:false,category:\"animals_and_nature\"},bouquet:{keywords:[\"flowers\",\"nature\",\"spring\"],char:\"💐\",fitzpatrick_scale:false,category:\"animals_and_nature\"},mushroom:{keywords:[\"plant\",\"vegetable\"],char:\"🍄\",fitzpatrick_scale:false,category:\"animals_and_nature\"},chestnut:{keywords:[\"food\",\"squirrel\"],char:\"🌰\",fitzpatrick_scale:false,category:\"animals_and_nature\"},jack_o_lantern:{keywords:[\"halloween\",\"light\",\"pumpkin\",\"creepy\",\"fall\"],char:\"🎃\",fitzpatrick_scale:false,category:\"animals_and_nature\"},shell:{keywords:[\"nature\",\"sea\",\"beach\"],char:\"🐚\",fitzpatrick_scale:false,category:\"animals_and_nature\"},spider_web:{keywords:[\"animal\",\"insect\",\"arachnid\",\"silk\"],char:\"🕸\",fitzpatrick_scale:false,category:\"animals_and_nature\"},earth_americas:{keywords:[\"globe\",\"world\",\"USA\",\"international\"],char:\"🌎\",fitzpatrick_scale:false,category:\"animals_and_nature\"},earth_africa:{keywords:[\"globe\",\"world\",\"international\"],char:\"🌍\",fitzpatrick_scale:false,category:\"animals_and_nature\"},earth_asia:{keywords:[\"globe\",\"world\",\"east\",\"international\"],char:\"🌏\",fitzpatrick_scale:false,category:\"animals_and_nature\"},full_moon:{keywords:[\"nature\",\"yellow\",\"twilight\",\"planet\",\"space\",\"night\",\"evening\",\"sleep\"],char:\"🌕\",fitzpatrick_scale:false,category:\"animals_and_nature\"},waning_gibbous_moon:{keywords:[\"nature\",\"twilight\",\"planet\",\"space\",\"night\",\"evening\",\"sleep\",\"waxing_gibbous_moon\"],char:\"🌖\",fitzpatrick_scale:false,category:\"animals_and_nature\"},last_quarter_moon:{keywords:[\"nature\",\"twilight\",\"planet\",\"space\",\"night\",\"evening\",\"sleep\"],char:\"🌗\",fitzpatrick_scale:false,category:\"animals_and_nature\"},waning_crescent_moon:{keywords:[\"nature\",\"twilight\",\"planet\",\"space\",\"night\",\"evening\",\"sleep\"],char:\"🌘\",fitzpatrick_scale:false,category:\"animals_and_nature\"},new_moon:{keywords:[\"nature\",\"twilight\",\"planet\",\"space\",\"night\",\"evening\",\"sleep\"],char:\"🌑\",fitzpatrick_scale:false,category:\"animals_and_nature\"},waxing_crescent_moon:{keywords:[\"nature\",\"twilight\",\"planet\",\"space\",\"night\",\"evening\",\"sleep\"],char:\"🌒\",fitzpatrick_scale:false,category:\"animals_and_nature\"},first_quarter_moon:{keywords:[\"nature\",\"twilight\",\"planet\",\"space\",\"night\",\"evening\",\"sleep\"],char:\"🌓\",fitzpatrick_scale:false,category:\"animals_and_nature\"},waxing_gibbous_moon:{keywords:[\"nature\",\"night\",\"sky\",\"gray\",\"twilight\",\"planet\",\"space\",\"evening\",\"sleep\"],char:\"🌔\",fitzpatrick_scale:false,category:\"animals_and_nature\"},new_moon_with_face:{keywords:[\"nature\",\"twilight\",\"planet\",\"space\",\"night\",\"evening\",\"sleep\"],char:\"🌚\",fitzpatrick_scale:false,category:\"animals_and_nature\"},full_moon_with_face:{keywords:[\"nature\",\"twilight\",\"planet\",\"space\",\"night\",\"evening\",\"sleep\"],char:\"🌝\",fitzpatrick_scale:false,category:\"animals_and_nature\"},first_quarter_moon_with_face:{keywords:[\"nature\",\"twilight\",\"planet\",\"space\",\"night\",\"evening\",\"sleep\"],char:\"🌛\",fitzpatrick_scale:false,category:\"animals_and_nature\"},last_quarter_moon_with_face:{keywords:[\"nature\",\"twilight\",\"planet\",\"space\",\"night\",\"evening\",\"sleep\"],char:\"🌜\",fitzpatrick_scale:false,category:\"animals_and_nature\"},sun_with_face:{keywords:[\"nature\",\"morning\",\"sky\"],char:\"🌞\",fitzpatrick_scale:false,category:\"animals_and_nature\"},crescent_moon:{keywords:[\"night\",\"sleep\",\"sky\",\"evening\",\"magic\"],char:\"🌙\",fitzpatrick_scale:false,category:\"animals_and_nature\"},star:{keywords:[\"night\",\"yellow\"],char:\"⭐\",fitzpatrick_scale:false,category:\"animals_and_nature\"},star2:{keywords:[\"night\",\"sparkle\",\"awesome\",\"good\",\"magic\"],char:\"🌟\",fitzpatrick_scale:false,category:\"animals_and_nature\"},dizzy:{keywords:[\"star\",\"sparkle\",\"shoot\",\"magic\"],char:\"💫\",fitzpatrick_scale:false,category:\"animals_and_nature\"},sparkles:{keywords:[\"stars\",\"shine\",\"shiny\",\"cool\",\"awesome\",\"good\",\"magic\"],char:\"✨\",fitzpatrick_scale:false,category:\"animals_and_nature\"},comet:{keywords:[\"space\"],char:\"☄\",fitzpatrick_scale:false,category:\"animals_and_nature\"},sunny:{keywords:[\"weather\",\"nature\",\"brightness\",\"summer\",\"beach\",\"spring\"],char:\"☀️\",fitzpatrick_scale:false,category:\"animals_and_nature\"},sun_behind_small_cloud:{keywords:[\"weather\"],char:\"🌤\",fitzpatrick_scale:false,category:\"animals_and_nature\"},partly_sunny:{keywords:[\"weather\",\"nature\",\"cloudy\",\"morning\",\"fall\",\"spring\"],char:\"⛅\",fitzpatrick_scale:false,category:\"animals_and_nature\"},sun_behind_large_cloud:{keywords:[\"weather\"],char:\"🌥\",fitzpatrick_scale:false,category:\"animals_and_nature\"},sun_behind_rain_cloud:{keywords:[\"weather\"],char:\"🌦\",fitzpatrick_scale:false,category:\"animals_and_nature\"},cloud:{keywords:[\"weather\",\"sky\"],char:\"☁️\",fitzpatrick_scale:false,category:\"animals_and_nature\"},cloud_with_rain:{keywords:[\"weather\"],char:\"🌧\",fitzpatrick_scale:false,category:\"animals_and_nature\"},cloud_with_lightning_and_rain:{keywords:[\"weather\",\"lightning\"],char:\"⛈\",fitzpatrick_scale:false,category:\"animals_and_nature\"},cloud_with_lightning:{keywords:[\"weather\",\"thunder\"],char:\"🌩\",fitzpatrick_scale:false,category:\"animals_and_nature\"},zap:{keywords:[\"thunder\",\"weather\",\"lightning bolt\",\"fast\"],char:\"⚡\",fitzpatrick_scale:false,category:\"animals_and_nature\"},fire:{keywords:[\"hot\",\"cook\",\"flame\"],char:\"🔥\",fitzpatrick_scale:false,category:\"animals_and_nature\"},boom:{keywords:[\"bomb\",\"explode\",\"explosion\",\"collision\",\"blown\"],char:\"💥\",fitzpatrick_scale:false,category:\"animals_and_nature\"},snowflake:{keywords:[\"winter\",\"season\",\"cold\",\"weather\",\"christmas\",\"xmas\"],char:\"❄️\",fitzpatrick_scale:false,category:\"animals_and_nature\"},cloud_with_snow:{keywords:[\"weather\"],char:\"🌨\",fitzpatrick_scale:false,category:\"animals_and_nature\"},snowman:{keywords:[\"winter\",\"season\",\"cold\",\"weather\",\"christmas\",\"xmas\",\"frozen\",\"without_snow\"],char:\"⛄\",fitzpatrick_scale:false,category:\"animals_and_nature\"},snowman_with_snow:{keywords:[\"winter\",\"season\",\"cold\",\"weather\",\"christmas\",\"xmas\",\"frozen\"],char:\"☃\",fitzpatrick_scale:false,category:\"animals_and_nature\"},wind_face:{keywords:[\"gust\",\"air\"],char:\"🌬\",fitzpatrick_scale:false,category:\"animals_and_nature\"},dash:{keywords:[\"wind\",\"air\",\"fast\",\"shoo\",\"fart\",\"smoke\",\"puff\"],char:\"💨\",fitzpatrick_scale:false,category:\"animals_and_nature\"},tornado:{keywords:[\"weather\",\"cyclone\",\"twister\"],char:\"🌪\",fitzpatrick_scale:false,category:\"animals_and_nature\"},fog:{keywords:[\"weather\"],char:\"🌫\",fitzpatrick_scale:false,category:\"animals_and_nature\"},open_umbrella:{keywords:[\"weather\",\"spring\"],char:\"☂\",fitzpatrick_scale:false,category:\"animals_and_nature\"},umbrella:{keywords:[\"rainy\",\"weather\",\"spring\"],char:\"☔\",fitzpatrick_scale:false,category:\"animals_and_nature\"},droplet:{keywords:[\"water\",\"drip\",\"faucet\",\"spring\"],char:\"💧\",fitzpatrick_scale:false,category:\"animals_and_nature\"},sweat_drops:{keywords:[\"water\",\"drip\",\"oops\"],char:\"💦\",fitzpatrick_scale:false,category:\"animals_and_nature\"},ocean:{keywords:[\"sea\",\"water\",\"wave\",\"nature\",\"tsunami\",\"disaster\"],char:\"🌊\",fitzpatrick_scale:false,category:\"animals_and_nature\"},green_apple:{keywords:[\"fruit\",\"nature\"],char:\"🍏\",fitzpatrick_scale:false,category:\"food_and_drink\"},apple:{keywords:[\"fruit\",\"mac\",\"school\"],char:\"🍎\",fitzpatrick_scale:false,category:\"food_and_drink\"},pear:{keywords:[\"fruit\",\"nature\",\"food\"],char:\"🍐\",fitzpatrick_scale:false,category:\"food_and_drink\"},tangerine:{keywords:[\"food\",\"fruit\",\"nature\",\"orange\"],char:\"🍊\",fitzpatrick_scale:false,category:\"food_and_drink\"},lemon:{keywords:[\"fruit\",\"nature\"],char:\"🍋\",fitzpatrick_scale:false,category:\"food_and_drink\"},banana:{keywords:[\"fruit\",\"food\",\"monkey\"],char:\"🍌\",fitzpatrick_scale:false,category:\"food_and_drink\"},watermelon:{keywords:[\"fruit\",\"food\",\"picnic\",\"summer\"],char:\"🍉\",fitzpatrick_scale:false,category:\"food_and_drink\"},grapes:{keywords:[\"fruit\",\"food\",\"wine\"],char:\"🍇\",fitzpatrick_scale:false,category:\"food_and_drink\"},strawberry:{keywords:[\"fruit\",\"food\",\"nature\"],char:\"🍓\",fitzpatrick_scale:false,category:\"food_and_drink\"},melon:{keywords:[\"fruit\",\"nature\",\"food\"],char:\"🍈\",fitzpatrick_scale:false,category:\"food_and_drink\"},cherries:{keywords:[\"food\",\"fruit\"],char:\"🍒\",fitzpatrick_scale:false,category:\"food_and_drink\"},peach:{keywords:[\"fruit\",\"nature\",\"food\"],char:\"🍑\",fitzpatrick_scale:false,category:\"food_and_drink\"},pineapple:{keywords:[\"fruit\",\"nature\",\"food\"],char:\"🍍\",fitzpatrick_scale:false,category:\"food_and_drink\"},coconut:{keywords:[\"fruit\",\"nature\",\"food\",\"palm\"],char:\"🥥\",fitzpatrick_scale:false,category:\"food_and_drink\"},kiwi_fruit:{keywords:[\"fruit\",\"food\"],char:\"🥝\",fitzpatrick_scale:false,category:\"food_and_drink\"},mango:{keywords:[\"fruit\",\"food\",\"tropical\"],char:\"🥭\",fitzpatrick_scale:false,category:\"food_and_drink\"},avocado:{keywords:[\"fruit\",\"food\"],char:\"🥑\",fitzpatrick_scale:false,category:\"food_and_drink\"},broccoli:{keywords:[\"fruit\",\"food\",\"vegetable\"],char:\"🥦\",fitzpatrick_scale:false,category:\"food_and_drink\"},tomato:{keywords:[\"fruit\",\"vegetable\",\"nature\",\"food\"],char:\"🍅\",fitzpatrick_scale:false,category:\"food_and_drink\"},eggplant:{keywords:[\"vegetable\",\"nature\",\"food\",\"aubergine\"],char:\"🍆\",fitzpatrick_scale:false,category:\"food_and_drink\"},cucumber:{keywords:[\"fruit\",\"food\",\"pickle\"],char:\"🥒\",fitzpatrick_scale:false,category:\"food_and_drink\"},carrot:{keywords:[\"vegetable\",\"food\",\"orange\"],char:\"🥕\",fitzpatrick_scale:false,category:\"food_and_drink\"},hot_pepper:{keywords:[\"food\",\"spicy\",\"chilli\",\"chili\"],char:\"🌶\",fitzpatrick_scale:false,category:\"food_and_drink\"},potato:{keywords:[\"food\",\"tuber\",\"vegatable\",\"starch\"],char:\"🥔\",fitzpatrick_scale:false,category:\"food_and_drink\"},corn:{keywords:[\"food\",\"vegetable\",\"plant\"],char:\"🌽\",fitzpatrick_scale:false,category:\"food_and_drink\"},leafy_greens:{keywords:[\"food\",\"vegetable\",\"plant\",\"bok choy\",\"cabbage\",\"kale\",\"lettuce\"],char:\"🥬\",fitzpatrick_scale:false,category:\"food_and_drink\"},sweet_potato:{keywords:[\"food\",\"nature\"],char:\"🍠\",fitzpatrick_scale:false,category:\"food_and_drink\"},peanuts:{keywords:[\"food\",\"nut\"],char:\"🥜\",fitzpatrick_scale:false,category:\"food_and_drink\"},honey_pot:{keywords:[\"bees\",\"sweet\",\"kitchen\"],char:\"🍯\",fitzpatrick_scale:false,category:\"food_and_drink\"},croissant:{keywords:[\"food\",\"bread\",\"french\"],char:\"🥐\",fitzpatrick_scale:false,category:\"food_and_drink\"},bread:{keywords:[\"food\",\"wheat\",\"breakfast\",\"toast\"],char:\"🍞\",fitzpatrick_scale:false,category:\"food_and_drink\"},baguette_bread:{keywords:[\"food\",\"bread\",\"french\"],char:\"🥖\",fitzpatrick_scale:false,category:\"food_and_drink\"},bagel:{keywords:[\"food\",\"bread\",\"bakery\",\"schmear\"],char:\"🥯\",fitzpatrick_scale:false,category:\"food_and_drink\"},pretzel:{keywords:[\"food\",\"bread\",\"twisted\"],char:\"🥨\",fitzpatrick_scale:false,category:\"food_and_drink\"},cheese:{keywords:[\"food\",\"chadder\"],char:\"🧀\",fitzpatrick_scale:false,category:\"food_and_drink\"},egg:{keywords:[\"food\",\"chicken\",\"breakfast\"],char:\"🥚\",fitzpatrick_scale:false,category:\"food_and_drink\"},bacon:{keywords:[\"food\",\"breakfast\",\"pork\",\"pig\",\"meat\"],char:\"🥓\",fitzpatrick_scale:false,category:\"food_and_drink\"},steak:{keywords:[\"food\",\"cow\",\"meat\",\"cut\",\"chop\",\"lambchop\",\"porkchop\"],char:\"🥩\",fitzpatrick_scale:false,category:\"food_and_drink\"},pancakes:{keywords:[\"food\",\"breakfast\",\"flapjacks\",\"hotcakes\"],char:\"🥞\",fitzpatrick_scale:false,category:\"food_and_drink\"},poultry_leg:{keywords:[\"food\",\"meat\",\"drumstick\",\"bird\",\"chicken\",\"turkey\"],char:\"🍗\",fitzpatrick_scale:false,category:\"food_and_drink\"},meat_on_bone:{keywords:[\"good\",\"food\",\"drumstick\"],char:\"🍖\",fitzpatrick_scale:false,category:\"food_and_drink\"},bone:{keywords:[\"skeleton\"],char:\"🦴\",fitzpatrick_scale:false,category:\"food_and_drink\"},fried_shrimp:{keywords:[\"food\",\"animal\",\"appetizer\",\"summer\"],char:\"🍤\",fitzpatrick_scale:false,category:\"food_and_drink\"},fried_egg:{keywords:[\"food\",\"breakfast\",\"kitchen\",\"egg\"],char:\"🍳\",fitzpatrick_scale:false,category:\"food_and_drink\"},hamburger:{keywords:[\"meat\",\"fast food\",\"beef\",\"cheeseburger\",\"mcdonalds\",\"burger king\"],char:\"🍔\",fitzpatrick_scale:false,category:\"food_and_drink\"},fries:{keywords:[\"chips\",\"snack\",\"fast food\"],char:\"🍟\",fitzpatrick_scale:false,category:\"food_and_drink\"},stuffed_flatbread:{keywords:[\"food\",\"flatbread\",\"stuffed\",\"gyro\"],char:\"🥙\",fitzpatrick_scale:false,category:\"food_and_drink\"},hotdog:{keywords:[\"food\",\"frankfurter\"],char:\"🌭\",fitzpatrick_scale:false,category:\"food_and_drink\"},pizza:{keywords:[\"food\",\"party\"],char:\"🍕\",fitzpatrick_scale:false,category:\"food_and_drink\"},sandwich:{keywords:[\"food\",\"lunch\",\"bread\"],char:\"🥪\",fitzpatrick_scale:false,category:\"food_and_drink\"},canned_food:{keywords:[\"food\",\"soup\"],char:\"🥫\",fitzpatrick_scale:false,category:\"food_and_drink\"},spaghetti:{keywords:[\"food\",\"italian\",\"noodle\"],char:\"🍝\",fitzpatrick_scale:false,category:\"food_and_drink\"},taco:{keywords:[\"food\",\"mexican\"],char:\"🌮\",fitzpatrick_scale:false,category:\"food_and_drink\"},burrito:{keywords:[\"food\",\"mexican\"],char:\"🌯\",fitzpatrick_scale:false,category:\"food_and_drink\"},green_salad:{keywords:[\"food\",\"healthy\",\"lettuce\"],char:\"🥗\",fitzpatrick_scale:false,category:\"food_and_drink\"},shallow_pan_of_food:{keywords:[\"food\",\"cooking\",\"casserole\",\"paella\"],char:\"🥘\",fitzpatrick_scale:false,category:\"food_and_drink\"},ramen:{keywords:[\"food\",\"japanese\",\"noodle\",\"chopsticks\"],char:\"🍜\",fitzpatrick_scale:false,category:\"food_and_drink\"},stew:{keywords:[\"food\",\"meat\",\"soup\"],char:\"🍲\",fitzpatrick_scale:false,category:\"food_and_drink\"},fish_cake:{keywords:[\"food\",\"japan\",\"sea\",\"beach\",\"narutomaki\",\"pink\",\"swirl\",\"kamaboko\",\"surimi\",\"ramen\"],char:\"🍥\",fitzpatrick_scale:false,category:\"food_and_drink\"},fortune_cookie:{keywords:[\"food\",\"prophecy\"],char:\"🥠\",fitzpatrick_scale:false,category:\"food_and_drink\"},sushi:{keywords:[\"food\",\"fish\",\"japanese\",\"rice\"],char:\"🍣\",fitzpatrick_scale:false,category:\"food_and_drink\"},bento:{keywords:[\"food\",\"japanese\",\"box\"],char:\"🍱\",fitzpatrick_scale:false,category:\"food_and_drink\"},curry:{keywords:[\"food\",\"spicy\",\"hot\",\"indian\"],char:\"🍛\",fitzpatrick_scale:false,category:\"food_and_drink\"},rice_ball:{keywords:[\"food\",\"japanese\"],char:\"🍙\",fitzpatrick_scale:false,category:\"food_and_drink\"},rice:{keywords:[\"food\",\"china\",\"asian\"],char:\"🍚\",fitzpatrick_scale:false,category:\"food_and_drink\"},rice_cracker:{keywords:[\"food\",\"japanese\"],char:\"🍘\",fitzpatrick_scale:false,category:\"food_and_drink\"},oden:{keywords:[\"food\",\"japanese\"],char:\"🍢\",fitzpatrick_scale:false,category:\"food_and_drink\"},dango:{keywords:[\"food\",\"dessert\",\"sweet\",\"japanese\",\"barbecue\",\"meat\"],char:\"🍡\",fitzpatrick_scale:false,category:\"food_and_drink\"},shaved_ice:{keywords:[\"hot\",\"dessert\",\"summer\"],char:\"🍧\",fitzpatrick_scale:false,category:\"food_and_drink\"},ice_cream:{keywords:[\"food\",\"hot\",\"dessert\"],char:\"🍨\",fitzpatrick_scale:false,category:\"food_and_drink\"},icecream:{keywords:[\"food\",\"hot\",\"dessert\",\"summer\"],char:\"🍦\",fitzpatrick_scale:false,category:\"food_and_drink\"},pie:{keywords:[\"food\",\"dessert\",\"pastry\"],char:\"🥧\",fitzpatrick_scale:false,category:\"food_and_drink\"},cake:{keywords:[\"food\",\"dessert\"],char:\"🍰\",fitzpatrick_scale:false,category:\"food_and_drink\"},cupcake:{keywords:[\"food\",\"dessert\",\"bakery\",\"sweet\"],char:\"🧁\",fitzpatrick_scale:false,category:\"food_and_drink\"},moon_cake:{keywords:[\"food\",\"autumn\"],char:\"🥮\",fitzpatrick_scale:false,category:\"food_and_drink\"},birthday:{keywords:[\"food\",\"dessert\",\"cake\"],char:\"🎂\",fitzpatrick_scale:false,category:\"food_and_drink\"},custard:{keywords:[\"dessert\",\"food\"],char:\"🍮\",fitzpatrick_scale:false,category:\"food_and_drink\"},candy:{keywords:[\"snack\",\"dessert\",\"sweet\",\"lolly\"],char:\"🍬\",fitzpatrick_scale:false,category:\"food_and_drink\"},lollipop:{keywords:[\"food\",\"snack\",\"candy\",\"sweet\"],char:\"🍭\",fitzpatrick_scale:false,category:\"food_and_drink\"},chocolate_bar:{keywords:[\"food\",\"snack\",\"dessert\",\"sweet\"],char:\"🍫\",fitzpatrick_scale:false,category:\"food_and_drink\"},popcorn:{keywords:[\"food\",\"movie theater\",\"films\",\"snack\"],char:\"🍿\",fitzpatrick_scale:false,category:\"food_and_drink\"},dumpling:{keywords:[\"food\",\"empanada\",\"pierogi\",\"potsticker\"],char:\"🥟\",fitzpatrick_scale:false,category:\"food_and_drink\"},doughnut:{keywords:[\"food\",\"dessert\",\"snack\",\"sweet\",\"donut\"],char:\"🍩\",fitzpatrick_scale:false,category:\"food_and_drink\"},cookie:{keywords:[\"food\",\"snack\",\"oreo\",\"chocolate\",\"sweet\",\"dessert\"],char:\"🍪\",fitzpatrick_scale:false,category:\"food_and_drink\"},milk_glass:{keywords:[\"beverage\",\"drink\",\"cow\"],char:\"🥛\",fitzpatrick_scale:false,category:\"food_and_drink\"},beer:{keywords:[\"relax\",\"beverage\",\"drink\",\"drunk\",\"party\",\"pub\",\"summer\",\"alcohol\",\"booze\"],char:\"🍺\",fitzpatrick_scale:false,category:\"food_and_drink\"},beers:{keywords:[\"relax\",\"beverage\",\"drink\",\"drunk\",\"party\",\"pub\",\"summer\",\"alcohol\",\"booze\"],char:\"🍻\",fitzpatrick_scale:false,category:\"food_and_drink\"},clinking_glasses:{keywords:[\"beverage\",\"drink\",\"party\",\"alcohol\",\"celebrate\",\"cheers\",\"wine\",\"champagne\",\"toast\"],char:\"🥂\",fitzpatrick_scale:false,category:\"food_and_drink\"},wine_glass:{keywords:[\"drink\",\"beverage\",\"drunk\",\"alcohol\",\"booze\"],char:\"🍷\",fitzpatrick_scale:false,category:\"food_and_drink\"},tumbler_glass:{keywords:[\"drink\",\"beverage\",\"drunk\",\"alcohol\",\"liquor\",\"booze\",\"bourbon\",\"scotch\",\"whisky\",\"glass\",\"shot\"],char:\"🥃\",fitzpatrick_scale:false,category:\"food_and_drink\"},cocktail:{keywords:[\"drink\",\"drunk\",\"alcohol\",\"beverage\",\"booze\",\"mojito\"],char:\"🍸\",fitzpatrick_scale:false,category:\"food_and_drink\"},tropical_drink:{keywords:[\"beverage\",\"cocktail\",\"summer\",\"beach\",\"alcohol\",\"booze\",\"mojito\"],char:\"🍹\",fitzpatrick_scale:false,category:\"food_and_drink\"},champagne:{keywords:[\"drink\",\"wine\",\"bottle\",\"celebration\"],char:\"🍾\",fitzpatrick_scale:false,category:\"food_and_drink\"},sake:{keywords:[\"wine\",\"drink\",\"drunk\",\"beverage\",\"japanese\",\"alcohol\",\"booze\"],char:\"🍶\",fitzpatrick_scale:false,category:\"food_and_drink\"},tea:{keywords:[\"drink\",\"bowl\",\"breakfast\",\"green\",\"british\"],char:\"🍵\",fitzpatrick_scale:false,category:\"food_and_drink\"},cup_with_straw:{keywords:[\"drink\",\"soda\"],char:\"🥤\",fitzpatrick_scale:false,category:\"food_and_drink\"},coffee:{keywords:[\"beverage\",\"caffeine\",\"latte\",\"espresso\"],char:\"☕\",fitzpatrick_scale:false,category:\"food_and_drink\"},baby_bottle:{keywords:[\"food\",\"container\",\"milk\"],char:\"🍼\",fitzpatrick_scale:false,category:\"food_and_drink\"},salt:{keywords:[\"condiment\",\"shaker\"],char:\"🧂\",fitzpatrick_scale:false,category:\"food_and_drink\"},spoon:{keywords:[\"cutlery\",\"kitchen\",\"tableware\"],char:\"🥄\",fitzpatrick_scale:false,category:\"food_and_drink\"},fork_and_knife:{keywords:[\"cutlery\",\"kitchen\"],char:\"🍴\",fitzpatrick_scale:false,category:\"food_and_drink\"},plate_with_cutlery:{keywords:[\"food\",\"eat\",\"meal\",\"lunch\",\"dinner\",\"restaurant\"],char:\"🍽\",fitzpatrick_scale:false,category:\"food_and_drink\"},bowl_with_spoon:{keywords:[\"food\",\"breakfast\",\"cereal\",\"oatmeal\",\"porridge\"],char:\"🥣\",fitzpatrick_scale:false,category:\"food_and_drink\"},takeout_box:{keywords:[\"food\",\"leftovers\"],char:\"🥡\",fitzpatrick_scale:false,category:\"food_and_drink\"},chopsticks:{keywords:[\"food\"],char:\"🥢\",fitzpatrick_scale:false,category:\"food_and_drink\"},soccer:{keywords:[\"sports\",\"football\"],char:\"⚽\",fitzpatrick_scale:false,category:\"activity\"},basketball:{keywords:[\"sports\",\"balls\",\"NBA\"],char:\"🏀\",fitzpatrick_scale:false,category:\"activity\"},football:{keywords:[\"sports\",\"balls\",\"NFL\"],char:\"🏈\",fitzpatrick_scale:false,category:\"activity\"},baseball:{keywords:[\"sports\",\"balls\"],char:\"⚾\",fitzpatrick_scale:false,category:\"activity\"},softball:{keywords:[\"sports\",\"balls\"],char:\"🥎\",fitzpatrick_scale:false,category:\"activity\"},tennis:{keywords:[\"sports\",\"balls\",\"green\"],char:\"🎾\",fitzpatrick_scale:false,category:\"activity\"},volleyball:{keywords:[\"sports\",\"balls\"],char:\"🏐\",fitzpatrick_scale:false,category:\"activity\"},rugby_football:{keywords:[\"sports\",\"team\"],char:\"🏉\",fitzpatrick_scale:false,category:\"activity\"},flying_disc:{keywords:[\"sports\",\"frisbee\",\"ultimate\"],char:\"🥏\",fitzpatrick_scale:false,category:\"activity\"},\"8ball\":{keywords:[\"pool\",\"hobby\",\"game\",\"luck\",\"magic\"],char:\"🎱\",fitzpatrick_scale:false,category:\"activity\"},golf:{keywords:[\"sports\",\"business\",\"flag\",\"hole\",\"summer\"],char:\"⛳\",fitzpatrick_scale:false,category:\"activity\"},golfing_woman:{keywords:[\"sports\",\"business\",\"woman\",\"female\"],char:\"🏌️‍♀️\",fitzpatrick_scale:false,category:\"activity\"},golfing_man:{keywords:[\"sports\",\"business\"],char:\"🏌\",fitzpatrick_scale:true,category:\"activity\"},ping_pong:{keywords:[\"sports\",\"pingpong\"],char:\"🏓\",fitzpatrick_scale:false,category:\"activity\"},badminton:{keywords:[\"sports\"],char:\"🏸\",fitzpatrick_scale:false,category:\"activity\"},goal_net:{keywords:[\"sports\"],char:\"🥅\",fitzpatrick_scale:false,category:\"activity\"},ice_hockey:{keywords:[\"sports\"],char:\"🏒\",fitzpatrick_scale:false,category:\"activity\"},field_hockey:{keywords:[\"sports\"],char:\"🏑\",fitzpatrick_scale:false,category:\"activity\"},lacrosse:{keywords:[\"sports\",\"ball\",\"stick\"],char:\"🥍\",fitzpatrick_scale:false,category:\"activity\"},cricket:{keywords:[\"sports\"],char:\"🏏\",fitzpatrick_scale:false,category:\"activity\"},ski:{keywords:[\"sports\",\"winter\",\"cold\",\"snow\"],char:\"🎿\",fitzpatrick_scale:false,category:\"activity\"},skier:{keywords:[\"sports\",\"winter\",\"snow\"],char:\"⛷\",fitzpatrick_scale:false,category:\"activity\"},snowboarder:{keywords:[\"sports\",\"winter\"],char:\"🏂\",fitzpatrick_scale:true,category:\"activity\"},person_fencing:{keywords:[\"sports\",\"fencing\",\"sword\"],char:\"🤺\",fitzpatrick_scale:false,category:\"activity\"},women_wrestling:{keywords:[\"sports\",\"wrestlers\"],char:\"🤼‍♀️\",fitzpatrick_scale:false,category:\"activity\"},men_wrestling:{keywords:[\"sports\",\"wrestlers\"],char:\"🤼‍♂️\",fitzpatrick_scale:false,category:\"activity\"},woman_cartwheeling:{keywords:[\"gymnastics\"],char:\"🤸‍♀️\",fitzpatrick_scale:true,category:\"activity\"},man_cartwheeling:{keywords:[\"gymnastics\"],char:\"🤸‍♂️\",fitzpatrick_scale:true,category:\"activity\"},woman_playing_handball:{keywords:[\"sports\"],char:\"🤾‍♀️\",fitzpatrick_scale:true,category:\"activity\"},man_playing_handball:{keywords:[\"sports\"],char:\"🤾‍♂️\",fitzpatrick_scale:true,category:\"activity\"},ice_skate:{keywords:[\"sports\"],char:\"⛸\",fitzpatrick_scale:false,category:\"activity\"},curling_stone:{keywords:[\"sports\"],char:\"🥌\",fitzpatrick_scale:false,category:\"activity\"},skateboard:{keywords:[\"board\"],char:\"🛹\",fitzpatrick_scale:false,category:\"activity\"},sled:{keywords:[\"sleigh\",\"luge\",\"toboggan\"],char:\"🛷\",fitzpatrick_scale:false,category:\"activity\"},bow_and_arrow:{keywords:[\"sports\"],char:\"🏹\",fitzpatrick_scale:false,category:\"activity\"},fishing_pole_and_fish:{keywords:[\"food\",\"hobby\",\"summer\"],char:\"🎣\",fitzpatrick_scale:false,category:\"activity\"},boxing_glove:{keywords:[\"sports\",\"fighting\"],char:\"🥊\",fitzpatrick_scale:false,category:\"activity\"},martial_arts_uniform:{keywords:[\"judo\",\"karate\",\"taekwondo\"],char:\"🥋\",fitzpatrick_scale:false,category:\"activity\"},rowing_woman:{keywords:[\"sports\",\"hobby\",\"water\",\"ship\",\"woman\",\"female\"],char:\"🚣‍♀️\",fitzpatrick_scale:true,category:\"activity\"},rowing_man:{keywords:[\"sports\",\"hobby\",\"water\",\"ship\"],char:\"🚣\",fitzpatrick_scale:true,category:\"activity\"},climbing_woman:{keywords:[\"sports\",\"hobby\",\"woman\",\"female\",\"rock\"],char:\"🧗‍♀️\",fitzpatrick_scale:true,category:\"activity\"},climbing_man:{keywords:[\"sports\",\"hobby\",\"man\",\"male\",\"rock\"],char:\"🧗‍♂️\",fitzpatrick_scale:true,category:\"activity\"},swimming_woman:{keywords:[\"sports\",\"exercise\",\"human\",\"athlete\",\"water\",\"summer\",\"woman\",\"female\"],char:\"🏊‍♀️\",fitzpatrick_scale:true,category:\"activity\"},swimming_man:{keywords:[\"sports\",\"exercise\",\"human\",\"athlete\",\"water\",\"summer\"],char:\"🏊\",fitzpatrick_scale:true,category:\"activity\"},woman_playing_water_polo:{keywords:[\"sports\",\"pool\"],char:\"🤽‍♀️\",fitzpatrick_scale:true,category:\"activity\"},man_playing_water_polo:{keywords:[\"sports\",\"pool\"],char:\"🤽‍♂️\",fitzpatrick_scale:true,category:\"activity\"},woman_in_lotus_position:{keywords:[\"woman\",\"female\",\"meditation\",\"yoga\",\"serenity\",\"zen\",\"mindfulness\"],char:\"🧘‍♀️\",fitzpatrick_scale:true,category:\"activity\"},man_in_lotus_position:{keywords:[\"man\",\"male\",\"meditation\",\"yoga\",\"serenity\",\"zen\",\"mindfulness\"],char:\"🧘‍♂️\",fitzpatrick_scale:true,category:\"activity\"},surfing_woman:{keywords:[\"sports\",\"ocean\",\"sea\",\"summer\",\"beach\",\"woman\",\"female\"],char:\"🏄‍♀️\",fitzpatrick_scale:true,category:\"activity\"},surfing_man:{keywords:[\"sports\",\"ocean\",\"sea\",\"summer\",\"beach\"],char:\"🏄\",fitzpatrick_scale:true,category:\"activity\"},bath:{keywords:[\"clean\",\"shower\",\"bathroom\"],char:\"🛀\",fitzpatrick_scale:true,category:\"activity\"},basketball_woman:{keywords:[\"sports\",\"human\",\"woman\",\"female\"],char:\"⛹️‍♀️\",fitzpatrick_scale:true,category:\"activity\"},basketball_man:{keywords:[\"sports\",\"human\"],char:\"⛹\",fitzpatrick_scale:true,category:\"activity\"},weight_lifting_woman:{keywords:[\"sports\",\"training\",\"exercise\",\"woman\",\"female\"],char:\"🏋️‍♀️\",fitzpatrick_scale:true,category:\"activity\"},weight_lifting_man:{keywords:[\"sports\",\"training\",\"exercise\"],char:\"🏋\",fitzpatrick_scale:true,category:\"activity\"},biking_woman:{keywords:[\"sports\",\"bike\",\"exercise\",\"hipster\",\"woman\",\"female\"],char:\"🚴‍♀️\",fitzpatrick_scale:true,category:\"activity\"},biking_man:{keywords:[\"sports\",\"bike\",\"exercise\",\"hipster\"],char:\"🚴\",fitzpatrick_scale:true,category:\"activity\"},mountain_biking_woman:{keywords:[\"transportation\",\"sports\",\"human\",\"race\",\"bike\",\"woman\",\"female\"],char:\"🚵‍♀️\",fitzpatrick_scale:true,category:\"activity\"},mountain_biking_man:{keywords:[\"transportation\",\"sports\",\"human\",\"race\",\"bike\"],char:\"🚵\",fitzpatrick_scale:true,category:\"activity\"},horse_racing:{keywords:[\"animal\",\"betting\",\"competition\",\"gambling\",\"luck\"],char:\"🏇\",fitzpatrick_scale:true,category:\"activity\"},business_suit_levitating:{keywords:[\"suit\",\"business\",\"levitate\",\"hover\",\"jump\"],char:\"🕴\",fitzpatrick_scale:true,category:\"activity\"},trophy:{keywords:[\"win\",\"award\",\"contest\",\"place\",\"ftw\",\"ceremony\"],char:\"🏆\",fitzpatrick_scale:false,category:\"activity\"},running_shirt_with_sash:{keywords:[\"play\",\"pageant\"],char:\"🎽\",fitzpatrick_scale:false,category:\"activity\"},medal_sports:{keywords:[\"award\",\"winning\"],char:\"🏅\",fitzpatrick_scale:false,category:\"activity\"},medal_military:{keywords:[\"award\",\"winning\",\"army\"],char:\"🎖\",fitzpatrick_scale:false,category:\"activity\"},\"1st_place_medal\":{keywords:[\"award\",\"winning\",\"first\"],char:\"🥇\",fitzpatrick_scale:false,category:\"activity\"},\"2nd_place_medal\":{keywords:[\"award\",\"second\"],char:\"🥈\",fitzpatrick_scale:false,category:\"activity\"},\"3rd_place_medal\":{keywords:[\"award\",\"third\"],char:\"🥉\",fitzpatrick_scale:false,category:\"activity\"},reminder_ribbon:{keywords:[\"sports\",\"cause\",\"support\",\"awareness\"],char:\"🎗\",fitzpatrick_scale:false,category:\"activity\"},rosette:{keywords:[\"flower\",\"decoration\",\"military\"],char:\"🏵\",fitzpatrick_scale:false,category:\"activity\"},ticket:{keywords:[\"event\",\"concert\",\"pass\"],char:\"🎫\",fitzpatrick_scale:false,category:\"activity\"},tickets:{keywords:[\"sports\",\"concert\",\"entrance\"],char:\"🎟\",fitzpatrick_scale:false,category:\"activity\"},performing_arts:{keywords:[\"acting\",\"theater\",\"drama\"],char:\"🎭\",fitzpatrick_scale:false,category:\"activity\"},art:{keywords:[\"design\",\"paint\",\"draw\",\"colors\"],char:\"🎨\",fitzpatrick_scale:false,category:\"activity\"},circus_tent:{keywords:[\"festival\",\"carnival\",\"party\"],char:\"🎪\",fitzpatrick_scale:false,category:\"activity\"},woman_juggling:{keywords:[\"juggle\",\"balance\",\"skill\",\"multitask\"],char:\"🤹‍♀️\",fitzpatrick_scale:true,category:\"activity\"},man_juggling:{keywords:[\"juggle\",\"balance\",\"skill\",\"multitask\"],char:\"🤹‍♂️\",fitzpatrick_scale:true,category:\"activity\"},microphone:{keywords:[\"sound\",\"music\",\"PA\",\"sing\",\"talkshow\"],char:\"🎤\",fitzpatrick_scale:false,category:\"activity\"},headphones:{keywords:[\"music\",\"score\",\"gadgets\"],char:\"🎧\",fitzpatrick_scale:false,category:\"activity\"},musical_score:{keywords:[\"treble\",\"clef\",\"compose\"],char:\"🎼\",fitzpatrick_scale:false,category:\"activity\"},musical_keyboard:{keywords:[\"piano\",\"instrument\",\"compose\"],char:\"🎹\",fitzpatrick_scale:false,category:\"activity\"},drum:{keywords:[\"music\",\"instrument\",\"drumsticks\",\"snare\"],char:\"🥁\",fitzpatrick_scale:false,category:\"activity\"},saxophone:{keywords:[\"music\",\"instrument\",\"jazz\",\"blues\"],char:\"🎷\",fitzpatrick_scale:false,category:\"activity\"},trumpet:{keywords:[\"music\",\"brass\"],char:\"🎺\",fitzpatrick_scale:false,category:\"activity\"},guitar:{keywords:[\"music\",\"instrument\"],char:\"🎸\",fitzpatrick_scale:false,category:\"activity\"},violin:{keywords:[\"music\",\"instrument\",\"orchestra\",\"symphony\"],char:\"🎻\",fitzpatrick_scale:false,category:\"activity\"},clapper:{keywords:[\"movie\",\"film\",\"record\"],char:\"🎬\",fitzpatrick_scale:false,category:\"activity\"},video_game:{keywords:[\"play\",\"console\",\"PS4\",\"controller\"],char:\"🎮\",fitzpatrick_scale:false,category:\"activity\"},space_invader:{keywords:[\"game\",\"arcade\",\"play\"],char:\"👾\",fitzpatrick_scale:false,category:\"activity\"},dart:{keywords:[\"game\",\"play\",\"bar\",\"target\",\"bullseye\"],char:\"🎯\",fitzpatrick_scale:false,category:\"activity\"},game_die:{keywords:[\"dice\",\"random\",\"tabletop\",\"play\",\"luck\"],char:\"🎲\",fitzpatrick_scale:false,category:\"activity\"},chess_pawn:{keywords:[\"expendable\"],char:\"♟\",fitzpatrick_scale:false,category:\"activity\"},slot_machine:{keywords:[\"bet\",\"gamble\",\"vegas\",\"fruit machine\",\"luck\",\"casino\"],char:\"🎰\",fitzpatrick_scale:false,category:\"activity\"},jigsaw:{keywords:[\"interlocking\",\"puzzle\",\"piece\"],char:\"🧩\",fitzpatrick_scale:false,category:\"activity\"},bowling:{keywords:[\"sports\",\"fun\",\"play\"],char:\"🎳\",fitzpatrick_scale:false,category:\"activity\"},red_car:{keywords:[\"red\",\"transportation\",\"vehicle\"],char:\"🚗\",fitzpatrick_scale:false,category:\"travel_and_places\"},taxi:{keywords:[\"uber\",\"vehicle\",\"cars\",\"transportation\"],char:\"🚕\",fitzpatrick_scale:false,category:\"travel_and_places\"},blue_car:{keywords:[\"transportation\",\"vehicle\"],char:\"🚙\",fitzpatrick_scale:false,category:\"travel_and_places\"},bus:{keywords:[\"car\",\"vehicle\",\"transportation\"],char:\"🚌\",fitzpatrick_scale:false,category:\"travel_and_places\"},trolleybus:{keywords:[\"bart\",\"transportation\",\"vehicle\"],char:\"🚎\",fitzpatrick_scale:false,category:\"travel_and_places\"},racing_car:{keywords:[\"sports\",\"race\",\"fast\",\"formula\",\"f1\"],char:\"🏎\",fitzpatrick_scale:false,category:\"travel_and_places\"},police_car:{keywords:[\"vehicle\",\"cars\",\"transportation\",\"law\",\"legal\",\"enforcement\"],char:\"🚓\",fitzpatrick_scale:false,category:\"travel_and_places\"},ambulance:{keywords:[\"health\",\"911\",\"hospital\"],char:\"🚑\",fitzpatrick_scale:false,category:\"travel_and_places\"},fire_engine:{keywords:[\"transportation\",\"cars\",\"vehicle\"],char:\"🚒\",fitzpatrick_scale:false,category:\"travel_and_places\"},minibus:{keywords:[\"vehicle\",\"car\",\"transportation\"],char:\"🚐\",fitzpatrick_scale:false,category:\"travel_and_places\"},truck:{keywords:[\"cars\",\"transportation\"],char:\"🚚\",fitzpatrick_scale:false,category:\"travel_and_places\"},articulated_lorry:{keywords:[\"vehicle\",\"cars\",\"transportation\",\"express\"],char:\"🚛\",fitzpatrick_scale:false,category:\"travel_and_places\"},tractor:{keywords:[\"vehicle\",\"car\",\"farming\",\"agriculture\"],char:\"🚜\",fitzpatrick_scale:false,category:\"travel_and_places\"},kick_scooter:{keywords:[\"vehicle\",\"kick\",\"razor\"],char:\"🛴\",fitzpatrick_scale:false,category:\"travel_and_places\"},motorcycle:{keywords:[\"race\",\"sports\",\"fast\"],char:\"🏍\",fitzpatrick_scale:false,category:\"travel_and_places\"},bike:{keywords:[\"sports\",\"bicycle\",\"exercise\",\"hipster\"],char:\"🚲\",fitzpatrick_scale:false,category:\"travel_and_places\"},motor_scooter:{keywords:[\"vehicle\",\"vespa\",\"sasha\"],char:\"🛵\",fitzpatrick_scale:false,category:\"travel_and_places\"},rotating_light:{keywords:[\"police\",\"ambulance\",\"911\",\"emergency\",\"alert\",\"error\",\"pinged\",\"law\",\"legal\"],char:\"🚨\",fitzpatrick_scale:false,category:\"travel_and_places\"},oncoming_police_car:{keywords:[\"vehicle\",\"law\",\"legal\",\"enforcement\",\"911\"],char:\"🚔\",fitzpatrick_scale:false,category:\"travel_and_places\"},oncoming_bus:{keywords:[\"vehicle\",\"transportation\"],char:\"🚍\",fitzpatrick_scale:false,category:\"travel_and_places\"},oncoming_automobile:{keywords:[\"car\",\"vehicle\",\"transportation\"],char:\"🚘\",fitzpatrick_scale:false,category:\"travel_and_places\"},oncoming_taxi:{keywords:[\"vehicle\",\"cars\",\"uber\"],char:\"🚖\",fitzpatrick_scale:false,category:\"travel_and_places\"},aerial_tramway:{keywords:[\"transportation\",\"vehicle\",\"ski\"],char:\"🚡\",fitzpatrick_scale:false,category:\"travel_and_places\"},mountain_cableway:{keywords:[\"transportation\",\"vehicle\",\"ski\"],char:\"🚠\",fitzpatrick_scale:false,category:\"travel_and_places\"},suspension_railway:{keywords:[\"vehicle\",\"transportation\"],char:\"🚟\",fitzpatrick_scale:false,category:\"travel_and_places\"},railway_car:{keywords:[\"transportation\",\"vehicle\"],char:\"🚃\",fitzpatrick_scale:false,category:\"travel_and_places\"},train:{keywords:[\"transportation\",\"vehicle\",\"carriage\",\"public\",\"travel\"],char:\"🚋\",fitzpatrick_scale:false,category:\"travel_and_places\"},monorail:{keywords:[\"transportation\",\"vehicle\"],char:\"🚝\",fitzpatrick_scale:false,category:\"travel_and_places\"},bullettrain_side:{keywords:[\"transportation\",\"vehicle\"],char:\"🚄\",fitzpatrick_scale:false,category:\"travel_and_places\"},bullettrain_front:{keywords:[\"transportation\",\"vehicle\",\"speed\",\"fast\",\"public\",\"travel\"],char:\"🚅\",fitzpatrick_scale:false,category:\"travel_and_places\"},light_rail:{keywords:[\"transportation\",\"vehicle\"],char:\"🚈\",fitzpatrick_scale:false,category:\"travel_and_places\"},mountain_railway:{keywords:[\"transportation\",\"vehicle\"],char:\"🚞\",fitzpatrick_scale:false,category:\"travel_and_places\"},steam_locomotive:{keywords:[\"transportation\",\"vehicle\",\"train\"],char:\"🚂\",fitzpatrick_scale:false,category:\"travel_and_places\"},train2:{keywords:[\"transportation\",\"vehicle\"],char:\"🚆\",fitzpatrick_scale:false,category:\"travel_and_places\"},metro:{keywords:[\"transportation\",\"blue-square\",\"mrt\",\"underground\",\"tube\"],char:\"🚇\",fitzpatrick_scale:false,category:\"travel_and_places\"},tram:{keywords:[\"transportation\",\"vehicle\"],char:\"🚊\",fitzpatrick_scale:false,category:\"travel_and_places\"},station:{keywords:[\"transportation\",\"vehicle\",\"public\"],char:\"🚉\",fitzpatrick_scale:false,category:\"travel_and_places\"},flying_saucer:{keywords:[\"transportation\",\"vehicle\",\"ufo\"],char:\"🛸\",fitzpatrick_scale:false,category:\"travel_and_places\"},helicopter:{keywords:[\"transportation\",\"vehicle\",\"fly\"],char:\"🚁\",fitzpatrick_scale:false,category:\"travel_and_places\"},small_airplane:{keywords:[\"flight\",\"transportation\",\"fly\",\"vehicle\"],char:\"🛩\",fitzpatrick_scale:false,category:\"travel_and_places\"},airplane:{keywords:[\"vehicle\",\"transportation\",\"flight\",\"fly\"],char:\"✈️\",fitzpatrick_scale:false,category:\"travel_and_places\"},flight_departure:{keywords:[\"airport\",\"flight\",\"landing\"],char:\"🛫\",fitzpatrick_scale:false,category:\"travel_and_places\"},flight_arrival:{keywords:[\"airport\",\"flight\",\"boarding\"],char:\"🛬\",fitzpatrick_scale:false,category:\"travel_and_places\"},sailboat:{keywords:[\"ship\",\"summer\",\"transportation\",\"water\",\"sailing\"],char:\"⛵\",fitzpatrick_scale:false,category:\"travel_and_places\"},motor_boat:{keywords:[\"ship\"],char:\"🛥\",fitzpatrick_scale:false,category:\"travel_and_places\"},speedboat:{keywords:[\"ship\",\"transportation\",\"vehicle\",\"summer\"],char:\"🚤\",fitzpatrick_scale:false,category:\"travel_and_places\"},ferry:{keywords:[\"boat\",\"ship\",\"yacht\"],char:\"⛴\",fitzpatrick_scale:false,category:\"travel_and_places\"},passenger_ship:{keywords:[\"yacht\",\"cruise\",\"ferry\"],char:\"🛳\",fitzpatrick_scale:false,category:\"travel_and_places\"},rocket:{keywords:[\"launch\",\"ship\",\"staffmode\",\"NASA\",\"outer space\",\"outer_space\",\"fly\"],char:\"🚀\",fitzpatrick_scale:false,category:\"travel_and_places\"},artificial_satellite:{keywords:[\"communication\",\"gps\",\"orbit\",\"spaceflight\",\"NASA\",\"ISS\"],char:\"🛰\",fitzpatrick_scale:false,category:\"travel_and_places\"},seat:{keywords:[\"sit\",\"airplane\",\"transport\",\"bus\",\"flight\",\"fly\"],char:\"💺\",fitzpatrick_scale:false,category:\"travel_and_places\"},canoe:{keywords:[\"boat\",\"paddle\",\"water\",\"ship\"],char:\"🛶\",fitzpatrick_scale:false,category:\"travel_and_places\"},anchor:{keywords:[\"ship\",\"ferry\",\"sea\",\"boat\"],char:\"⚓\",fitzpatrick_scale:false,category:\"travel_and_places\"},construction:{keywords:[\"wip\",\"progress\",\"caution\",\"warning\"],char:\"🚧\",fitzpatrick_scale:false,category:\"travel_and_places\"},fuelpump:{keywords:[\"gas station\",\"petroleum\"],char:\"⛽\",fitzpatrick_scale:false,category:\"travel_and_places\"},busstop:{keywords:[\"transportation\",\"wait\"],char:\"🚏\",fitzpatrick_scale:false,category:\"travel_and_places\"},vertical_traffic_light:{keywords:[\"transportation\",\"driving\"],char:\"🚦\",fitzpatrick_scale:false,category:\"travel_and_places\"},traffic_light:{keywords:[\"transportation\",\"signal\"],char:\"🚥\",fitzpatrick_scale:false,category:\"travel_and_places\"},checkered_flag:{keywords:[\"contest\",\"finishline\",\"race\",\"gokart\"],char:\"🏁\",fitzpatrick_scale:false,category:\"travel_and_places\"},ship:{keywords:[\"transportation\",\"titanic\",\"deploy\"],char:\"🚢\",fitzpatrick_scale:false,category:\"travel_and_places\"},ferris_wheel:{keywords:[\"photo\",\"carnival\",\"londoneye\"],char:\"🎡\",fitzpatrick_scale:false,category:\"travel_and_places\"},roller_coaster:{keywords:[\"carnival\",\"playground\",\"photo\",\"fun\"],char:\"🎢\",fitzpatrick_scale:false,category:\"travel_and_places\"},carousel_horse:{keywords:[\"photo\",\"carnival\"],char:\"🎠\",fitzpatrick_scale:false,category:\"travel_and_places\"},building_construction:{keywords:[\"wip\",\"working\",\"progress\"],char:\"🏗\",fitzpatrick_scale:false,category:\"travel_and_places\"},foggy:{keywords:[\"photo\",\"mountain\"],char:\"🌁\",fitzpatrick_scale:false,category:\"travel_and_places\"},tokyo_tower:{keywords:[\"photo\",\"japanese\"],char:\"🗼\",fitzpatrick_scale:false,category:\"travel_and_places\"},factory:{keywords:[\"building\",\"industry\",\"pollution\",\"smoke\"],char:\"🏭\",fitzpatrick_scale:false,category:\"travel_and_places\"},fountain:{keywords:[\"photo\",\"summer\",\"water\",\"fresh\"],char:\"⛲\",fitzpatrick_scale:false,category:\"travel_and_places\"},rice_scene:{keywords:[\"photo\",\"japan\",\"asia\",\"tsukimi\"],char:\"🎑\",fitzpatrick_scale:false,category:\"travel_and_places\"},mountain:{keywords:[\"photo\",\"nature\",\"environment\"],char:\"⛰\",fitzpatrick_scale:false,category:\"travel_and_places\"},mountain_snow:{keywords:[\"photo\",\"nature\",\"environment\",\"winter\",\"cold\"],char:\"🏔\",fitzpatrick_scale:false,category:\"travel_and_places\"},mount_fuji:{keywords:[\"photo\",\"mountain\",\"nature\",\"japanese\"],char:\"🗻\",fitzpatrick_scale:false,category:\"travel_and_places\"},volcano:{keywords:[\"photo\",\"nature\",\"disaster\"],char:\"🌋\",fitzpatrick_scale:false,category:\"travel_and_places\"},japan:{keywords:[\"nation\",\"country\",\"japanese\",\"asia\"],char:\"🗾\",fitzpatrick_scale:false,category:\"travel_and_places\"},camping:{keywords:[\"photo\",\"outdoors\",\"tent\"],char:\"🏕\",fitzpatrick_scale:false,category:\"travel_and_places\"},tent:{keywords:[\"photo\",\"camping\",\"outdoors\"],char:\"⛺\",fitzpatrick_scale:false,category:\"travel_and_places\"},national_park:{keywords:[\"photo\",\"environment\",\"nature\"],char:\"🏞\",fitzpatrick_scale:false,category:\"travel_and_places\"},motorway:{keywords:[\"road\",\"cupertino\",\"interstate\",\"highway\"],char:\"🛣\",fitzpatrick_scale:false,category:\"travel_and_places\"},railway_track:{keywords:[\"train\",\"transportation\"],char:\"🛤\",fitzpatrick_scale:false,category:\"travel_and_places\"},sunrise:{keywords:[\"morning\",\"view\",\"vacation\",\"photo\"],char:\"🌅\",fitzpatrick_scale:false,category:\"travel_and_places\"},sunrise_over_mountains:{keywords:[\"view\",\"vacation\",\"photo\"],char:\"🌄\",fitzpatrick_scale:false,category:\"travel_and_places\"},desert:{keywords:[\"photo\",\"warm\",\"saharah\"],char:\"🏜\",fitzpatrick_scale:false,category:\"travel_and_places\"},beach_umbrella:{keywords:[\"weather\",\"summer\",\"sunny\",\"sand\",\"mojito\"],char:\"🏖\",fitzpatrick_scale:false,category:\"travel_and_places\"},desert_island:{keywords:[\"photo\",\"tropical\",\"mojito\"],char:\"🏝\",fitzpatrick_scale:false,category:\"travel_and_places\"},city_sunrise:{keywords:[\"photo\",\"good morning\",\"dawn\"],char:\"🌇\",fitzpatrick_scale:false,category:\"travel_and_places\"},city_sunset:{keywords:[\"photo\",\"evening\",\"sky\",\"buildings\"],char:\"🌆\",fitzpatrick_scale:false,category:\"travel_and_places\"},cityscape:{keywords:[\"photo\",\"night life\",\"urban\"],char:\"🏙\",fitzpatrick_scale:false,category:\"travel_and_places\"},night_with_stars:{keywords:[\"evening\",\"city\",\"downtown\"],char:\"🌃\",fitzpatrick_scale:false,category:\"travel_and_places\"},bridge_at_night:{keywords:[\"photo\",\"sanfrancisco\"],char:\"🌉\",fitzpatrick_scale:false,category:\"travel_and_places\"},milky_way:{keywords:[\"photo\",\"space\",\"stars\"],char:\"🌌\",fitzpatrick_scale:false,category:\"travel_and_places\"},stars:{keywords:[\"night\",\"photo\"],char:\"🌠\",fitzpatrick_scale:false,category:\"travel_and_places\"},sparkler:{keywords:[\"stars\",\"night\",\"shine\"],char:\"🎇\",fitzpatrick_scale:false,category:\"travel_and_places\"},fireworks:{keywords:[\"photo\",\"festival\",\"carnival\",\"congratulations\"],char:\"🎆\",fitzpatrick_scale:false,category:\"travel_and_places\"},rainbow:{keywords:[\"nature\",\"happy\",\"unicorn_face\",\"photo\",\"sky\",\"spring\"],char:\"🌈\",fitzpatrick_scale:false,category:\"travel_and_places\"},houses:{keywords:[\"buildings\",\"photo\"],char:\"🏘\",fitzpatrick_scale:false,category:\"travel_and_places\"},european_castle:{keywords:[\"building\",\"royalty\",\"history\"],char:\"🏰\",fitzpatrick_scale:false,category:\"travel_and_places\"},japanese_castle:{keywords:[\"photo\",\"building\"],char:\"🏯\",fitzpatrick_scale:false,category:\"travel_and_places\"},stadium:{keywords:[\"photo\",\"place\",\"sports\",\"concert\",\"venue\"],char:\"🏟\",fitzpatrick_scale:false,category:\"travel_and_places\"},statue_of_liberty:{keywords:[\"american\",\"newyork\"],char:\"🗽\",fitzpatrick_scale:false,category:\"travel_and_places\"},house:{keywords:[\"building\",\"home\"],char:\"🏠\",fitzpatrick_scale:false,category:\"travel_and_places\"},house_with_garden:{keywords:[\"home\",\"plant\",\"nature\"],char:\"🏡\",fitzpatrick_scale:false,category:\"travel_and_places\"},derelict_house:{keywords:[\"abandon\",\"evict\",\"broken\",\"building\"],char:\"🏚\",fitzpatrick_scale:false,category:\"travel_and_places\"},office:{keywords:[\"building\",\"bureau\",\"work\"],char:\"🏢\",fitzpatrick_scale:false,category:\"travel_and_places\"},department_store:{keywords:[\"building\",\"shopping\",\"mall\"],char:\"🏬\",fitzpatrick_scale:false,category:\"travel_and_places\"},post_office:{keywords:[\"building\",\"envelope\",\"communication\"],char:\"🏣\",fitzpatrick_scale:false,category:\"travel_and_places\"},european_post_office:{keywords:[\"building\",\"email\"],char:\"🏤\",fitzpatrick_scale:false,category:\"travel_and_places\"},hospital:{keywords:[\"building\",\"health\",\"surgery\",\"doctor\"],char:\"🏥\",fitzpatrick_scale:false,category:\"travel_and_places\"},bank:{keywords:[\"building\",\"money\",\"sales\",\"cash\",\"business\",\"enterprise\"],char:\"🏦\",fitzpatrick_scale:false,category:\"travel_and_places\"},hotel:{keywords:[\"building\",\"accomodation\",\"checkin\"],char:\"🏨\",fitzpatrick_scale:false,category:\"travel_and_places\"},convenience_store:{keywords:[\"building\",\"shopping\",\"groceries\"],char:\"🏪\",fitzpatrick_scale:false,category:\"travel_and_places\"},school:{keywords:[\"building\",\"student\",\"education\",\"learn\",\"teach\"],char:\"🏫\",fitzpatrick_scale:false,category:\"travel_and_places\"},love_hotel:{keywords:[\"like\",\"affection\",\"dating\"],char:\"🏩\",fitzpatrick_scale:false,category:\"travel_and_places\"},wedding:{keywords:[\"love\",\"like\",\"affection\",\"couple\",\"marriage\",\"bride\",\"groom\"],char:\"💒\",fitzpatrick_scale:false,category:\"travel_and_places\"},classical_building:{keywords:[\"art\",\"culture\",\"history\"],char:\"🏛\",fitzpatrick_scale:false,category:\"travel_and_places\"},church:{keywords:[\"building\",\"religion\",\"christ\"],char:\"⛪\",fitzpatrick_scale:false,category:\"travel_and_places\"},mosque:{keywords:[\"islam\",\"worship\",\"minaret\"],char:\"🕌\",fitzpatrick_scale:false,category:\"travel_and_places\"},synagogue:{keywords:[\"judaism\",\"worship\",\"temple\",\"jewish\"],char:\"🕍\",fitzpatrick_scale:false,category:\"travel_and_places\"},kaaba:{keywords:[\"mecca\",\"mosque\",\"islam\"],char:\"🕋\",fitzpatrick_scale:false,category:\"travel_and_places\"},shinto_shrine:{keywords:[\"temple\",\"japan\",\"kyoto\"],char:\"⛩\",fitzpatrick_scale:false,category:\"travel_and_places\"},watch:{keywords:[\"time\",\"accessories\"],char:\"⌚\",fitzpatrick_scale:false,category:\"objects\"},iphone:{keywords:[\"technology\",\"apple\",\"gadgets\",\"dial\"],char:\"📱\",fitzpatrick_scale:false,category:\"objects\"},calling:{keywords:[\"iphone\",\"incoming\"],char:\"📲\",fitzpatrick_scale:false,category:\"objects\"},computer:{keywords:[\"technology\",\"laptop\",\"screen\",\"display\",\"monitor\"],char:\"💻\",fitzpatrick_scale:false,category:\"objects\"},keyboard:{keywords:[\"technology\",\"computer\",\"type\",\"input\",\"text\"],char:\"⌨\",fitzpatrick_scale:false,category:\"objects\"},desktop_computer:{keywords:[\"technology\",\"computing\",\"screen\"],char:\"🖥\",fitzpatrick_scale:false,category:\"objects\"},printer:{keywords:[\"paper\",\"ink\"],char:\"🖨\",fitzpatrick_scale:false,category:\"objects\"},computer_mouse:{keywords:[\"click\"],char:\"🖱\",fitzpatrick_scale:false,category:\"objects\"},trackball:{keywords:[\"technology\",\"trackpad\"],char:\"🖲\",fitzpatrick_scale:false,category:\"objects\"},joystick:{keywords:[\"game\",\"play\"],char:\"🕹\",fitzpatrick_scale:false,category:\"objects\"},clamp:{keywords:[\"tool\"],char:\"🗜\",fitzpatrick_scale:false,category:\"objects\"},minidisc:{keywords:[\"technology\",\"record\",\"data\",\"disk\",\"90s\"],char:\"💽\",fitzpatrick_scale:false,category:\"objects\"},floppy_disk:{keywords:[\"oldschool\",\"technology\",\"save\",\"90s\",\"80s\"],char:\"💾\",fitzpatrick_scale:false,category:\"objects\"},cd:{keywords:[\"technology\",\"dvd\",\"disk\",\"disc\",\"90s\"],char:\"💿\",fitzpatrick_scale:false,category:\"objects\"},dvd:{keywords:[\"cd\",\"disk\",\"disc\"],char:\"📀\",fitzpatrick_scale:false,category:\"objects\"},vhs:{keywords:[\"record\",\"video\",\"oldschool\",\"90s\",\"80s\"],char:\"📼\",fitzpatrick_scale:false,category:\"objects\"},camera:{keywords:[\"gadgets\",\"photography\"],char:\"📷\",fitzpatrick_scale:false,category:\"objects\"},camera_flash:{keywords:[\"photography\",\"gadgets\"],char:\"📸\",fitzpatrick_scale:false,category:\"objects\"},video_camera:{keywords:[\"film\",\"record\"],char:\"📹\",fitzpatrick_scale:false,category:\"objects\"},movie_camera:{keywords:[\"film\",\"record\"],char:\"🎥\",fitzpatrick_scale:false,category:\"objects\"},film_projector:{keywords:[\"video\",\"tape\",\"record\",\"movie\"],char:\"📽\",fitzpatrick_scale:false,category:\"objects\"},film_strip:{keywords:[\"movie\"],char:\"🎞\",fitzpatrick_scale:false,category:\"objects\"},telephone_receiver:{keywords:[\"technology\",\"communication\",\"dial\"],char:\"📞\",fitzpatrick_scale:false,category:\"objects\"},phone:{keywords:[\"technology\",\"communication\",\"dial\",\"telephone\"],char:\"☎️\",fitzpatrick_scale:false,category:\"objects\"},pager:{keywords:[\"bbcall\",\"oldschool\",\"90s\"],char:\"📟\",fitzpatrick_scale:false,category:\"objects\"},fax:{keywords:[\"communication\",\"technology\"],char:\"📠\",fitzpatrick_scale:false,category:\"objects\"},tv:{keywords:[\"technology\",\"program\",\"oldschool\",\"show\",\"television\"],char:\"📺\",fitzpatrick_scale:false,category:\"objects\"},radio:{keywords:[\"communication\",\"music\",\"podcast\",\"program\"],char:\"📻\",fitzpatrick_scale:false,category:\"objects\"},studio_microphone:{keywords:[\"sing\",\"recording\",\"artist\",\"talkshow\"],char:\"🎙\",fitzpatrick_scale:false,category:\"objects\"},level_slider:{keywords:[\"scale\"],char:\"🎚\",fitzpatrick_scale:false,category:\"objects\"},control_knobs:{keywords:[\"dial\"],char:\"🎛\",fitzpatrick_scale:false,category:\"objects\"},compass:{keywords:[\"magnetic\",\"navigation\",\"orienteering\"],char:\"🧭\",fitzpatrick_scale:false,category:\"objects\"},stopwatch:{keywords:[\"time\",\"deadline\"],char:\"⏱\",fitzpatrick_scale:false,category:\"objects\"},timer_clock:{keywords:[\"alarm\"],char:\"⏲\",fitzpatrick_scale:false,category:\"objects\"},alarm_clock:{keywords:[\"time\",\"wake\"],char:\"⏰\",fitzpatrick_scale:false,category:\"objects\"},mantelpiece_clock:{keywords:[\"time\"],char:\"🕰\",fitzpatrick_scale:false,category:\"objects\"},hourglass_flowing_sand:{keywords:[\"oldschool\",\"time\",\"countdown\"],char:\"⏳\",fitzpatrick_scale:false,category:\"objects\"},hourglass:{keywords:[\"time\",\"clock\",\"oldschool\",\"limit\",\"exam\",\"quiz\",\"test\"],char:\"⌛\",fitzpatrick_scale:false,category:\"objects\"},satellite:{keywords:[\"communication\",\"future\",\"radio\",\"space\"],char:\"📡\",fitzpatrick_scale:false,category:\"objects\"},battery:{keywords:[\"power\",\"energy\",\"sustain\"],char:\"🔋\",fitzpatrick_scale:false,category:\"objects\"},electric_plug:{keywords:[\"charger\",\"power\"],char:\"🔌\",fitzpatrick_scale:false,category:\"objects\"},bulb:{keywords:[\"light\",\"electricity\",\"idea\"],char:\"💡\",fitzpatrick_scale:false,category:\"objects\"},flashlight:{keywords:[\"dark\",\"camping\",\"sight\",\"night\"],char:\"🔦\",fitzpatrick_scale:false,category:\"objects\"},candle:{keywords:[\"fire\",\"wax\"],char:\"🕯\",fitzpatrick_scale:false,category:\"objects\"},fire_extinguisher:{keywords:[\"quench\"],char:\"🧯\",fitzpatrick_scale:false,category:\"objects\"},wastebasket:{keywords:[\"bin\",\"trash\",\"rubbish\",\"garbage\",\"toss\"],char:\"🗑\",fitzpatrick_scale:false,category:\"objects\"},oil_drum:{keywords:[\"barrell\"],char:\"🛢\",fitzpatrick_scale:false,category:\"objects\"},money_with_wings:{keywords:[\"dollar\",\"bills\",\"payment\",\"sale\"],char:\"💸\",fitzpatrick_scale:false,category:\"objects\"},dollar:{keywords:[\"money\",\"sales\",\"bill\",\"currency\"],char:\"💵\",fitzpatrick_scale:false,category:\"objects\"},yen:{keywords:[\"money\",\"sales\",\"japanese\",\"dollar\",\"currency\"],char:\"💴\",fitzpatrick_scale:false,category:\"objects\"},euro:{keywords:[\"money\",\"sales\",\"dollar\",\"currency\"],char:\"💶\",fitzpatrick_scale:false,category:\"objects\"},pound:{keywords:[\"british\",\"sterling\",\"money\",\"sales\",\"bills\",\"uk\",\"england\",\"currency\"],char:\"💷\",fitzpatrick_scale:false,category:\"objects\"},moneybag:{keywords:[\"dollar\",\"payment\",\"coins\",\"sale\"],char:\"💰\",fitzpatrick_scale:false,category:\"objects\"},credit_card:{keywords:[\"money\",\"sales\",\"dollar\",\"bill\",\"payment\",\"shopping\"],char:\"💳\",fitzpatrick_scale:false,category:\"objects\"},gem:{keywords:[\"blue\",\"ruby\",\"diamond\",\"jewelry\"],char:\"💎\",fitzpatrick_scale:false,category:\"objects\"},balance_scale:{keywords:[\"law\",\"fairness\",\"weight\"],char:\"⚖\",fitzpatrick_scale:false,category:\"objects\"},toolbox:{keywords:[\"tools\",\"diy\",\"fix\",\"maintainer\",\"mechanic\"],char:\"🧰\",fitzpatrick_scale:false,category:\"objects\"},wrench:{keywords:[\"tools\",\"diy\",\"ikea\",\"fix\",\"maintainer\"],char:\"🔧\",fitzpatrick_scale:false,category:\"objects\"},hammer:{keywords:[\"tools\",\"build\",\"create\"],char:\"🔨\",fitzpatrick_scale:false,category:\"objects\"},hammer_and_pick:{keywords:[\"tools\",\"build\",\"create\"],char:\"⚒\",fitzpatrick_scale:false,category:\"objects\"},hammer_and_wrench:{keywords:[\"tools\",\"build\",\"create\"],char:\"🛠\",fitzpatrick_scale:false,category:\"objects\"},pick:{keywords:[\"tools\",\"dig\"],char:\"⛏\",fitzpatrick_scale:false,category:\"objects\"},nut_and_bolt:{keywords:[\"handy\",\"tools\",\"fix\"],char:\"🔩\",fitzpatrick_scale:false,category:\"objects\"},gear:{keywords:[\"cog\"],char:\"⚙\",fitzpatrick_scale:false,category:\"objects\"},brick:{keywords:[\"bricks\"],char:\"🧱\",fitzpatrick_scale:false,category:\"objects\"},chains:{keywords:[\"lock\",\"arrest\"],char:\"⛓\",fitzpatrick_scale:false,category:\"objects\"},magnet:{keywords:[\"attraction\",\"magnetic\"],char:\"🧲\",fitzpatrick_scale:false,category:\"objects\"},gun:{keywords:[\"violence\",\"weapon\",\"pistol\",\"revolver\"],char:\"🔫\",fitzpatrick_scale:false,category:\"objects\"},bomb:{keywords:[\"boom\",\"explode\",\"explosion\",\"terrorism\"],char:\"💣\",fitzpatrick_scale:false,category:\"objects\"},firecracker:{keywords:[\"dynamite\",\"boom\",\"explode\",\"explosion\",\"explosive\"],char:\"🧨\",fitzpatrick_scale:false,category:\"objects\"},hocho:{keywords:[\"knife\",\"blade\",\"cutlery\",\"kitchen\",\"weapon\"],char:\"🔪\",fitzpatrick_scale:false,category:\"objects\"},dagger:{keywords:[\"weapon\"],char:\"🗡\",fitzpatrick_scale:false,category:\"objects\"},crossed_swords:{keywords:[\"weapon\"],char:\"⚔\",fitzpatrick_scale:false,category:\"objects\"},shield:{keywords:[\"protection\",\"security\"],char:\"🛡\",fitzpatrick_scale:false,category:\"objects\"},smoking:{keywords:[\"kills\",\"tobacco\",\"cigarette\",\"joint\",\"smoke\"],char:\"🚬\",fitzpatrick_scale:false,category:\"objects\"},skull_and_crossbones:{keywords:[\"poison\",\"danger\",\"deadly\",\"scary\",\"death\",\"pirate\",\"evil\"],char:\"☠\",fitzpatrick_scale:false,category:\"objects\"},coffin:{keywords:[\"vampire\",\"dead\",\"die\",\"death\",\"rip\",\"graveyard\",\"cemetery\",\"casket\",\"funeral\",\"box\"],char:\"⚰\",fitzpatrick_scale:false,category:\"objects\"},funeral_urn:{keywords:[\"dead\",\"die\",\"death\",\"rip\",\"ashes\"],char:\"⚱\",fitzpatrick_scale:false,category:\"objects\"},amphora:{keywords:[\"vase\",\"jar\"],char:\"🏺\",fitzpatrick_scale:false,category:\"objects\"},crystal_ball:{keywords:[\"disco\",\"party\",\"magic\",\"circus\",\"fortune_teller\"],char:\"🔮\",fitzpatrick_scale:false,category:\"objects\"},prayer_beads:{keywords:[\"dhikr\",\"religious\"],char:\"📿\",fitzpatrick_scale:false,category:\"objects\"},nazar_amulet:{keywords:[\"bead\",\"charm\"],char:\"🧿\",fitzpatrick_scale:false,category:\"objects\"},barber:{keywords:[\"hair\",\"salon\",\"style\"],char:\"💈\",fitzpatrick_scale:false,category:\"objects\"},alembic:{keywords:[\"distilling\",\"science\",\"experiment\",\"chemistry\"],char:\"⚗\",fitzpatrick_scale:false,category:\"objects\"},telescope:{keywords:[\"stars\",\"space\",\"zoom\",\"science\",\"astronomy\"],char:\"🔭\",fitzpatrick_scale:false,category:\"objects\"},microscope:{keywords:[\"laboratory\",\"experiment\",\"zoomin\",\"science\",\"study\"],char:\"🔬\",fitzpatrick_scale:false,category:\"objects\"},hole:{keywords:[\"embarrassing\"],char:\"🕳\",fitzpatrick_scale:false,category:\"objects\"},pill:{keywords:[\"health\",\"medicine\",\"doctor\",\"pharmacy\",\"drug\"],char:\"💊\",fitzpatrick_scale:false,category:\"objects\"},syringe:{keywords:[\"health\",\"hospital\",\"drugs\",\"blood\",\"medicine\",\"needle\",\"doctor\",\"nurse\"],char:\"💉\",fitzpatrick_scale:false,category:\"objects\"},dna:{keywords:[\"biologist\",\"genetics\",\"life\"],char:\"🧬\",fitzpatrick_scale:false,category:\"objects\"},microbe:{keywords:[\"amoeba\",\"bacteria\",\"germs\"],char:\"🦠\",fitzpatrick_scale:false,category:\"objects\"},petri_dish:{keywords:[\"bacteria\",\"biology\",\"culture\",\"lab\"],char:\"🧫\",fitzpatrick_scale:false,category:\"objects\"},test_tube:{keywords:[\"chemistry\",\"experiment\",\"lab\",\"science\"],char:\"🧪\",fitzpatrick_scale:false,category:\"objects\"},thermometer:{keywords:[\"weather\",\"temperature\",\"hot\",\"cold\"],char:\"🌡\",fitzpatrick_scale:false,category:\"objects\"},broom:{keywords:[\"cleaning\",\"sweeping\",\"witch\"],char:\"🧹\",fitzpatrick_scale:false,category:\"objects\"},basket:{keywords:[\"laundry\"],char:\"🧺\",fitzpatrick_scale:false,category:\"objects\"},toilet_paper:{keywords:[\"roll\"],char:\"🧻\",fitzpatrick_scale:false,category:\"objects\"},label:{keywords:[\"sale\",\"tag\"],char:\"🏷\",fitzpatrick_scale:false,category:\"objects\"},bookmark:{keywords:[\"favorite\",\"label\",\"save\"],char:\"🔖\",fitzpatrick_scale:false,category:\"objects\"},toilet:{keywords:[\"restroom\",\"wc\",\"washroom\",\"bathroom\",\"potty\"],char:\"🚽\",fitzpatrick_scale:false,category:\"objects\"},shower:{keywords:[\"clean\",\"water\",\"bathroom\"],char:\"🚿\",fitzpatrick_scale:false,category:\"objects\"},bathtub:{keywords:[\"clean\",\"shower\",\"bathroom\"],char:\"🛁\",fitzpatrick_scale:false,category:\"objects\"},soap:{keywords:[\"bar\",\"bathing\",\"cleaning\",\"lather\"],char:\"🧼\",fitzpatrick_scale:false,category:\"objects\"},sponge:{keywords:[\"absorbing\",\"cleaning\",\"porous\"],char:\"🧽\",fitzpatrick_scale:false,category:\"objects\"},lotion_bottle:{keywords:[\"moisturizer\",\"sunscreen\"],char:\"🧴\",fitzpatrick_scale:false,category:\"objects\"},key:{keywords:[\"lock\",\"door\",\"password\"],char:\"🔑\",fitzpatrick_scale:false,category:\"objects\"},old_key:{keywords:[\"lock\",\"door\",\"password\"],char:\"🗝\",fitzpatrick_scale:false,category:\"objects\"},couch_and_lamp:{keywords:[\"read\",\"chill\"],char:\"🛋\",fitzpatrick_scale:false,category:\"objects\"},sleeping_bed:{keywords:[\"bed\",\"rest\"],char:\"🛌\",fitzpatrick_scale:true,category:\"objects\"},bed:{keywords:[\"sleep\",\"rest\"],char:\"🛏\",fitzpatrick_scale:false,category:\"objects\"},door:{keywords:[\"house\",\"entry\",\"exit\"],char:\"🚪\",fitzpatrick_scale:false,category:\"objects\"},bellhop_bell:{keywords:[\"service\"],char:\"🛎\",fitzpatrick_scale:false,category:\"objects\"},teddy_bear:{keywords:[\"plush\",\"stuffed\"],char:\"🧸\",fitzpatrick_scale:false,category:\"objects\"},framed_picture:{keywords:[\"photography\"],char:\"🖼\",fitzpatrick_scale:false,category:\"objects\"},world_map:{keywords:[\"location\",\"direction\"],char:\"🗺\",fitzpatrick_scale:false,category:\"objects\"},parasol_on_ground:{keywords:[\"weather\",\"summer\"],char:\"⛱\",fitzpatrick_scale:false,category:\"objects\"},moyai:{keywords:[\"rock\",\"easter island\",\"moai\"],char:\"🗿\",fitzpatrick_scale:false,category:\"objects\"},shopping:{keywords:[\"mall\",\"buy\",\"purchase\"],char:\"🛍\",fitzpatrick_scale:false,category:\"objects\"},shopping_cart:{keywords:[\"trolley\"],char:\"🛒\",fitzpatrick_scale:false,category:\"objects\"},balloon:{keywords:[\"party\",\"celebration\",\"birthday\",\"circus\"],char:\"🎈\",fitzpatrick_scale:false,category:\"objects\"},flags:{keywords:[\"fish\",\"japanese\",\"koinobori\",\"carp\",\"banner\"],char:\"🎏\",fitzpatrick_scale:false,category:\"objects\"},ribbon:{keywords:[\"decoration\",\"pink\",\"girl\",\"bowtie\"],char:\"🎀\",fitzpatrick_scale:false,category:\"objects\"},gift:{keywords:[\"present\",\"birthday\",\"christmas\",\"xmas\"],char:\"🎁\",fitzpatrick_scale:false,category:\"objects\"},confetti_ball:{keywords:[\"festival\",\"party\",\"birthday\",\"circus\"],char:\"🎊\",fitzpatrick_scale:false,category:\"objects\"},tada:{keywords:[\"party\",\"congratulations\",\"birthday\",\"magic\",\"circus\",\"celebration\"],char:\"🎉\",fitzpatrick_scale:false,category:\"objects\"},dolls:{keywords:[\"japanese\",\"toy\",\"kimono\"],char:\"🎎\",fitzpatrick_scale:false,category:\"objects\"},wind_chime:{keywords:[\"nature\",\"ding\",\"spring\",\"bell\"],char:\"🎐\",fitzpatrick_scale:false,category:\"objects\"},crossed_flags:{keywords:[\"japanese\",\"nation\",\"country\",\"border\"],char:\"🎌\",fitzpatrick_scale:false,category:\"objects\"},izakaya_lantern:{keywords:[\"light\",\"paper\",\"halloween\",\"spooky\"],char:\"🏮\",fitzpatrick_scale:false,category:\"objects\"},red_envelope:{keywords:[\"gift\"],char:\"🧧\",fitzpatrick_scale:false,category:\"objects\"},email:{keywords:[\"letter\",\"postal\",\"inbox\",\"communication\"],char:\"✉️\",fitzpatrick_scale:false,category:\"objects\"},envelope_with_arrow:{keywords:[\"email\",\"communication\"],char:\"📩\",fitzpatrick_scale:false,category:\"objects\"},incoming_envelope:{keywords:[\"email\",\"inbox\"],char:\"📨\",fitzpatrick_scale:false,category:\"objects\"},\"e-mail\":{keywords:[\"communication\",\"inbox\"],char:\"📧\",fitzpatrick_scale:false,category:\"objects\"},love_letter:{keywords:[\"email\",\"like\",\"affection\",\"envelope\",\"valentines\"],char:\"💌\",fitzpatrick_scale:false,category:\"objects\"},postbox:{keywords:[\"email\",\"letter\",\"envelope\"],char:\"📮\",fitzpatrick_scale:false,category:\"objects\"},mailbox_closed:{keywords:[\"email\",\"communication\",\"inbox\"],char:\"📪\",fitzpatrick_scale:false,category:\"objects\"},mailbox:{keywords:[\"email\",\"inbox\",\"communication\"],char:\"📫\",fitzpatrick_scale:false,category:\"objects\"},mailbox_with_mail:{keywords:[\"email\",\"inbox\",\"communication\"],char:\"📬\",fitzpatrick_scale:false,category:\"objects\"},mailbox_with_no_mail:{keywords:[\"email\",\"inbox\"],char:\"📭\",fitzpatrick_scale:false,category:\"objects\"},package:{keywords:[\"mail\",\"gift\",\"cardboard\",\"box\",\"moving\"],char:\"📦\",fitzpatrick_scale:false,category:\"objects\"},postal_horn:{keywords:[\"instrument\",\"music\"],char:\"📯\",fitzpatrick_scale:false,category:\"objects\"},inbox_tray:{keywords:[\"email\",\"documents\"],char:\"📥\",fitzpatrick_scale:false,category:\"objects\"},outbox_tray:{keywords:[\"inbox\",\"email\"],char:\"📤\",fitzpatrick_scale:false,category:\"objects\"},scroll:{keywords:[\"documents\",\"ancient\",\"history\",\"paper\"],char:\"📜\",fitzpatrick_scale:false,category:\"objects\"},page_with_curl:{keywords:[\"documents\",\"office\",\"paper\"],char:\"📃\",fitzpatrick_scale:false,category:\"objects\"},bookmark_tabs:{keywords:[\"favorite\",\"save\",\"order\",\"tidy\"],char:\"📑\",fitzpatrick_scale:false,category:\"objects\"},receipt:{keywords:[\"accounting\",\"expenses\"],char:\"🧾\",fitzpatrick_scale:false,category:\"objects\"},bar_chart:{keywords:[\"graph\",\"presentation\",\"stats\"],char:\"📊\",fitzpatrick_scale:false,category:\"objects\"},chart_with_upwards_trend:{keywords:[\"graph\",\"presentation\",\"stats\",\"recovery\",\"business\",\"economics\",\"money\",\"sales\",\"good\",\"success\"],char:\"📈\",fitzpatrick_scale:false,category:\"objects\"},chart_with_downwards_trend:{keywords:[\"graph\",\"presentation\",\"stats\",\"recession\",\"business\",\"economics\",\"money\",\"sales\",\"bad\",\"failure\"],char:\"📉\",fitzpatrick_scale:false,category:\"objects\"},page_facing_up:{keywords:[\"documents\",\"office\",\"paper\",\"information\"],char:\"📄\",fitzpatrick_scale:false,category:\"objects\"},date:{keywords:[\"calendar\",\"schedule\"],char:\"📅\",fitzpatrick_scale:false,category:\"objects\"},calendar:{keywords:[\"schedule\",\"date\",\"planning\"],char:\"📆\",fitzpatrick_scale:false,category:\"objects\"},spiral_calendar:{keywords:[\"date\",\"schedule\",\"planning\"],char:\"🗓\",fitzpatrick_scale:false,category:\"objects\"},card_index:{keywords:[\"business\",\"stationery\"],char:\"📇\",fitzpatrick_scale:false,category:\"objects\"},card_file_box:{keywords:[\"business\",\"stationery\"],char:\"🗃\",fitzpatrick_scale:false,category:\"objects\"},ballot_box:{keywords:[\"election\",\"vote\"],char:\"🗳\",fitzpatrick_scale:false,category:\"objects\"},file_cabinet:{keywords:[\"filing\",\"organizing\"],char:\"🗄\",fitzpatrick_scale:false,category:\"objects\"},clipboard:{keywords:[\"stationery\",\"documents\"],char:\"📋\",fitzpatrick_scale:false,category:\"objects\"},spiral_notepad:{keywords:[\"memo\",\"stationery\"],char:\"🗒\",fitzpatrick_scale:false,category:\"objects\"},file_folder:{keywords:[\"documents\",\"business\",\"office\"],char:\"📁\",fitzpatrick_scale:false,category:\"objects\"},open_file_folder:{keywords:[\"documents\",\"load\"],char:\"📂\",fitzpatrick_scale:false,category:\"objects\"},card_index_dividers:{keywords:[\"organizing\",\"business\",\"stationery\"],char:\"🗂\",fitzpatrick_scale:false,category:\"objects\"},newspaper_roll:{keywords:[\"press\",\"headline\"],char:\"🗞\",fitzpatrick_scale:false,category:\"objects\"},newspaper:{keywords:[\"press\",\"headline\"],char:\"📰\",fitzpatrick_scale:false,category:\"objects\"},notebook:{keywords:[\"stationery\",\"record\",\"notes\",\"paper\",\"study\"],char:\"📓\",fitzpatrick_scale:false,category:\"objects\"},closed_book:{keywords:[\"read\",\"library\",\"knowledge\",\"textbook\",\"learn\"],char:\"📕\",fitzpatrick_scale:false,category:\"objects\"},green_book:{keywords:[\"read\",\"library\",\"knowledge\",\"study\"],char:\"📗\",fitzpatrick_scale:false,category:\"objects\"},blue_book:{keywords:[\"read\",\"library\",\"knowledge\",\"learn\",\"study\"],char:\"📘\",fitzpatrick_scale:false,category:\"objects\"},orange_book:{keywords:[\"read\",\"library\",\"knowledge\",\"textbook\",\"study\"],char:\"📙\",fitzpatrick_scale:false,category:\"objects\"},notebook_with_decorative_cover:{keywords:[\"classroom\",\"notes\",\"record\",\"paper\",\"study\"],char:\"📔\",fitzpatrick_scale:false,category:\"objects\"},ledger:{keywords:[\"notes\",\"paper\"],char:\"📒\",fitzpatrick_scale:false,category:\"objects\"},books:{keywords:[\"literature\",\"library\",\"study\"],char:\"📚\",fitzpatrick_scale:false,category:\"objects\"},open_book:{keywords:[\"book\",\"read\",\"library\",\"knowledge\",\"literature\",\"learn\",\"study\"],char:\"📖\",fitzpatrick_scale:false,category:\"objects\"},safety_pin:{keywords:[\"diaper\"],char:\"🧷\",fitzpatrick_scale:false,category:\"objects\"},link:{keywords:[\"rings\",\"url\"],char:\"🔗\",fitzpatrick_scale:false,category:\"objects\"},paperclip:{keywords:[\"documents\",\"stationery\"],char:\"📎\",fitzpatrick_scale:false,category:\"objects\"},paperclips:{keywords:[\"documents\",\"stationery\"],char:\"🖇\",fitzpatrick_scale:false,category:\"objects\"},scissors:{keywords:[\"stationery\",\"cut\"],char:\"✂️\",fitzpatrick_scale:false,category:\"objects\"},triangular_ruler:{keywords:[\"stationery\",\"math\",\"architect\",\"sketch\"],char:\"📐\",fitzpatrick_scale:false,category:\"objects\"},straight_ruler:{keywords:[\"stationery\",\"calculate\",\"length\",\"math\",\"school\",\"drawing\",\"architect\",\"sketch\"],char:\"📏\",fitzpatrick_scale:false,category:\"objects\"},abacus:{keywords:[\"calculation\"],char:\"🧮\",fitzpatrick_scale:false,category:\"objects\"},pushpin:{keywords:[\"stationery\",\"mark\",\"here\"],char:\"📌\",fitzpatrick_scale:false,category:\"objects\"},round_pushpin:{keywords:[\"stationery\",\"location\",\"map\",\"here\"],char:\"📍\",fitzpatrick_scale:false,category:\"objects\"},triangular_flag_on_post:{keywords:[\"mark\",\"milestone\",\"place\"],char:\"🚩\",fitzpatrick_scale:false,category:\"objects\"},white_flag:{keywords:[\"losing\",\"loser\",\"lost\",\"surrender\",\"give up\",\"fail\"],char:\"🏳\",fitzpatrick_scale:false,category:\"objects\"},black_flag:{keywords:[\"pirate\"],char:\"🏴\",fitzpatrick_scale:false,category:\"objects\"},rainbow_flag:{keywords:[\"flag\",\"rainbow\",\"pride\",\"gay\",\"lgbt\",\"glbt\",\"queer\",\"homosexual\",\"lesbian\",\"bisexual\",\"transgender\"],char:\"🏳️‍🌈\",fitzpatrick_scale:false,category:\"objects\"},closed_lock_with_key:{keywords:[\"security\",\"privacy\"],char:\"🔐\",fitzpatrick_scale:false,category:\"objects\"},lock:{keywords:[\"security\",\"password\",\"padlock\"],char:\"🔒\",fitzpatrick_scale:false,category:\"objects\"},unlock:{keywords:[\"privacy\",\"security\"],char:\"🔓\",fitzpatrick_scale:false,category:\"objects\"},lock_with_ink_pen:{keywords:[\"security\",\"secret\"],char:\"🔏\",fitzpatrick_scale:false,category:\"objects\"},pen:{keywords:[\"stationery\",\"writing\",\"write\"],char:\"🖊\",fitzpatrick_scale:false,category:\"objects\"},fountain_pen:{keywords:[\"stationery\",\"writing\",\"write\"],char:\"🖋\",fitzpatrick_scale:false,category:\"objects\"},black_nib:{keywords:[\"pen\",\"stationery\",\"writing\",\"write\"],char:\"✒️\",fitzpatrick_scale:false,category:\"objects\"},memo:{keywords:[\"write\",\"documents\",\"stationery\",\"pencil\",\"paper\",\"writing\",\"legal\",\"exam\",\"quiz\",\"test\",\"study\",\"compose\"],char:\"📝\",fitzpatrick_scale:false,category:\"objects\"},pencil2:{keywords:[\"stationery\",\"write\",\"paper\",\"writing\",\"school\",\"study\"],char:\"✏️\",fitzpatrick_scale:false,category:\"objects\"},crayon:{keywords:[\"drawing\",\"creativity\"],char:\"🖍\",fitzpatrick_scale:false,category:\"objects\"},paintbrush:{keywords:[\"drawing\",\"creativity\",\"art\"],char:\"🖌\",fitzpatrick_scale:false,category:\"objects\"},mag:{keywords:[\"search\",\"zoom\",\"find\",\"detective\"],char:\"🔍\",fitzpatrick_scale:false,category:\"objects\"},mag_right:{keywords:[\"search\",\"zoom\",\"find\",\"detective\"],char:\"🔎\",fitzpatrick_scale:false,category:\"objects\"},heart:{keywords:[\"love\",\"like\",\"valentines\"],char:\"❤️\",fitzpatrick_scale:false,category:\"symbols\"},orange_heart:{keywords:[\"love\",\"like\",\"affection\",\"valentines\"],char:\"🧡\",fitzpatrick_scale:false,category:\"symbols\"},yellow_heart:{keywords:[\"love\",\"like\",\"affection\",\"valentines\"],char:\"💛\",fitzpatrick_scale:false,category:\"symbols\"},green_heart:{keywords:[\"love\",\"like\",\"affection\",\"valentines\"],char:\"💚\",fitzpatrick_scale:false,category:\"symbols\"},blue_heart:{keywords:[\"love\",\"like\",\"affection\",\"valentines\"],char:\"💙\",fitzpatrick_scale:false,category:\"symbols\"},purple_heart:{keywords:[\"love\",\"like\",\"affection\",\"valentines\"],char:\"💜\",fitzpatrick_scale:false,category:\"symbols\"},black_heart:{keywords:[\"evil\"],char:\"🖤\",fitzpatrick_scale:false,category:\"symbols\"},broken_heart:{keywords:[\"sad\",\"sorry\",\"break\",\"heart\",\"heartbreak\"],char:\"💔\",fitzpatrick_scale:false,category:\"symbols\"},heavy_heart_exclamation:{keywords:[\"decoration\",\"love\"],char:\"❣\",fitzpatrick_scale:false,category:\"symbols\"},two_hearts:{keywords:[\"love\",\"like\",\"affection\",\"valentines\",\"heart\"],char:\"💕\",fitzpatrick_scale:false,category:\"symbols\"},revolving_hearts:{keywords:[\"love\",\"like\",\"affection\",\"valentines\"],char:\"💞\",fitzpatrick_scale:false,category:\"symbols\"},heartbeat:{keywords:[\"love\",\"like\",\"affection\",\"valentines\",\"pink\",\"heart\"],char:\"💓\",fitzpatrick_scale:false,category:\"symbols\"},heartpulse:{keywords:[\"like\",\"love\",\"affection\",\"valentines\",\"pink\"],char:\"💗\",fitzpatrick_scale:false,category:\"symbols\"},sparkling_heart:{keywords:[\"love\",\"like\",\"affection\",\"valentines\"],char:\"💖\",fitzpatrick_scale:false,category:\"symbols\"},cupid:{keywords:[\"love\",\"like\",\"heart\",\"affection\",\"valentines\"],char:\"💘\",fitzpatrick_scale:false,category:\"symbols\"},gift_heart:{keywords:[\"love\",\"valentines\"],char:\"💝\",fitzpatrick_scale:false,category:\"symbols\"},heart_decoration:{keywords:[\"purple-square\",\"love\",\"like\"],char:\"💟\",fitzpatrick_scale:false,category:\"symbols\"},peace_symbol:{keywords:[\"hippie\"],char:\"☮\",fitzpatrick_scale:false,category:\"symbols\"},latin_cross:{keywords:[\"christianity\"],char:\"✝\",fitzpatrick_scale:false,category:\"symbols\"},star_and_crescent:{keywords:[\"islam\"],char:\"☪\",fitzpatrick_scale:false,category:\"symbols\"},om:{keywords:[\"hinduism\",\"buddhism\",\"sikhism\",\"jainism\"],char:\"🕉\",fitzpatrick_scale:false,category:\"symbols\"},wheel_of_dharma:{keywords:[\"hinduism\",\"buddhism\",\"sikhism\",\"jainism\"],char:\"☸\",fitzpatrick_scale:false,category:\"symbols\"},star_of_david:{keywords:[\"judaism\"],char:\"✡\",fitzpatrick_scale:false,category:\"symbols\"},six_pointed_star:{keywords:[\"purple-square\",\"religion\",\"jewish\",\"hexagram\"],char:\"🔯\",fitzpatrick_scale:false,category:\"symbols\"},menorah:{keywords:[\"hanukkah\",\"candles\",\"jewish\"],char:\"🕎\",fitzpatrick_scale:false,category:\"symbols\"},yin_yang:{keywords:[\"balance\"],char:\"☯\",fitzpatrick_scale:false,category:\"symbols\"},orthodox_cross:{keywords:[\"suppedaneum\",\"religion\"],char:\"☦\",fitzpatrick_scale:false,category:\"symbols\"},place_of_worship:{keywords:[\"religion\",\"church\",\"temple\",\"prayer\"],char:\"🛐\",fitzpatrick_scale:false,category:\"symbols\"},ophiuchus:{keywords:[\"sign\",\"purple-square\",\"constellation\",\"astrology\"],char:\"⛎\",fitzpatrick_scale:false,category:\"symbols\"},aries:{keywords:[\"sign\",\"purple-square\",\"zodiac\",\"astrology\"],char:\"♈\",fitzpatrick_scale:false,category:\"symbols\"},taurus:{keywords:[\"purple-square\",\"sign\",\"zodiac\",\"astrology\"],char:\"♉\",fitzpatrick_scale:false,category:\"symbols\"},gemini:{keywords:[\"sign\",\"zodiac\",\"purple-square\",\"astrology\"],char:\"♊\",fitzpatrick_scale:false,category:\"symbols\"},cancer:{keywords:[\"sign\",\"zodiac\",\"purple-square\",\"astrology\"],char:\"♋\",fitzpatrick_scale:false,category:\"symbols\"},leo:{keywords:[\"sign\",\"purple-square\",\"zodiac\",\"astrology\"],char:\"♌\",fitzpatrick_scale:false,category:\"symbols\"},virgo:{keywords:[\"sign\",\"zodiac\",\"purple-square\",\"astrology\"],char:\"♍\",fitzpatrick_scale:false,category:\"symbols\"},libra:{keywords:[\"sign\",\"purple-square\",\"zodiac\",\"astrology\"],char:\"♎\",fitzpatrick_scale:false,category:\"symbols\"},scorpius:{keywords:[\"sign\",\"zodiac\",\"purple-square\",\"astrology\",\"scorpio\"],char:\"♏\",fitzpatrick_scale:false,category:\"symbols\"},sagittarius:{keywords:[\"sign\",\"zodiac\",\"purple-square\",\"astrology\"],char:\"♐\",fitzpatrick_scale:false,category:\"symbols\"},capricorn:{keywords:[\"sign\",\"zodiac\",\"purple-square\",\"astrology\"],char:\"♑\",fitzpatrick_scale:false,category:\"symbols\"},aquarius:{keywords:[\"sign\",\"purple-square\",\"zodiac\",\"astrology\"],char:\"♒\",fitzpatrick_scale:false,category:\"symbols\"},pisces:{keywords:[\"purple-square\",\"sign\",\"zodiac\",\"astrology\"],char:\"♓\",fitzpatrick_scale:false,category:\"symbols\"},id:{keywords:[\"purple-square\",\"words\"],char:\"🆔\",fitzpatrick_scale:false,category:\"symbols\"},atom_symbol:{keywords:[\"science\",\"physics\",\"chemistry\"],char:\"⚛\",fitzpatrick_scale:false,category:\"symbols\"},u7a7a:{keywords:[\"kanji\",\"japanese\",\"chinese\",\"empty\",\"sky\",\"blue-square\"],char:\"🈳\",fitzpatrick_scale:false,category:\"symbols\"},u5272:{keywords:[\"cut\",\"divide\",\"chinese\",\"kanji\",\"pink-square\"],char:\"🈹\",fitzpatrick_scale:false,category:\"symbols\"},radioactive:{keywords:[\"nuclear\",\"danger\"],char:\"☢\",fitzpatrick_scale:false,category:\"symbols\"},biohazard:{keywords:[\"danger\"],char:\"☣\",fitzpatrick_scale:false,category:\"symbols\"},mobile_phone_off:{keywords:[\"mute\",\"orange-square\",\"silence\",\"quiet\"],char:\"📴\",fitzpatrick_scale:false,category:\"symbols\"},vibration_mode:{keywords:[\"orange-square\",\"phone\"],char:\"📳\",fitzpatrick_scale:false,category:\"symbols\"},u6709:{keywords:[\"orange-square\",\"chinese\",\"have\",\"kanji\"],char:\"🈶\",fitzpatrick_scale:false,category:\"symbols\"},u7121:{keywords:[\"nothing\",\"chinese\",\"kanji\",\"japanese\",\"orange-square\"],char:\"🈚\",fitzpatrick_scale:false,category:\"symbols\"},u7533:{keywords:[\"chinese\",\"japanese\",\"kanji\",\"orange-square\"],char:\"🈸\",fitzpatrick_scale:false,category:\"symbols\"},u55b6:{keywords:[\"japanese\",\"opening hours\",\"orange-square\"],char:\"🈺\",fitzpatrick_scale:false,category:\"symbols\"},u6708:{keywords:[\"chinese\",\"month\",\"moon\",\"japanese\",\"orange-square\",\"kanji\"],char:\"🈷️\",fitzpatrick_scale:false,category:\"symbols\"},eight_pointed_black_star:{keywords:[\"orange-square\",\"shape\",\"polygon\"],char:\"✴️\",fitzpatrick_scale:false,category:\"symbols\"},vs:{keywords:[\"words\",\"orange-square\"],char:\"🆚\",fitzpatrick_scale:false,category:\"symbols\"},accept:{keywords:[\"ok\",\"good\",\"chinese\",\"kanji\",\"agree\",\"yes\",\"orange-circle\"],char:\"🉑\",fitzpatrick_scale:false,category:\"symbols\"},white_flower:{keywords:[\"japanese\",\"spring\"],char:\"💮\",fitzpatrick_scale:false,category:\"symbols\"},ideograph_advantage:{keywords:[\"chinese\",\"kanji\",\"obtain\",\"get\",\"circle\"],char:\"🉐\",fitzpatrick_scale:false,category:\"symbols\"},secret:{keywords:[\"privacy\",\"chinese\",\"sshh\",\"kanji\",\"red-circle\"],char:\"㊙️\",fitzpatrick_scale:false,category:\"symbols\"},congratulations:{keywords:[\"chinese\",\"kanji\",\"japanese\",\"red-circle\"],char:\"㊗️\",fitzpatrick_scale:false,category:\"symbols\"},u5408:{keywords:[\"japanese\",\"chinese\",\"join\",\"kanji\",\"red-square\"],char:\"🈴\",fitzpatrick_scale:false,category:\"symbols\"},u6e80:{keywords:[\"full\",\"chinese\",\"japanese\",\"red-square\",\"kanji\"],char:\"🈵\",fitzpatrick_scale:false,category:\"symbols\"},u7981:{keywords:[\"kanji\",\"japanese\",\"chinese\",\"forbidden\",\"limit\",\"restricted\",\"red-square\"],char:\"🈲\",fitzpatrick_scale:false,category:\"symbols\"},a:{keywords:[\"red-square\",\"alphabet\",\"letter\"],char:\"🅰️\",fitzpatrick_scale:false,category:\"symbols\"},b:{keywords:[\"red-square\",\"alphabet\",\"letter\"],char:\"🅱️\",fitzpatrick_scale:false,category:\"symbols\"},ab:{keywords:[\"red-square\",\"alphabet\"],char:\"🆎\",fitzpatrick_scale:false,category:\"symbols\"},cl:{keywords:[\"alphabet\",\"words\",\"red-square\"],char:\"🆑\",fitzpatrick_scale:false,category:\"symbols\"},o2:{keywords:[\"alphabet\",\"red-square\",\"letter\"],char:\"🅾️\",fitzpatrick_scale:false,category:\"symbols\"},sos:{keywords:[\"help\",\"red-square\",\"words\",\"emergency\",\"911\"],char:\"🆘\",fitzpatrick_scale:false,category:\"symbols\"},no_entry:{keywords:[\"limit\",\"security\",\"privacy\",\"bad\",\"denied\",\"stop\",\"circle\"],char:\"⛔\",fitzpatrick_scale:false,category:\"symbols\"},name_badge:{keywords:[\"fire\",\"forbid\"],char:\"📛\",fitzpatrick_scale:false,category:\"symbols\"},no_entry_sign:{keywords:[\"forbid\",\"stop\",\"limit\",\"denied\",\"disallow\",\"circle\"],char:\"🚫\",fitzpatrick_scale:false,category:\"symbols\"},x:{keywords:[\"no\",\"delete\",\"remove\",\"cancel\",\"red\"],char:\"❌\",fitzpatrick_scale:false,category:\"symbols\"},o:{keywords:[\"circle\",\"round\"],char:\"⭕\",fitzpatrick_scale:false,category:\"symbols\"},stop_sign:{keywords:[\"stop\"],char:\"🛑\",fitzpatrick_scale:false,category:\"symbols\"},anger:{keywords:[\"angry\",\"mad\"],char:\"💢\",fitzpatrick_scale:false,category:\"symbols\"},hotsprings:{keywords:[\"bath\",\"warm\",\"relax\"],char:\"♨️\",fitzpatrick_scale:false,category:\"symbols\"},no_pedestrians:{keywords:[\"rules\",\"crossing\",\"walking\",\"circle\"],char:\"🚷\",fitzpatrick_scale:false,category:\"symbols\"},do_not_litter:{keywords:[\"trash\",\"bin\",\"garbage\",\"circle\"],char:\"🚯\",fitzpatrick_scale:false,category:\"symbols\"},no_bicycles:{keywords:[\"cyclist\",\"prohibited\",\"circle\"],char:\"🚳\",fitzpatrick_scale:false,category:\"symbols\"},\"non-potable_water\":{keywords:[\"drink\",\"faucet\",\"tap\",\"circle\"],char:\"🚱\",fitzpatrick_scale:false,category:\"symbols\"},underage:{keywords:[\"18\",\"drink\",\"pub\",\"night\",\"minor\",\"circle\"],char:\"🔞\",fitzpatrick_scale:false,category:\"symbols\"},no_mobile_phones:{keywords:[\"iphone\",\"mute\",\"circle\"],char:\"📵\",fitzpatrick_scale:false,category:\"symbols\"},exclamation:{keywords:[\"heavy_exclamation_mark\",\"danger\",\"surprise\",\"punctuation\",\"wow\",\"warning\"],char:\"❗\",fitzpatrick_scale:false,category:\"symbols\"},grey_exclamation:{keywords:[\"surprise\",\"punctuation\",\"gray\",\"wow\",\"warning\"],char:\"❕\",fitzpatrick_scale:false,category:\"symbols\"},question:{keywords:[\"doubt\",\"confused\"],char:\"❓\",fitzpatrick_scale:false,category:\"symbols\"},grey_question:{keywords:[\"doubts\",\"gray\",\"huh\",\"confused\"],char:\"❔\",fitzpatrick_scale:false,category:\"symbols\"},bangbang:{keywords:[\"exclamation\",\"surprise\"],char:\"‼️\",fitzpatrick_scale:false,category:\"symbols\"},interrobang:{keywords:[\"wat\",\"punctuation\",\"surprise\"],char:\"⁉️\",fitzpatrick_scale:false,category:\"symbols\"},100:{keywords:[\"score\",\"perfect\",\"numbers\",\"century\",\"exam\",\"quiz\",\"test\",\"pass\",\"hundred\"],char:\"💯\",fitzpatrick_scale:false,category:\"symbols\"},low_brightness:{keywords:[\"sun\",\"afternoon\",\"warm\",\"summer\"],char:\"🔅\",fitzpatrick_scale:false,category:\"symbols\"},high_brightness:{keywords:[\"sun\",\"light\"],char:\"🔆\",fitzpatrick_scale:false,category:\"symbols\"},trident:{keywords:[\"weapon\",\"spear\"],char:\"🔱\",fitzpatrick_scale:false,category:\"symbols\"},fleur_de_lis:{keywords:[\"decorative\",\"scout\"],char:\"⚜\",fitzpatrick_scale:false,category:\"symbols\"},part_alternation_mark:{keywords:[\"graph\",\"presentation\",\"stats\",\"business\",\"economics\",\"bad\"],char:\"〽️\",fitzpatrick_scale:false,category:\"symbols\"},warning:{keywords:[\"exclamation\",\"wip\",\"alert\",\"error\",\"problem\",\"issue\"],char:\"⚠️\",fitzpatrick_scale:false,category:\"symbols\"},children_crossing:{keywords:[\"school\",\"warning\",\"danger\",\"sign\",\"driving\",\"yellow-diamond\"],char:\"🚸\",fitzpatrick_scale:false,category:\"symbols\"},beginner:{keywords:[\"badge\",\"shield\"],char:\"🔰\",fitzpatrick_scale:false,category:\"symbols\"},recycle:{keywords:[\"arrow\",\"environment\",\"garbage\",\"trash\"],char:\"♻️\",fitzpatrick_scale:false,category:\"symbols\"},u6307:{keywords:[\"chinese\",\"point\",\"green-square\",\"kanji\"],char:\"🈯\",fitzpatrick_scale:false,category:\"symbols\"},chart:{keywords:[\"green-square\",\"graph\",\"presentation\",\"stats\"],char:\"💹\",fitzpatrick_scale:false,category:\"symbols\"},sparkle:{keywords:[\"stars\",\"green-square\",\"awesome\",\"good\",\"fireworks\"],char:\"❇️\",fitzpatrick_scale:false,category:\"symbols\"},eight_spoked_asterisk:{keywords:[\"star\",\"sparkle\",\"green-square\"],char:\"✳️\",fitzpatrick_scale:false,category:\"symbols\"},negative_squared_cross_mark:{keywords:[\"x\",\"green-square\",\"no\",\"deny\"],char:\"❎\",fitzpatrick_scale:false,category:\"symbols\"},white_check_mark:{keywords:[\"green-square\",\"ok\",\"agree\",\"vote\",\"election\",\"answer\",\"tick\"],char:\"✅\",fitzpatrick_scale:false,category:\"symbols\"},diamond_shape_with_a_dot_inside:{keywords:[\"jewel\",\"blue\",\"gem\",\"crystal\",\"fancy\"],char:\"💠\",fitzpatrick_scale:false,category:\"symbols\"},cyclone:{keywords:[\"weather\",\"swirl\",\"blue\",\"cloud\",\"vortex\",\"spiral\",\"whirlpool\",\"spin\",\"tornado\",\"hurricane\",\"typhoon\"],char:\"🌀\",fitzpatrick_scale:false,category:\"symbols\"},loop:{keywords:[\"tape\",\"cassette\"],char:\"➿\",fitzpatrick_scale:false,category:\"symbols\"},globe_with_meridians:{keywords:[\"earth\",\"international\",\"world\",\"internet\",\"interweb\",\"i18n\"],char:\"🌐\",fitzpatrick_scale:false,category:\"symbols\"},m:{keywords:[\"alphabet\",\"blue-circle\",\"letter\"],char:\"Ⓜ️\",fitzpatrick_scale:false,category:\"symbols\"},atm:{keywords:[\"money\",\"sales\",\"cash\",\"blue-square\",\"payment\",\"bank\"],char:\"🏧\",fitzpatrick_scale:false,category:\"symbols\"},sa:{keywords:[\"japanese\",\"blue-square\",\"katakana\"],char:\"🈂️\",fitzpatrick_scale:false,category:\"symbols\"},passport_control:{keywords:[\"custom\",\"blue-square\"],char:\"🛂\",fitzpatrick_scale:false,category:\"symbols\"},customs:{keywords:[\"passport\",\"border\",\"blue-square\"],char:\"🛃\",fitzpatrick_scale:false,category:\"symbols\"},baggage_claim:{keywords:[\"blue-square\",\"airport\",\"transport\"],char:\"🛄\",fitzpatrick_scale:false,category:\"symbols\"},left_luggage:{keywords:[\"blue-square\",\"travel\"],char:\"🛅\",fitzpatrick_scale:false,category:\"symbols\"},wheelchair:{keywords:[\"blue-square\",\"disabled\",\"a11y\",\"accessibility\"],char:\"♿\",fitzpatrick_scale:false,category:\"symbols\"},no_smoking:{keywords:[\"cigarette\",\"blue-square\",\"smell\",\"smoke\"],char:\"🚭\",fitzpatrick_scale:false,category:\"symbols\"},wc:{keywords:[\"toilet\",\"restroom\",\"blue-square\"],char:\"🚾\",fitzpatrick_scale:false,category:\"symbols\"},parking:{keywords:[\"cars\",\"blue-square\",\"alphabet\",\"letter\"],char:\"🅿️\",fitzpatrick_scale:false,category:\"symbols\"},potable_water:{keywords:[\"blue-square\",\"liquid\",\"restroom\",\"cleaning\",\"faucet\"],char:\"🚰\",fitzpatrick_scale:false,category:\"symbols\"},mens:{keywords:[\"toilet\",\"restroom\",\"wc\",\"blue-square\",\"gender\",\"male\"],char:\"🚹\",fitzpatrick_scale:false,category:\"symbols\"},womens:{keywords:[\"purple-square\",\"woman\",\"female\",\"toilet\",\"loo\",\"restroom\",\"gender\"],char:\"🚺\",fitzpatrick_scale:false,category:\"symbols\"},baby_symbol:{keywords:[\"orange-square\",\"child\"],char:\"🚼\",fitzpatrick_scale:false,category:\"symbols\"},restroom:{keywords:[\"blue-square\",\"toilet\",\"refresh\",\"wc\",\"gender\"],char:\"🚻\",fitzpatrick_scale:false,category:\"symbols\"},put_litter_in_its_place:{keywords:[\"blue-square\",\"sign\",\"human\",\"info\"],char:\"🚮\",fitzpatrick_scale:false,category:\"symbols\"},cinema:{keywords:[\"blue-square\",\"record\",\"film\",\"movie\",\"curtain\",\"stage\",\"theater\"],char:\"🎦\",fitzpatrick_scale:false,category:\"symbols\"},signal_strength:{keywords:[\"blue-square\",\"reception\",\"phone\",\"internet\",\"connection\",\"wifi\",\"bluetooth\",\"bars\"],char:\"📶\",fitzpatrick_scale:false,category:\"symbols\"},koko:{keywords:[\"blue-square\",\"here\",\"katakana\",\"japanese\",\"destination\"],char:\"🈁\",fitzpatrick_scale:false,category:\"symbols\"},ng:{keywords:[\"blue-square\",\"words\",\"shape\",\"icon\"],char:\"🆖\",fitzpatrick_scale:false,category:\"symbols\"},ok:{keywords:[\"good\",\"agree\",\"yes\",\"blue-square\"],char:\"🆗\",fitzpatrick_scale:false,category:\"symbols\"},up:{keywords:[\"blue-square\",\"above\",\"high\"],char:\"🆙\",fitzpatrick_scale:false,category:\"symbols\"},cool:{keywords:[\"words\",\"blue-square\"],char:\"🆒\",fitzpatrick_scale:false,category:\"symbols\"},new:{keywords:[\"blue-square\",\"words\",\"start\"],char:\"🆕\",fitzpatrick_scale:false,category:\"symbols\"},free:{keywords:[\"blue-square\",\"words\"],char:\"🆓\",fitzpatrick_scale:false,category:\"symbols\"},zero:{keywords:[\"0\",\"numbers\",\"blue-square\",\"null\"],char:\"0️⃣\",fitzpatrick_scale:false,category:\"symbols\"},one:{keywords:[\"blue-square\",\"numbers\",\"1\"],char:\"1️⃣\",fitzpatrick_scale:false,category:\"symbols\"},two:{keywords:[\"numbers\",\"2\",\"prime\",\"blue-square\"],char:\"2️⃣\",fitzpatrick_scale:false,category:\"symbols\"},three:{keywords:[\"3\",\"numbers\",\"prime\",\"blue-square\"],char:\"3️⃣\",fitzpatrick_scale:false,category:\"symbols\"},four:{keywords:[\"4\",\"numbers\",\"blue-square\"],char:\"4️⃣\",fitzpatrick_scale:false,category:\"symbols\"},five:{keywords:[\"5\",\"numbers\",\"blue-square\",\"prime\"],char:\"5️⃣\",fitzpatrick_scale:false,category:\"symbols\"},six:{keywords:[\"6\",\"numbers\",\"blue-square\"],char:\"6️⃣\",fitzpatrick_scale:false,category:\"symbols\"},seven:{keywords:[\"7\",\"numbers\",\"blue-square\",\"prime\"],char:\"7️⃣\",fitzpatrick_scale:false,category:\"symbols\"},eight:{keywords:[\"8\",\"blue-square\",\"numbers\"],char:\"8️⃣\",fitzpatrick_scale:false,category:\"symbols\"},nine:{keywords:[\"blue-square\",\"numbers\",\"9\"],char:\"9️⃣\",fitzpatrick_scale:false,category:\"symbols\"},keycap_ten:{keywords:[\"numbers\",\"10\",\"blue-square\"],char:\"🔟\",fitzpatrick_scale:false,category:\"symbols\"},asterisk:{keywords:[\"star\",\"keycap\"],char:\"*⃣\",fitzpatrick_scale:false,category:\"symbols\"},1234:{keywords:[\"numbers\",\"blue-square\"],char:\"🔢\",fitzpatrick_scale:false,category:\"symbols\"},eject_button:{keywords:[\"blue-square\"],char:\"⏏️\",fitzpatrick_scale:false,category:\"symbols\"},arrow_forward:{keywords:[\"blue-square\",\"right\",\"direction\",\"play\"],char:\"▶️\",fitzpatrick_scale:false,category:\"symbols\"},pause_button:{keywords:[\"pause\",\"blue-square\"],char:\"⏸\",fitzpatrick_scale:false,category:\"symbols\"},next_track_button:{keywords:[\"forward\",\"next\",\"blue-square\"],char:\"⏭\",fitzpatrick_scale:false,category:\"symbols\"},stop_button:{keywords:[\"blue-square\"],char:\"⏹\",fitzpatrick_scale:false,category:\"symbols\"},record_button:{keywords:[\"blue-square\"],char:\"⏺\",fitzpatrick_scale:false,category:\"symbols\"},play_or_pause_button:{keywords:[\"blue-square\",\"play\",\"pause\"],char:\"⏯\",fitzpatrick_scale:false,category:\"symbols\"},previous_track_button:{keywords:[\"backward\"],char:\"⏮\",fitzpatrick_scale:false,category:\"symbols\"},fast_forward:{keywords:[\"blue-square\",\"play\",\"speed\",\"continue\"],char:\"⏩\",fitzpatrick_scale:false,category:\"symbols\"},rewind:{keywords:[\"play\",\"blue-square\"],char:\"⏪\",fitzpatrick_scale:false,category:\"symbols\"},twisted_rightwards_arrows:{keywords:[\"blue-square\",\"shuffle\",\"music\",\"random\"],char:\"🔀\",fitzpatrick_scale:false,category:\"symbols\"},repeat:{keywords:[\"loop\",\"record\"],char:\"🔁\",fitzpatrick_scale:false,category:\"symbols\"},repeat_one:{keywords:[\"blue-square\",\"loop\"],char:\"🔂\",fitzpatrick_scale:false,category:\"symbols\"},arrow_backward:{keywords:[\"blue-square\",\"left\",\"direction\"],char:\"◀️\",fitzpatrick_scale:false,category:\"symbols\"},arrow_up_small:{keywords:[\"blue-square\",\"triangle\",\"direction\",\"point\",\"forward\",\"top\"],char:\"🔼\",fitzpatrick_scale:false,category:\"symbols\"},arrow_down_small:{keywords:[\"blue-square\",\"direction\",\"bottom\"],char:\"🔽\",fitzpatrick_scale:false,category:\"symbols\"},arrow_double_up:{keywords:[\"blue-square\",\"direction\",\"top\"],char:\"⏫\",fitzpatrick_scale:false,category:\"symbols\"},arrow_double_down:{keywords:[\"blue-square\",\"direction\",\"bottom\"],char:\"⏬\",fitzpatrick_scale:false,category:\"symbols\"},arrow_right:{keywords:[\"blue-square\",\"next\"],char:\"➡️\",fitzpatrick_scale:false,category:\"symbols\"},arrow_left:{keywords:[\"blue-square\",\"previous\",\"back\"],char:\"⬅️\",fitzpatrick_scale:false,category:\"symbols\"},arrow_up:{keywords:[\"blue-square\",\"continue\",\"top\",\"direction\"],char:\"⬆️\",fitzpatrick_scale:false,category:\"symbols\"},arrow_down:{keywords:[\"blue-square\",\"direction\",\"bottom\"],char:\"⬇️\",fitzpatrick_scale:false,category:\"symbols\"},arrow_upper_right:{keywords:[\"blue-square\",\"point\",\"direction\",\"diagonal\",\"northeast\"],char:\"↗️\",fitzpatrick_scale:false,category:\"symbols\"},arrow_lower_right:{keywords:[\"blue-square\",\"direction\",\"diagonal\",\"southeast\"],char:\"↘️\",fitzpatrick_scale:false,category:\"symbols\"},arrow_lower_left:{keywords:[\"blue-square\",\"direction\",\"diagonal\",\"southwest\"],char:\"↙️\",fitzpatrick_scale:false,category:\"symbols\"},arrow_upper_left:{keywords:[\"blue-square\",\"point\",\"direction\",\"diagonal\",\"northwest\"],char:\"↖️\",fitzpatrick_scale:false,category:\"symbols\"},arrow_up_down:{keywords:[\"blue-square\",\"direction\",\"way\",\"vertical\"],char:\"↕️\",fitzpatrick_scale:false,category:\"symbols\"},left_right_arrow:{keywords:[\"shape\",\"direction\",\"horizontal\",\"sideways\"],char:\"↔️\",fitzpatrick_scale:false,category:\"symbols\"},arrows_counterclockwise:{keywords:[\"blue-square\",\"sync\",\"cycle\"],char:\"🔄\",fitzpatrick_scale:false,category:\"symbols\"},arrow_right_hook:{keywords:[\"blue-square\",\"return\",\"rotate\",\"direction\"],char:\"↪️\",fitzpatrick_scale:false,category:\"symbols\"},leftwards_arrow_with_hook:{keywords:[\"back\",\"return\",\"blue-square\",\"undo\",\"enter\"],char:\"↩️\",fitzpatrick_scale:false,category:\"symbols\"},arrow_heading_up:{keywords:[\"blue-square\",\"direction\",\"top\"],char:\"⤴️\",fitzpatrick_scale:false,category:\"symbols\"},arrow_heading_down:{keywords:[\"blue-square\",\"direction\",\"bottom\"],char:\"⤵️\",fitzpatrick_scale:false,category:\"symbols\"},hash:{keywords:[\"symbol\",\"blue-square\",\"twitter\"],char:\"#️⃣\",fitzpatrick_scale:false,category:\"symbols\"},information_source:{keywords:[\"blue-square\",\"alphabet\",\"letter\"],char:\"ℹ️\",fitzpatrick_scale:false,category:\"symbols\"},abc:{keywords:[\"blue-square\",\"alphabet\"],char:\"🔤\",fitzpatrick_scale:false,category:\"symbols\"},abcd:{keywords:[\"blue-square\",\"alphabet\"],char:\"🔡\",fitzpatrick_scale:false,category:\"symbols\"},capital_abcd:{keywords:[\"alphabet\",\"words\",\"blue-square\"],char:\"🔠\",fitzpatrick_scale:false,category:\"symbols\"},symbols:{keywords:[\"blue-square\",\"music\",\"note\",\"ampersand\",\"percent\",\"glyphs\",\"characters\"],char:\"🔣\",fitzpatrick_scale:false,category:\"symbols\"},musical_note:{keywords:[\"score\",\"tone\",\"sound\"],char:\"🎵\",fitzpatrick_scale:false,category:\"symbols\"},notes:{keywords:[\"music\",\"score\"],char:\"🎶\",fitzpatrick_scale:false,category:\"symbols\"},wavy_dash:{keywords:[\"draw\",\"line\",\"moustache\",\"mustache\",\"squiggle\",\"scribble\"],char:\"〰️\",fitzpatrick_scale:false,category:\"symbols\"},curly_loop:{keywords:[\"scribble\",\"draw\",\"shape\",\"squiggle\"],char:\"➰\",fitzpatrick_scale:false,category:\"symbols\"},heavy_check_mark:{keywords:[\"ok\",\"nike\",\"answer\",\"yes\",\"tick\"],char:\"✔️\",fitzpatrick_scale:false,category:\"symbols\"},arrows_clockwise:{keywords:[\"sync\",\"cycle\",\"round\",\"repeat\"],char:\"🔃\",fitzpatrick_scale:false,category:\"symbols\"},heavy_plus_sign:{keywords:[\"math\",\"calculation\",\"addition\",\"more\",\"increase\"],char:\"➕\",fitzpatrick_scale:false,category:\"symbols\"},heavy_minus_sign:{keywords:[\"math\",\"calculation\",\"subtract\",\"less\"],char:\"➖\",fitzpatrick_scale:false,category:\"symbols\"},heavy_division_sign:{keywords:[\"divide\",\"math\",\"calculation\"],char:\"➗\",fitzpatrick_scale:false,category:\"symbols\"},heavy_multiplication_x:{keywords:[\"math\",\"calculation\"],char:\"✖️\",fitzpatrick_scale:false,category:\"symbols\"},infinity:{keywords:[\"forever\"],char:\"♾\",fitzpatrick_scale:false,category:\"symbols\"},heavy_dollar_sign:{keywords:[\"money\",\"sales\",\"payment\",\"currency\",\"buck\"],char:\"💲\",fitzpatrick_scale:false,category:\"symbols\"},currency_exchange:{keywords:[\"money\",\"sales\",\"dollar\",\"travel\"],char:\"💱\",fitzpatrick_scale:false,category:\"symbols\"},copyright:{keywords:[\"ip\",\"license\",\"circle\",\"law\",\"legal\"],char:\"©️\",fitzpatrick_scale:false,category:\"symbols\"},registered:{keywords:[\"alphabet\",\"circle\"],char:\"®️\",fitzpatrick_scale:false,category:\"symbols\"},tm:{keywords:[\"trademark\",\"brand\",\"law\",\"legal\"],char:\"™️\",fitzpatrick_scale:false,category:\"symbols\"},end:{keywords:[\"words\",\"arrow\"],char:\"🔚\",fitzpatrick_scale:false,category:\"symbols\"},back:{keywords:[\"arrow\",\"words\",\"return\"],char:\"🔙\",fitzpatrick_scale:false,category:\"symbols\"},on:{keywords:[\"arrow\",\"words\"],char:\"🔛\",fitzpatrick_scale:false,category:\"symbols\"},top:{keywords:[\"words\",\"blue-square\"],char:\"🔝\",fitzpatrick_scale:false,category:\"symbols\"},soon:{keywords:[\"arrow\",\"words\"],char:\"🔜\",fitzpatrick_scale:false,category:\"symbols\"},ballot_box_with_check:{keywords:[\"ok\",\"agree\",\"confirm\",\"black-square\",\"vote\",\"election\",\"yes\",\"tick\"],char:\"☑️\",fitzpatrick_scale:false,category:\"symbols\"},radio_button:{keywords:[\"input\",\"old\",\"music\",\"circle\"],char:\"🔘\",fitzpatrick_scale:false,category:\"symbols\"},white_circle:{keywords:[\"shape\",\"round\"],char:\"⚪\",fitzpatrick_scale:false,category:\"symbols\"},black_circle:{keywords:[\"shape\",\"button\",\"round\"],char:\"⚫\",fitzpatrick_scale:false,category:\"symbols\"},red_circle:{keywords:[\"shape\",\"error\",\"danger\"],char:\"🔴\",fitzpatrick_scale:false,category:\"symbols\"},large_blue_circle:{keywords:[\"shape\",\"icon\",\"button\"],char:\"🔵\",fitzpatrick_scale:false,category:\"symbols\"},small_orange_diamond:{keywords:[\"shape\",\"jewel\",\"gem\"],char:\"🔸\",fitzpatrick_scale:false,category:\"symbols\"},small_blue_diamond:{keywords:[\"shape\",\"jewel\",\"gem\"],char:\"🔹\",fitzpatrick_scale:false,category:\"symbols\"},large_orange_diamond:{keywords:[\"shape\",\"jewel\",\"gem\"],char:\"🔶\",fitzpatrick_scale:false,category:\"symbols\"},large_blue_diamond:{keywords:[\"shape\",\"jewel\",\"gem\"],char:\"🔷\",fitzpatrick_scale:false,category:\"symbols\"},small_red_triangle:{keywords:[\"shape\",\"direction\",\"up\",\"top\"],char:\"🔺\",fitzpatrick_scale:false,category:\"symbols\"},black_small_square:{keywords:[\"shape\",\"icon\"],char:\"▪️\",fitzpatrick_scale:false,category:\"symbols\"},white_small_square:{keywords:[\"shape\",\"icon\"],char:\"▫️\",fitzpatrick_scale:false,category:\"symbols\"},black_large_square:{keywords:[\"shape\",\"icon\",\"button\"],char:\"⬛\",fitzpatrick_scale:false,category:\"symbols\"},white_large_square:{keywords:[\"shape\",\"icon\",\"stone\",\"button\"],char:\"⬜\",fitzpatrick_scale:false,category:\"symbols\"},small_red_triangle_down:{keywords:[\"shape\",\"direction\",\"bottom\"],char:\"🔻\",fitzpatrick_scale:false,category:\"symbols\"},black_medium_square:{keywords:[\"shape\",\"button\",\"icon\"],char:\"◼️\",fitzpatrick_scale:false,category:\"symbols\"},white_medium_square:{keywords:[\"shape\",\"stone\",\"icon\"],char:\"◻️\",fitzpatrick_scale:false,category:\"symbols\"},black_medium_small_square:{keywords:[\"icon\",\"shape\",\"button\"],char:\"◾\",fitzpatrick_scale:false,category:\"symbols\"},white_medium_small_square:{keywords:[\"shape\",\"stone\",\"icon\",\"button\"],char:\"◽\",fitzpatrick_scale:false,category:\"symbols\"},black_square_button:{keywords:[\"shape\",\"input\",\"frame\"],char:\"🔲\",fitzpatrick_scale:false,category:\"symbols\"},white_square_button:{keywords:[\"shape\",\"input\"],char:\"🔳\",fitzpatrick_scale:false,category:\"symbols\"},speaker:{keywords:[\"sound\",\"volume\",\"silence\",\"broadcast\"],char:\"🔈\",fitzpatrick_scale:false,category:\"symbols\"},sound:{keywords:[\"volume\",\"speaker\",\"broadcast\"],char:\"🔉\",fitzpatrick_scale:false,category:\"symbols\"},loud_sound:{keywords:[\"volume\",\"noise\",\"noisy\",\"speaker\",\"broadcast\"],char:\"🔊\",fitzpatrick_scale:false,category:\"symbols\"},mute:{keywords:[\"sound\",\"volume\",\"silence\",\"quiet\"],char:\"🔇\",fitzpatrick_scale:false,category:\"symbols\"},mega:{keywords:[\"sound\",\"speaker\",\"volume\"],char:\"📣\",fitzpatrick_scale:false,category:\"symbols\"},loudspeaker:{keywords:[\"volume\",\"sound\"],char:\"📢\",fitzpatrick_scale:false,category:\"symbols\"},bell:{keywords:[\"sound\",\"notification\",\"christmas\",\"xmas\",\"chime\"],char:\"🔔\",fitzpatrick_scale:false,category:\"symbols\"},no_bell:{keywords:[\"sound\",\"volume\",\"mute\",\"quiet\",\"silent\"],char:\"🔕\",fitzpatrick_scale:false,category:\"symbols\"},black_joker:{keywords:[\"poker\",\"cards\",\"game\",\"play\",\"magic\"],char:\"🃏\",fitzpatrick_scale:false,category:\"symbols\"},mahjong:{keywords:[\"game\",\"play\",\"chinese\",\"kanji\"],char:\"🀄\",fitzpatrick_scale:false,category:\"symbols\"},spades:{keywords:[\"poker\",\"cards\",\"suits\",\"magic\"],char:\"♠️\",fitzpatrick_scale:false,category:\"symbols\"},clubs:{keywords:[\"poker\",\"cards\",\"magic\",\"suits\"],char:\"♣️\",fitzpatrick_scale:false,category:\"symbols\"},hearts:{keywords:[\"poker\",\"cards\",\"magic\",\"suits\"],char:\"♥️\",fitzpatrick_scale:false,category:\"symbols\"},diamonds:{keywords:[\"poker\",\"cards\",\"magic\",\"suits\"],char:\"♦️\",fitzpatrick_scale:false,category:\"symbols\"},flower_playing_cards:{keywords:[\"game\",\"sunset\",\"red\"],char:\"🎴\",fitzpatrick_scale:false,category:\"symbols\"},thought_balloon:{keywords:[\"bubble\",\"cloud\",\"speech\",\"thinking\",\"dream\"],char:\"💭\",fitzpatrick_scale:false,category:\"symbols\"},right_anger_bubble:{keywords:[\"caption\",\"speech\",\"thinking\",\"mad\"],char:\"🗯\",fitzpatrick_scale:false,category:\"symbols\"},speech_balloon:{keywords:[\"bubble\",\"words\",\"message\",\"talk\",\"chatting\"],char:\"💬\",fitzpatrick_scale:false,category:\"symbols\"},left_speech_bubble:{keywords:[\"words\",\"message\",\"talk\",\"chatting\"],char:\"🗨\",fitzpatrick_scale:false,category:\"symbols\"},clock1:{keywords:[\"time\",\"late\",\"early\",\"schedule\"],char:\"🕐\",fitzpatrick_scale:false,category:\"symbols\"},clock2:{keywords:[\"time\",\"late\",\"early\",\"schedule\"],char:\"🕑\",fitzpatrick_scale:false,category:\"symbols\"},clock3:{keywords:[\"time\",\"late\",\"early\",\"schedule\"],char:\"🕒\",fitzpatrick_scale:false,category:\"symbols\"},clock4:{keywords:[\"time\",\"late\",\"early\",\"schedule\"],char:\"🕓\",fitzpatrick_scale:false,category:\"symbols\"},clock5:{keywords:[\"time\",\"late\",\"early\",\"schedule\"],char:\"🕔\",fitzpatrick_scale:false,category:\"symbols\"},clock6:{keywords:[\"time\",\"late\",\"early\",\"schedule\",\"dawn\",\"dusk\"],char:\"🕕\",fitzpatrick_scale:false,category:\"symbols\"},clock7:{keywords:[\"time\",\"late\",\"early\",\"schedule\"],char:\"🕖\",fitzpatrick_scale:false,category:\"symbols\"},clock8:{keywords:[\"time\",\"late\",\"early\",\"schedule\"],char:\"🕗\",fitzpatrick_scale:false,category:\"symbols\"},clock9:{keywords:[\"time\",\"late\",\"early\",\"schedule\"],char:\"🕘\",fitzpatrick_scale:false,category:\"symbols\"},clock10:{keywords:[\"time\",\"late\",\"early\",\"schedule\"],char:\"🕙\",fitzpatrick_scale:false,category:\"symbols\"},clock11:{keywords:[\"time\",\"late\",\"early\",\"schedule\"],char:\"🕚\",fitzpatrick_scale:false,category:\"symbols\"},clock12:{keywords:[\"time\",\"noon\",\"midnight\",\"midday\",\"late\",\"early\",\"schedule\"],char:\"🕛\",fitzpatrick_scale:false,category:\"symbols\"},clock130:{keywords:[\"time\",\"late\",\"early\",\"schedule\"],char:\"🕜\",fitzpatrick_scale:false,category:\"symbols\"},clock230:{keywords:[\"time\",\"late\",\"early\",\"schedule\"],char:\"🕝\",fitzpatrick_scale:false,category:\"symbols\"},clock330:{keywords:[\"time\",\"late\",\"early\",\"schedule\"],char:\"🕞\",fitzpatrick_scale:false,category:\"symbols\"},clock430:{keywords:[\"time\",\"late\",\"early\",\"schedule\"],char:\"🕟\",fitzpatrick_scale:false,category:\"symbols\"},clock530:{keywords:[\"time\",\"late\",\"early\",\"schedule\"],char:\"🕠\",fitzpatrick_scale:false,category:\"symbols\"},clock630:{keywords:[\"time\",\"late\",\"early\",\"schedule\"],char:\"🕡\",fitzpatrick_scale:false,category:\"symbols\"},clock730:{keywords:[\"time\",\"late\",\"early\",\"schedule\"],char:\"🕢\",fitzpatrick_scale:false,category:\"symbols\"},clock830:{keywords:[\"time\",\"late\",\"early\",\"schedule\"],char:\"🕣\",fitzpatrick_scale:false,category:\"symbols\"},clock930:{keywords:[\"time\",\"late\",\"early\",\"schedule\"],char:\"🕤\",fitzpatrick_scale:false,category:\"symbols\"},clock1030:{keywords:[\"time\",\"late\",\"early\",\"schedule\"],char:\"🕥\",fitzpatrick_scale:false,category:\"symbols\"},clock1130:{keywords:[\"time\",\"late\",\"early\",\"schedule\"],char:\"🕦\",fitzpatrick_scale:false,category:\"symbols\"},clock1230:{keywords:[\"time\",\"late\",\"early\",\"schedule\"],char:\"🕧\",fitzpatrick_scale:false,category:\"symbols\"},afghanistan:{keywords:[\"af\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇦🇫\",fitzpatrick_scale:false,category:\"flags\"},aland_islands:{keywords:[\"Åland\",\"islands\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇦🇽\",fitzpatrick_scale:false,category:\"flags\"},albania:{keywords:[\"al\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇦🇱\",fitzpatrick_scale:false,category:\"flags\"},algeria:{keywords:[\"dz\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇩🇿\",fitzpatrick_scale:false,category:\"flags\"},american_samoa:{keywords:[\"american\",\"ws\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇦🇸\",fitzpatrick_scale:false,category:\"flags\"},andorra:{keywords:[\"ad\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇦🇩\",fitzpatrick_scale:false,category:\"flags\"},angola:{keywords:[\"ao\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇦🇴\",fitzpatrick_scale:false,category:\"flags\"},anguilla:{keywords:[\"ai\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇦🇮\",fitzpatrick_scale:false,category:\"flags\"},antarctica:{keywords:[\"aq\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇦🇶\",fitzpatrick_scale:false,category:\"flags\"},antigua_barbuda:{keywords:[\"antigua\",\"barbuda\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇦🇬\",fitzpatrick_scale:false,category:\"flags\"},argentina:{keywords:[\"ar\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇦🇷\",fitzpatrick_scale:false,category:\"flags\"},armenia:{keywords:[\"am\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇦🇲\",fitzpatrick_scale:false,category:\"flags\"},aruba:{keywords:[\"aw\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇦🇼\",fitzpatrick_scale:false,category:\"flags\"},australia:{keywords:[\"au\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇦🇺\",fitzpatrick_scale:false,category:\"flags\"},austria:{keywords:[\"at\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇦🇹\",fitzpatrick_scale:false,category:\"flags\"},azerbaijan:{keywords:[\"az\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇦🇿\",fitzpatrick_scale:false,category:\"flags\"},bahamas:{keywords:[\"bs\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇧🇸\",fitzpatrick_scale:false,category:\"flags\"},bahrain:{keywords:[\"bh\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇧🇭\",fitzpatrick_scale:false,category:\"flags\"},bangladesh:{keywords:[\"bd\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇧🇩\",fitzpatrick_scale:false,category:\"flags\"},barbados:{keywords:[\"bb\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇧🇧\",fitzpatrick_scale:false,category:\"flags\"},belarus:{keywords:[\"by\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇧🇾\",fitzpatrick_scale:false,category:\"flags\"},belgium:{keywords:[\"be\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇧🇪\",fitzpatrick_scale:false,category:\"flags\"},belize:{keywords:[\"bz\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇧🇿\",fitzpatrick_scale:false,category:\"flags\"},benin:{keywords:[\"bj\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇧🇯\",fitzpatrick_scale:false,category:\"flags\"},bermuda:{keywords:[\"bm\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇧🇲\",fitzpatrick_scale:false,category:\"flags\"},bhutan:{keywords:[\"bt\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇧🇹\",fitzpatrick_scale:false,category:\"flags\"},bolivia:{keywords:[\"bo\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇧🇴\",fitzpatrick_scale:false,category:\"flags\"},caribbean_netherlands:{keywords:[\"bonaire\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇧🇶\",fitzpatrick_scale:false,category:\"flags\"},bosnia_herzegovina:{keywords:[\"bosnia\",\"herzegovina\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇧🇦\",fitzpatrick_scale:false,category:\"flags\"},botswana:{keywords:[\"bw\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇧🇼\",fitzpatrick_scale:false,category:\"flags\"},brazil:{keywords:[\"br\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇧🇷\",fitzpatrick_scale:false,category:\"flags\"},british_indian_ocean_territory:{keywords:[\"british\",\"indian\",\"ocean\",\"territory\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇮🇴\",fitzpatrick_scale:false,category:\"flags\"},british_virgin_islands:{keywords:[\"british\",\"virgin\",\"islands\",\"bvi\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇻🇬\",fitzpatrick_scale:false,category:\"flags\"},brunei:{keywords:[\"bn\",\"darussalam\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇧🇳\",fitzpatrick_scale:false,category:\"flags\"},bulgaria:{keywords:[\"bg\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇧🇬\",fitzpatrick_scale:false,category:\"flags\"},burkina_faso:{keywords:[\"burkina\",\"faso\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇧🇫\",fitzpatrick_scale:false,category:\"flags\"},burundi:{keywords:[\"bi\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇧🇮\",fitzpatrick_scale:false,category:\"flags\"},cape_verde:{keywords:[\"cabo\",\"verde\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇨🇻\",fitzpatrick_scale:false,category:\"flags\"},cambodia:{keywords:[\"kh\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇰🇭\",fitzpatrick_scale:false,category:\"flags\"},cameroon:{keywords:[\"cm\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇨🇲\",fitzpatrick_scale:false,category:\"flags\"},canada:{keywords:[\"ca\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇨🇦\",fitzpatrick_scale:false,category:\"flags\"},canary_islands:{keywords:[\"canary\",\"islands\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇮🇨\",fitzpatrick_scale:false,category:\"flags\"},cayman_islands:{keywords:[\"cayman\",\"islands\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇰🇾\",fitzpatrick_scale:false,category:\"flags\"},central_african_republic:{keywords:[\"central\",\"african\",\"republic\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇨🇫\",fitzpatrick_scale:false,category:\"flags\"},chad:{keywords:[\"td\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇹🇩\",fitzpatrick_scale:false,category:\"flags\"},chile:{keywords:[\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇨🇱\",fitzpatrick_scale:false,category:\"flags\"},cn:{keywords:[\"china\",\"chinese\",\"prc\",\"flag\",\"country\",\"nation\",\"banner\"],char:\"🇨🇳\",fitzpatrick_scale:false,category:\"flags\"},christmas_island:{keywords:[\"christmas\",\"island\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇨🇽\",fitzpatrick_scale:false,category:\"flags\"},cocos_islands:{keywords:[\"cocos\",\"keeling\",\"islands\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇨🇨\",fitzpatrick_scale:false,category:\"flags\"},colombia:{keywords:[\"co\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇨🇴\",fitzpatrick_scale:false,category:\"flags\"},comoros:{keywords:[\"km\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇰🇲\",fitzpatrick_scale:false,category:\"flags\"},congo_brazzaville:{keywords:[\"congo\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇨🇬\",fitzpatrick_scale:false,category:\"flags\"},congo_kinshasa:{keywords:[\"congo\",\"democratic\",\"republic\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇨🇩\",fitzpatrick_scale:false,category:\"flags\"},cook_islands:{keywords:[\"cook\",\"islands\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇨🇰\",fitzpatrick_scale:false,category:\"flags\"},costa_rica:{keywords:[\"costa\",\"rica\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇨🇷\",fitzpatrick_scale:false,category:\"flags\"},croatia:{keywords:[\"hr\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇭🇷\",fitzpatrick_scale:false,category:\"flags\"},cuba:{keywords:[\"cu\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇨🇺\",fitzpatrick_scale:false,category:\"flags\"},curacao:{keywords:[\"curaçao\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇨🇼\",fitzpatrick_scale:false,category:\"flags\"},cyprus:{keywords:[\"cy\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇨🇾\",fitzpatrick_scale:false,category:\"flags\"},czech_republic:{keywords:[\"cz\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇨🇿\",fitzpatrick_scale:false,category:\"flags\"},denmark:{keywords:[\"dk\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇩🇰\",fitzpatrick_scale:false,category:\"flags\"},djibouti:{keywords:[\"dj\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇩🇯\",fitzpatrick_scale:false,category:\"flags\"},dominica:{keywords:[\"dm\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇩🇲\",fitzpatrick_scale:false,category:\"flags\"},dominican_republic:{keywords:[\"dominican\",\"republic\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇩🇴\",fitzpatrick_scale:false,category:\"flags\"},ecuador:{keywords:[\"ec\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇪🇨\",fitzpatrick_scale:false,category:\"flags\"},egypt:{keywords:[\"eg\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇪🇬\",fitzpatrick_scale:false,category:\"flags\"},el_salvador:{keywords:[\"el\",\"salvador\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇸🇻\",fitzpatrick_scale:false,category:\"flags\"},equatorial_guinea:{keywords:[\"equatorial\",\"gn\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇬🇶\",fitzpatrick_scale:false,category:\"flags\"},eritrea:{keywords:[\"er\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇪🇷\",fitzpatrick_scale:false,category:\"flags\"},estonia:{keywords:[\"ee\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇪🇪\",fitzpatrick_scale:false,category:\"flags\"},ethiopia:{keywords:[\"et\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇪🇹\",fitzpatrick_scale:false,category:\"flags\"},eu:{keywords:[\"european\",\"union\",\"flag\",\"banner\"],char:\"🇪🇺\",fitzpatrick_scale:false,category:\"flags\"},falkland_islands:{keywords:[\"falkland\",\"islands\",\"malvinas\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇫🇰\",fitzpatrick_scale:false,category:\"flags\"},faroe_islands:{keywords:[\"faroe\",\"islands\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇫🇴\",fitzpatrick_scale:false,category:\"flags\"},fiji:{keywords:[\"fj\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇫🇯\",fitzpatrick_scale:false,category:\"flags\"},finland:{keywords:[\"fi\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇫🇮\",fitzpatrick_scale:false,category:\"flags\"},fr:{keywords:[\"banner\",\"flag\",\"nation\",\"france\",\"french\",\"country\"],char:\"🇫🇷\",fitzpatrick_scale:false,category:\"flags\"},french_guiana:{keywords:[\"french\",\"guiana\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇬🇫\",fitzpatrick_scale:false,category:\"flags\"},french_polynesia:{keywords:[\"french\",\"polynesia\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇵🇫\",fitzpatrick_scale:false,category:\"flags\"},french_southern_territories:{keywords:[\"french\",\"southern\",\"territories\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇹🇫\",fitzpatrick_scale:false,category:\"flags\"},gabon:{keywords:[\"ga\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇬🇦\",fitzpatrick_scale:false,category:\"flags\"},gambia:{keywords:[\"gm\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇬🇲\",fitzpatrick_scale:false,category:\"flags\"},georgia:{keywords:[\"ge\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇬🇪\",fitzpatrick_scale:false,category:\"flags\"},de:{keywords:[\"german\",\"nation\",\"flag\",\"country\",\"banner\"],char:\"🇩🇪\",fitzpatrick_scale:false,category:\"flags\"},ghana:{keywords:[\"gh\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇬🇭\",fitzpatrick_scale:false,category:\"flags\"},gibraltar:{keywords:[\"gi\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇬🇮\",fitzpatrick_scale:false,category:\"flags\"},greece:{keywords:[\"gr\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇬🇷\",fitzpatrick_scale:false,category:\"flags\"},greenland:{keywords:[\"gl\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇬🇱\",fitzpatrick_scale:false,category:\"flags\"},grenada:{keywords:[\"gd\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇬🇩\",fitzpatrick_scale:false,category:\"flags\"},guadeloupe:{keywords:[\"gp\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇬🇵\",fitzpatrick_scale:false,category:\"flags\"},guam:{keywords:[\"gu\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇬🇺\",fitzpatrick_scale:false,category:\"flags\"},guatemala:{keywords:[\"gt\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇬🇹\",fitzpatrick_scale:false,category:\"flags\"},guernsey:{keywords:[\"gg\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇬🇬\",fitzpatrick_scale:false,category:\"flags\"},guinea:{keywords:[\"gn\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇬🇳\",fitzpatrick_scale:false,category:\"flags\"},guinea_bissau:{keywords:[\"gw\",\"bissau\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇬🇼\",fitzpatrick_scale:false,category:\"flags\"},guyana:{keywords:[\"gy\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇬🇾\",fitzpatrick_scale:false,category:\"flags\"},haiti:{keywords:[\"ht\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇭🇹\",fitzpatrick_scale:false,category:\"flags\"},honduras:{keywords:[\"hn\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇭🇳\",fitzpatrick_scale:false,category:\"flags\"},hong_kong:{keywords:[\"hong\",\"kong\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇭🇰\",fitzpatrick_scale:false,category:\"flags\"},hungary:{keywords:[\"hu\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇭🇺\",fitzpatrick_scale:false,category:\"flags\"},iceland:{keywords:[\"is\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇮🇸\",fitzpatrick_scale:false,category:\"flags\"},india:{keywords:[\"in\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇮🇳\",fitzpatrick_scale:false,category:\"flags\"},indonesia:{keywords:[\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇮🇩\",fitzpatrick_scale:false,category:\"flags\"},iran:{keywords:[\"iran,\",\"islamic\",\"republic\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇮🇷\",fitzpatrick_scale:false,category:\"flags\"},iraq:{keywords:[\"iq\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇮🇶\",fitzpatrick_scale:false,category:\"flags\"},ireland:{keywords:[\"ie\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇮🇪\",fitzpatrick_scale:false,category:\"flags\"},isle_of_man:{keywords:[\"isle\",\"man\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇮🇲\",fitzpatrick_scale:false,category:\"flags\"},israel:{keywords:[\"il\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇮🇱\",fitzpatrick_scale:false,category:\"flags\"},it:{keywords:[\"italy\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇮🇹\",fitzpatrick_scale:false,category:\"flags\"},cote_divoire:{keywords:[\"ivory\",\"coast\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇨🇮\",fitzpatrick_scale:false,category:\"flags\"},jamaica:{keywords:[\"jm\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇯🇲\",fitzpatrick_scale:false,category:\"flags\"},jp:{keywords:[\"japanese\",\"nation\",\"flag\",\"country\",\"banner\"],char:\"🇯🇵\",fitzpatrick_scale:false,category:\"flags\"},jersey:{keywords:[\"je\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇯🇪\",fitzpatrick_scale:false,category:\"flags\"},jordan:{keywords:[\"jo\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇯🇴\",fitzpatrick_scale:false,category:\"flags\"},kazakhstan:{keywords:[\"kz\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇰🇿\",fitzpatrick_scale:false,category:\"flags\"},kenya:{keywords:[\"ke\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇰🇪\",fitzpatrick_scale:false,category:\"flags\"},kiribati:{keywords:[\"ki\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇰🇮\",fitzpatrick_scale:false,category:\"flags\"},kosovo:{keywords:[\"xk\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇽🇰\",fitzpatrick_scale:false,category:\"flags\"},kuwait:{keywords:[\"kw\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇰🇼\",fitzpatrick_scale:false,category:\"flags\"},kyrgyzstan:{keywords:[\"kg\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇰🇬\",fitzpatrick_scale:false,category:\"flags\"},laos:{keywords:[\"lao\",\"democratic\",\"republic\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇱🇦\",fitzpatrick_scale:false,category:\"flags\"},latvia:{keywords:[\"lv\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇱🇻\",fitzpatrick_scale:false,category:\"flags\"},lebanon:{keywords:[\"lb\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇱🇧\",fitzpatrick_scale:false,category:\"flags\"},lesotho:{keywords:[\"ls\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇱🇸\",fitzpatrick_scale:false,category:\"flags\"},liberia:{keywords:[\"lr\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇱🇷\",fitzpatrick_scale:false,category:\"flags\"},libya:{keywords:[\"ly\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇱🇾\",fitzpatrick_scale:false,category:\"flags\"},liechtenstein:{keywords:[\"li\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇱🇮\",fitzpatrick_scale:false,category:\"flags\"},lithuania:{keywords:[\"lt\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇱🇹\",fitzpatrick_scale:false,category:\"flags\"},luxembourg:{keywords:[\"lu\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇱🇺\",fitzpatrick_scale:false,category:\"flags\"},macau:{keywords:[\"macao\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇲🇴\",fitzpatrick_scale:false,category:\"flags\"},macedonia:{keywords:[\"macedonia,\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇲🇰\",fitzpatrick_scale:false,category:\"flags\"},madagascar:{keywords:[\"mg\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇲🇬\",fitzpatrick_scale:false,category:\"flags\"},malawi:{keywords:[\"mw\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇲🇼\",fitzpatrick_scale:false,category:\"flags\"},malaysia:{keywords:[\"my\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇲🇾\",fitzpatrick_scale:false,category:\"flags\"},maldives:{keywords:[\"mv\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇲🇻\",fitzpatrick_scale:false,category:\"flags\"},mali:{keywords:[\"ml\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇲🇱\",fitzpatrick_scale:false,category:\"flags\"},malta:{keywords:[\"mt\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇲🇹\",fitzpatrick_scale:false,category:\"flags\"},marshall_islands:{keywords:[\"marshall\",\"islands\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇲🇭\",fitzpatrick_scale:false,category:\"flags\"},martinique:{keywords:[\"mq\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇲🇶\",fitzpatrick_scale:false,category:\"flags\"},mauritania:{keywords:[\"mr\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇲🇷\",fitzpatrick_scale:false,category:\"flags\"},mauritius:{keywords:[\"mu\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇲🇺\",fitzpatrick_scale:false,category:\"flags\"},mayotte:{keywords:[\"yt\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇾🇹\",fitzpatrick_scale:false,category:\"flags\"},mexico:{keywords:[\"mx\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇲🇽\",fitzpatrick_scale:false,category:\"flags\"},micronesia:{keywords:[\"micronesia,\",\"federated\",\"states\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇫🇲\",fitzpatrick_scale:false,category:\"flags\"},moldova:{keywords:[\"moldova,\",\"republic\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇲🇩\",fitzpatrick_scale:false,category:\"flags\"},monaco:{keywords:[\"mc\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇲🇨\",fitzpatrick_scale:false,category:\"flags\"},mongolia:{keywords:[\"mn\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇲🇳\",fitzpatrick_scale:false,category:\"flags\"},montenegro:{keywords:[\"me\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇲🇪\",fitzpatrick_scale:false,category:\"flags\"},montserrat:{keywords:[\"ms\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇲🇸\",fitzpatrick_scale:false,category:\"flags\"},morocco:{keywords:[\"ma\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇲🇦\",fitzpatrick_scale:false,category:\"flags\"},mozambique:{keywords:[\"mz\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇲🇿\",fitzpatrick_scale:false,category:\"flags\"},myanmar:{keywords:[\"mm\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇲🇲\",fitzpatrick_scale:false,category:\"flags\"},namibia:{keywords:[\"na\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇳🇦\",fitzpatrick_scale:false,category:\"flags\"},nauru:{keywords:[\"nr\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇳🇷\",fitzpatrick_scale:false,category:\"flags\"},nepal:{keywords:[\"np\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇳🇵\",fitzpatrick_scale:false,category:\"flags\"},netherlands:{keywords:[\"nl\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇳🇱\",fitzpatrick_scale:false,category:\"flags\"},new_caledonia:{keywords:[\"new\",\"caledonia\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇳🇨\",fitzpatrick_scale:false,category:\"flags\"},new_zealand:{keywords:[\"new\",\"zealand\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇳🇿\",fitzpatrick_scale:false,category:\"flags\"},nicaragua:{keywords:[\"ni\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇳🇮\",fitzpatrick_scale:false,category:\"flags\"},niger:{keywords:[\"ne\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇳🇪\",fitzpatrick_scale:false,category:\"flags\"},nigeria:{keywords:[\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇳🇬\",fitzpatrick_scale:false,category:\"flags\"},niue:{keywords:[\"nu\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇳🇺\",fitzpatrick_scale:false,category:\"flags\"},norfolk_island:{keywords:[\"norfolk\",\"island\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇳🇫\",fitzpatrick_scale:false,category:\"flags\"},northern_mariana_islands:{keywords:[\"northern\",\"mariana\",\"islands\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇲🇵\",fitzpatrick_scale:false,category:\"flags\"},north_korea:{keywords:[\"north\",\"korea\",\"nation\",\"flag\",\"country\",\"banner\"],char:\"🇰🇵\",fitzpatrick_scale:false,category:\"flags\"},norway:{keywords:[\"no\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇳🇴\",fitzpatrick_scale:false,category:\"flags\"},oman:{keywords:[\"om_symbol\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇴🇲\",fitzpatrick_scale:false,category:\"flags\"},pakistan:{keywords:[\"pk\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇵🇰\",fitzpatrick_scale:false,category:\"flags\"},palau:{keywords:[\"pw\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇵🇼\",fitzpatrick_scale:false,category:\"flags\"},palestinian_territories:{keywords:[\"palestine\",\"palestinian\",\"territories\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇵🇸\",fitzpatrick_scale:false,category:\"flags\"},panama:{keywords:[\"pa\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇵🇦\",fitzpatrick_scale:false,category:\"flags\"},papua_new_guinea:{keywords:[\"papua\",\"new\",\"guinea\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇵🇬\",fitzpatrick_scale:false,category:\"flags\"},paraguay:{keywords:[\"py\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇵🇾\",fitzpatrick_scale:false,category:\"flags\"},peru:{keywords:[\"pe\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇵🇪\",fitzpatrick_scale:false,category:\"flags\"},philippines:{keywords:[\"ph\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇵🇭\",fitzpatrick_scale:false,category:\"flags\"},pitcairn_islands:{keywords:[\"pitcairn\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇵🇳\",fitzpatrick_scale:false,category:\"flags\"},poland:{keywords:[\"pl\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇵🇱\",fitzpatrick_scale:false,category:\"flags\"},portugal:{keywords:[\"pt\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇵🇹\",fitzpatrick_scale:false,category:\"flags\"},puerto_rico:{keywords:[\"puerto\",\"rico\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇵🇷\",fitzpatrick_scale:false,category:\"flags\"},qatar:{keywords:[\"qa\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇶🇦\",fitzpatrick_scale:false,category:\"flags\"},reunion:{keywords:[\"réunion\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇷🇪\",fitzpatrick_scale:false,category:\"flags\"},romania:{keywords:[\"ro\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇷🇴\",fitzpatrick_scale:false,category:\"flags\"},ru:{keywords:[\"russian\",\"federation\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇷🇺\",fitzpatrick_scale:false,category:\"flags\"},rwanda:{keywords:[\"rw\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇷🇼\",fitzpatrick_scale:false,category:\"flags\"},st_barthelemy:{keywords:[\"saint\",\"barthélemy\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇧🇱\",fitzpatrick_scale:false,category:\"flags\"},st_helena:{keywords:[\"saint\",\"helena\",\"ascension\",\"tristan\",\"cunha\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇸🇭\",fitzpatrick_scale:false,category:\"flags\"},st_kitts_nevis:{keywords:[\"saint\",\"kitts\",\"nevis\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇰🇳\",fitzpatrick_scale:false,category:\"flags\"},st_lucia:{keywords:[\"saint\",\"lucia\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇱🇨\",fitzpatrick_scale:false,category:\"flags\"},st_pierre_miquelon:{keywords:[\"saint\",\"pierre\",\"miquelon\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇵🇲\",fitzpatrick_scale:false,category:\"flags\"},st_vincent_grenadines:{keywords:[\"saint\",\"vincent\",\"grenadines\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇻🇨\",fitzpatrick_scale:false,category:\"flags\"},samoa:{keywords:[\"ws\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇼🇸\",fitzpatrick_scale:false,category:\"flags\"},san_marino:{keywords:[\"san\",\"marino\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇸🇲\",fitzpatrick_scale:false,category:\"flags\"},sao_tome_principe:{keywords:[\"sao\",\"tome\",\"principe\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇸🇹\",fitzpatrick_scale:false,category:\"flags\"},saudi_arabia:{keywords:[\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇸🇦\",fitzpatrick_scale:false,category:\"flags\"},senegal:{keywords:[\"sn\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇸🇳\",fitzpatrick_scale:false,category:\"flags\"},serbia:{keywords:[\"rs\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇷🇸\",fitzpatrick_scale:false,category:\"flags\"},seychelles:{keywords:[\"sc\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇸🇨\",fitzpatrick_scale:false,category:\"flags\"},sierra_leone:{keywords:[\"sierra\",\"leone\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇸🇱\",fitzpatrick_scale:false,category:\"flags\"},singapore:{keywords:[\"sg\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇸🇬\",fitzpatrick_scale:false,category:\"flags\"},sint_maarten:{keywords:[\"sint\",\"maarten\",\"dutch\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇸🇽\",fitzpatrick_scale:false,category:\"flags\"},slovakia:{keywords:[\"sk\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇸🇰\",fitzpatrick_scale:false,category:\"flags\"},slovenia:{keywords:[\"si\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇸🇮\",fitzpatrick_scale:false,category:\"flags\"},solomon_islands:{keywords:[\"solomon\",\"islands\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇸🇧\",fitzpatrick_scale:false,category:\"flags\"},somalia:{keywords:[\"so\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇸🇴\",fitzpatrick_scale:false,category:\"flags\"},south_africa:{keywords:[\"south\",\"africa\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇿🇦\",fitzpatrick_scale:false,category:\"flags\"},south_georgia_south_sandwich_islands:{keywords:[\"south\",\"georgia\",\"sandwich\",\"islands\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇬🇸\",fitzpatrick_scale:false,category:\"flags\"},kr:{keywords:[\"south\",\"korea\",\"nation\",\"flag\",\"country\",\"banner\"],char:\"🇰🇷\",fitzpatrick_scale:false,category:\"flags\"},south_sudan:{keywords:[\"south\",\"sd\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇸🇸\",fitzpatrick_scale:false,category:\"flags\"},es:{keywords:[\"spain\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇪🇸\",fitzpatrick_scale:false,category:\"flags\"},sri_lanka:{keywords:[\"sri\",\"lanka\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇱🇰\",fitzpatrick_scale:false,category:\"flags\"},sudan:{keywords:[\"sd\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇸🇩\",fitzpatrick_scale:false,category:\"flags\"},suriname:{keywords:[\"sr\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇸🇷\",fitzpatrick_scale:false,category:\"flags\"},swaziland:{keywords:[\"sz\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇸🇿\",fitzpatrick_scale:false,category:\"flags\"},sweden:{keywords:[\"se\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇸🇪\",fitzpatrick_scale:false,category:\"flags\"},switzerland:{keywords:[\"ch\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇨🇭\",fitzpatrick_scale:false,category:\"flags\"},syria:{keywords:[\"syrian\",\"arab\",\"republic\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇸🇾\",fitzpatrick_scale:false,category:\"flags\"},taiwan:{keywords:[\"tw\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇹🇼\",fitzpatrick_scale:false,category:\"flags\"},tajikistan:{keywords:[\"tj\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇹🇯\",fitzpatrick_scale:false,category:\"flags\"},tanzania:{keywords:[\"tanzania,\",\"united\",\"republic\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇹🇿\",fitzpatrick_scale:false,category:\"flags\"},thailand:{keywords:[\"th\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇹🇭\",fitzpatrick_scale:false,category:\"flags\"},timor_leste:{keywords:[\"timor\",\"leste\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇹🇱\",fitzpatrick_scale:false,category:\"flags\"},togo:{keywords:[\"tg\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇹🇬\",fitzpatrick_scale:false,category:\"flags\"},tokelau:{keywords:[\"tk\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇹🇰\",fitzpatrick_scale:false,category:\"flags\"},tonga:{keywords:[\"to\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇹🇴\",fitzpatrick_scale:false,category:\"flags\"},trinidad_tobago:{keywords:[\"trinidad\",\"tobago\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇹🇹\",fitzpatrick_scale:false,category:\"flags\"},tunisia:{keywords:[\"tn\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇹🇳\",fitzpatrick_scale:false,category:\"flags\"},tr:{keywords:[\"turkey\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇹🇷\",fitzpatrick_scale:false,category:\"flags\"},turkmenistan:{keywords:[\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇹🇲\",fitzpatrick_scale:false,category:\"flags\"},turks_caicos_islands:{keywords:[\"turks\",\"caicos\",\"islands\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇹🇨\",fitzpatrick_scale:false,category:\"flags\"},tuvalu:{keywords:[\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇹🇻\",fitzpatrick_scale:false,category:\"flags\"},uganda:{keywords:[\"ug\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇺🇬\",fitzpatrick_scale:false,category:\"flags\"},ukraine:{keywords:[\"ua\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇺🇦\",fitzpatrick_scale:false,category:\"flags\"},united_arab_emirates:{keywords:[\"united\",\"arab\",\"emirates\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇦🇪\",fitzpatrick_scale:false,category:\"flags\"},uk:{keywords:[\"united\",\"kingdom\",\"great\",\"britain\",\"northern\",\"ireland\",\"flag\",\"nation\",\"country\",\"banner\",\"british\",\"UK\",\"english\",\"england\",\"union jack\"],char:\"🇬🇧\",fitzpatrick_scale:false,category:\"flags\"},england:{keywords:[\"flag\",\"english\"],char:\"🏴󠁧󠁢󠁥󠁮󠁧󠁿\",fitzpatrick_scale:false,category:\"flags\"},scotland:{keywords:[\"flag\",\"scottish\"],char:\"🏴󠁧󠁢󠁳󠁣󠁴󠁿\",fitzpatrick_scale:false,category:\"flags\"},wales:{keywords:[\"flag\",\"welsh\"],char:\"🏴󠁧󠁢󠁷󠁬󠁳󠁿\",fitzpatrick_scale:false,category:\"flags\"},us:{keywords:[\"united\",\"states\",\"america\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇺🇸\",fitzpatrick_scale:false,category:\"flags\"},us_virgin_islands:{keywords:[\"virgin\",\"islands\",\"us\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇻🇮\",fitzpatrick_scale:false,category:\"flags\"},uruguay:{keywords:[\"uy\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇺🇾\",fitzpatrick_scale:false,category:\"flags\"},uzbekistan:{keywords:[\"uz\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇺🇿\",fitzpatrick_scale:false,category:\"flags\"},vanuatu:{keywords:[\"vu\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇻🇺\",fitzpatrick_scale:false,category:\"flags\"},vatican_city:{keywords:[\"vatican\",\"city\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇻🇦\",fitzpatrick_scale:false,category:\"flags\"},venezuela:{keywords:[\"ve\",\"bolivarian\",\"republic\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇻🇪\",fitzpatrick_scale:false,category:\"flags\"},vietnam:{keywords:[\"viet\",\"nam\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇻🇳\",fitzpatrick_scale:false,category:\"flags\"},wallis_futuna:{keywords:[\"wallis\",\"futuna\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇼🇫\",fitzpatrick_scale:false,category:\"flags\"},western_sahara:{keywords:[\"western\",\"sahara\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇪🇭\",fitzpatrick_scale:false,category:\"flags\"},yemen:{keywords:[\"ye\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇾🇪\",fitzpatrick_scale:false,category:\"flags\"},zambia:{keywords:[\"zm\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇿🇲\",fitzpatrick_scale:false,category:\"flags\"},zimbabwe:{keywords:[\"zw\",\"flag\",\"nation\",\"country\",\"banner\"],char:\"🇿🇼\",fitzpatrick_scale:false,category:\"flags\"},united_nations:{keywords:[\"un\",\"flag\",\"banner\"],char:\"🇺🇳\",fitzpatrick_scale:false,category:\"flags\"},pirate_flag:{keywords:[\"skull\",\"crossbones\",\"flag\",\"banner\"],char:\"🏴‍☠️\",fitzpatrick_scale:false,category:\"flags\"}});"
  },
  {
    "path": "apps/web-antd/public/tinymce/plugins/help/js/i18n/keynav/ar.js",
    "content": "tinymce.Resource.add('tinymce.html-i18n.help-keynav.ar',\n'<h1>بدء التنقل بواسطة لوحة المفاتيح</h1>\\n' +\n  '\\n' +\n  '<dl>\\n' +\n  '  <dt>التركيز على شريط القوائم</dt>\\n' +\n  '  <dd>نظاما التشغيل Windows أو Linux: Alt + F9</dd>\\n' +\n  '  <dd>نظام التشغيل macOS: &#x2325;F9</dd>\\n' +\n  '  <dt>التركيز على شريط الأدوات</dt>\\n' +\n  '  <dd>نظاما التشغيل Windows أو Linux: Alt + F10</dd>\\n' +\n  '  <dd>نظام التشغيل macOS: &#x2325;F10</dd>\\n' +\n  '  <dt>التركيز على التذييل</dt>\\n' +\n  '  <dd>نظاما التشغيل Windows أو Linux: Alt + F11</dd>\\n' +\n  '  <dd>نظام التشغيل macOS: &#x2325;F11</dd>\\n' +\n  '  <dt>تركيز الإشعارات</dt>\\n' +\n  '  <dd>نظاما التشغيل Windows أو Linux: Alt + F12</dd>\\n' +\n  '  <dd>نظام التشغيل macOS: &#x2325;F12</dd>\\n' +\n  '  <dt>التركيز على شريط أدوات السياق</dt>\\n' +\n  '  <dd>أنظمة التشغيل Windows أو Linux أو macOS: Ctrl+F9</dd>\\n' +\n  '</dl>\\n' +\n  '\\n' +\n  '<p>سيبدأ التنقل عند عنصر واجهة المستخدم الأول، والذي سيتم تمييزه أو تسطيره في حالة العنصر الأول في\\n' +\n  '  مسار عنصر التذييل.</p>\\n' +\n  '\\n' +\n  '<h1>التنقل بين أقسام واجهة المستخدم</h1>\\n' +\n  '\\n' +\n  '<p>للانتقال من أحد أقسام واجهة المستخدم إلى القسم التالي، اضغط على <strong>Tab</strong>.</p>\\n' +\n  '\\n' +\n  '<p>للانتقال من أحد أقسام واجهة المستخدم إلى القسم السابق، اضغط على <strong>Shift+Tab</strong>.</p>\\n' +\n  '\\n' +\n  '<p>ترتيب علامات <strong>Tab</strong> لأقسام واجهة المستخدم هذه هو:</p>\\n' +\n  '\\n' +\n  '<ol>\\n' +\n  '  <li>شريط القوائم</li>\\n' +\n  '  <li>كل مجموعة شريط الأدوات</li>\\n' +\n  '  <li>الشريط الجانبي</li>\\n' +\n  '  <li>مسار العنصر في التذييل</li>\\n' +\n  '  <li>زر تبديل عدد الكلمات في التذييل</li>\\n' +\n  '  <li>رابط إدراج العلامة التجارية في التذييل</li>\\n' +\n  '  <li>مؤشر تغيير حجم المحرر في التذييل</li>\\n' +\n  '</ol>\\n' +\n  '\\n' +\n  '<p>إذا لم يكن قسم واجهة المستخدم موجودًا، فسيتم تخطيه.</p>\\n' +\n  '\\n' +\n  '<p>إذا كان التذييل يحتوي على التركيز على ‏‫التنقل بواسطة لوحة المفاتيح، ولا يوجد شريط جانبي مرئي، فإن الضغط على <strong>Shift+Tab</strong>\\n' +\n  '  ينقل التركيز إلى مجموعة شريط الأدوات الأولى، وليس الأخيرة.</p>\\n' +\n  '\\n' +\n  '<h1>التنقل بين أقسام واجهة المستخدم</h1>\\n' +\n  '\\n' +\n  '<p>للانتقال من أحد عناصر واجهة المستخدم إلى العنصر التالي، اضغط على مفتاح <strong>السهم</strong> المناسب.</p>\\n' +\n  '\\n' +\n  '<p>مفتاحا السهمين <strong>اليسار‎</strong> و<strong>اليمين‎</strong></p>\\n' +\n  '\\n' +\n  '<ul>\\n' +\n  '  <li>التنقل بين القوائم في شريط القوائم.</li>\\n' +\n  '  <li>فتح قائمة فرعية في القائمة.</li>\\n' +\n  '  <li>التنقل بين الأزرار في مجموعة شريط الأدوات.</li>\\n' +\n  '  <li>التنقل بين العناصر في مسار عنصر التذييل.</li>\\n' +\n  '</ul>\\n' +\n  '\\n' +\n  '<p>مفتاحا السهمين <strong>لأسفل‎</strong> و<strong>لأعلى‎</strong></p>\\n' +\n  '\\n' +\n  '<ul>\\n' +\n  '  <li>التنقل بين عناصر القائمة في القائمة.</li>\\n' +\n  '  <li>التنقل بين العناصر في قائمة شريط الأدوات المنبثقة.</li>\\n' +\n  '</ul>\\n' +\n  '\\n' +\n  '<p>دورة مفاتيح <strong>الأسهم‎</strong> داخل قسم واجهة المستخدم التي تم التركيز عليها.</p>\\n' +\n  '\\n' +\n  '<p>لإغلاق قائمة مفتوحة أو قائمة فرعية مفتوحة أو قائمة منبثقة مفتوحة، اضغط على مفتاح <strong>Esc</strong>.</p>\\n' +\n  '\\n' +\n  '<p>إذا كان التركيز الحالي على \"الجزء العلوي\" من قسم معين لواجهة المستخدم، فإن الضغط على مفتاح <strong>Esc</strong> يؤدي أيضًا إلى الخروج\\n' +\n  '  من التنقل بواسطة لوحة المفاتيح بالكامل.</p>\\n' +\n  '\\n' +\n  '<h1>تنفيذ عنصر قائمة أو زر شريط أدوات</h1>\\n' +\n  '\\n' +\n  '<p>عندما يتم تمييز عنصر القائمة المطلوب أو زر شريط الأدوات، اضغط على زر <strong>Return</strong>، أو <strong>Enter</strong>،\\n' +\n  '  أو <strong>مفتاح المسافة</strong> لتنفيذ العنصر.</p>\\n' +\n  '\\n' +\n  '<h1>التنقل في مربعات الحوار غير المبوبة</h1>\\n' +\n  '\\n' +\n  '<p>في مربعات الحوار غير المبوبة، يتم التركيز على المكون التفاعلي الأول عند فتح مربع الحوار.</p>\\n' +\n  '\\n' +\n  '<p>التنقل بين مكونات الحوار التفاعلي بالضغط على زر <strong>Tab</strong> أو <strong>Shift+Tab</strong>.</p>\\n' +\n  '\\n' +\n  '<h1>التنقل في مربعات الحوار المبوبة</h1>\\n' +\n  '\\n' +\n  '<p>في مربعات الحوار المبوبة، يتم التركيز على الزر الأول في قائمة علامات التبويب عند فتح مربع الحوار.</p>\\n' +\n  '\\n' +\n  '<p>التنقل بين المكونات التفاعلية لعلامة التبويب لمربع الحوار هذه بالضغط على زر <strong>Tab</strong> أو\\n' +\n  '  <strong>Shift+Tab</strong>.</p>\\n' +\n  '\\n' +\n  '<p>التبديل إلى علامة تبويب أخرى لمربع الحوار من خلال التركيز على قائمة علامة التبويب ثم الضغط على زر <strong>السهم</strong> المناسب\\n' +\n  '  مفتاح للتنقل بين علامات التبويب المتاحة.</p>\\n');"
  },
  {
    "path": "apps/web-antd/public/tinymce/plugins/help/js/i18n/keynav/bg_BG.js",
    "content": "tinymce.Resource.add('tinymce.html-i18n.help-keynav.bg_BG',\n'<h1>Начало на навигацията с клавиатурата</h1>\\n' +\n  '\\n' +\n  '<dl>\\n' +\n  '  <dt>Фокусиране върху лентата с менюта</dt>\\n' +\n  '  <dd>Windows или Linux: Alt+F9</dd>\\n' +\n  '  <dd>macOS: &#x2325;F9</dd>\\n' +\n  '  <dt>Фокусиране върху лентата с инструменти</dt>\\n' +\n  '  <dd>Windows или Linux: Alt+F10</dd>\\n' +\n  '  <dd>macOS: &#x2325;F10</dd>\\n' +\n  '  <dt>Фокусиране върху долния колонтитул</dt>\\n' +\n  '  <dd>Windows или Linux: Alt+F11</dd>\\n' +\n  '  <dd>macOS: &#x2325;F11</dd>\\n' +\n  '  <dt>Фокусиране на известието</dt>\\n' +\n  '  <dd>Windows или Linux: Alt+F12</dd>\\n' +\n  '  <dd>macOS: &#x2325;F12</dd>\\n' +\n  '  <dt>Фокусиране върху контекстуалната лента с инструменти</dt>\\n' +\n  '  <dd>Windows, Linux или macOS: Ctrl+F9</dd>\\n' +\n  '</dl>\\n' +\n  '\\n' +\n  '<p>Навигацията ще започне с първия елемент на ПИ, който ще бъде маркиран или подчертан в случая на първия елемент в\\n' +\n  '  пътя до елемента в долния колонтитул.</p>\\n' +\n  '\\n' +\n  '<h1>Навигиране между раздели на ПИ</h1>\\n' +\n  '\\n' +\n  '<p>За да преминете от един раздел на ПИ към следващия, натиснете <strong>Tab</strong>.</p>\\n' +\n  '\\n' +\n  '<p>За да преминете от един раздел на ПИ към предишния, натиснете <strong>Shift+Tab</strong>.</p>\\n' +\n  '\\n' +\n  '<p>Редът за <strong>обхождане с табулация</strong> на тези раздели на ПИ е:</p>\\n' +\n  '\\n' +\n  '<ol>\\n' +\n  '  <li>Лентата с менюта</li>\\n' +\n  '  <li>Всяка група на лентата с инструменти</li>\\n' +\n  '  <li>Страничната лента</li>\\n' +\n  '  <li>Пътят до елемента в долния колонтитул</li>\\n' +\n  '  <li>Бутонът за превключване на броя на думите в долния колонтитул</li>\\n' +\n  '  <li>Връзката за търговска марка в долния колонтитул</li>\\n' +\n  '  <li>Манипулаторът за преоразмеряване на редактора в долния колонтитул</li>\\n' +\n  '</ol>\\n' +\n  '\\n' +\n  '<p>Ако някой раздел на ПИ липсва, той се пропуска.</p>\\n' +\n  '\\n' +\n  '<p>Ако долният колонтитул има фокус за навигация с клавиатурата и няма странична лента, натискането на <strong>Shift+Tab</strong>\\n' +\n  '  премества фокуса към първата група на лентата с инструменти, а не към последната.</p>\\n' +\n  '\\n' +\n  '<h1>Навигиране в разделите на ПИ</h1>\\n' +\n  '\\n' +\n  '<p>За да преминете от един елемент на ПИ към следващия, натиснете съответния клавиш със <strong>стрелка</strong>.</p>\\n' +\n  '\\n' +\n  '<p>С клавишите със стрелка <strong>наляво</strong> и <strong>надясно</strong></p>\\n' +\n  '\\n' +\n  '<ul>\\n' +\n  '  <li>се придвижвате между менютата в лентата с менюто;</li>\\n' +\n  '  <li>отваряте подменю в меню;</li>\\n' +\n  '  <li>се придвижвате между бутоните в група на лентата с инструменти;</li>\\n' +\n  '  <li>се придвижвате между елементи в пътя до елемент в долния колонтитул.</li>\\n' +\n  '</ul>\\n' +\n  '\\n' +\n  '<p>С клавишите със стрелка <strong>надолу</strong> и <strong>нагоре</strong></p>\\n' +\n  '\\n' +\n  '<ul>\\n' +\n  '  <li>се придвижвате между елементите от менюто в дадено меню;</li>\\n' +\n  '  <li>се придвижвате между елементите в изскачащо меню на лентата с инструменти.</li>\\n' +\n  '</ul>\\n' +\n  '\\n' +\n  '<p>Клавишите със <strong>стрелки</strong> се придвижват в рамките на фокусирания раздел на ПИ.</p>\\n' +\n  '\\n' +\n  '<p>За да затворите отворено меню, подменю или изскачащо меню, натиснете клавиша <strong>Esc</strong>.</p>\\n' +\n  '\\n' +\n  '<p>Ако текущият фокус е върху „горната част“ на конкретен раздел на ПИ, натискането на клавиша <strong>Esc</strong> също излиза\\n' +\n  '  напълно от навигацията с клавиатурата.</p>\\n' +\n  '\\n' +\n  '<h1>Изпълнение на елемент от менюто или бутон от лентата с инструменти</h1>\\n' +\n  '\\n' +\n  '<p>Когато желаният елемент от менюто или бутон от лентата с инструменти е маркиран, натиснете <strong>Return</strong>, <strong>Enter</strong>\\n' +\n  '  или <strong>клавиша за интервал</strong>, за да изпълните елемента.</p>\\n' +\n  '\\n' +\n  '<h1>Навигиране в диалогови прозорци без раздели</h1>\\n' +\n  '\\n' +\n  '<p>В диалоговите прозорци без раздели първият интерактивен компонент се фокусира, когато се отвори диалоговият прозорец.</p>\\n' +\n  '\\n' +\n  '<p>Навигирайте между интерактивните компоненти на диалоговия прозорец, като натиснете <strong>Tab</strong> или <strong>Shift+Tab</strong>.</p>\\n' +\n  '\\n' +\n  '<h1>Навигиране в диалогови прозорци с раздели</h1>\\n' +\n  '\\n' +\n  '<p>В диалоговите прозорци с раздели първият бутон в менюто с раздели се фокусира, когато се отвори диалоговият прозорец.</p>\\n' +\n  '\\n' +\n  '<p>Навигирайте между интерактивните компоненти на този диалогов раздел, като натиснете <strong>Tab</strong> или\\n' +\n  '  <strong>Shift+Tab</strong>.</p>\\n' +\n  '\\n' +\n  '<p>Превключете към друг диалогов раздел, като фокусирате върху менюто с раздели и след това натиснете съответния клавиш със <strong>стрелка</strong>,\\n' +\n  '  за да преминете през наличните раздели.</p>\\n');"
  },
  {
    "path": "apps/web-antd/public/tinymce/plugins/help/js/i18n/keynav/ca.js",
    "content": "tinymce.Resource.add('tinymce.html-i18n.help-keynav.ca',\n'<h1>Inici de la navegació amb el teclat</h1>\\n' +\n  '\\n' +\n  '<dl>\\n' +\n  '  <dt>Enfocar la barra de menús</dt>\\n' +\n  '  <dd>Windows o Linux: Alt+F9</dd>\\n' +\n  '  <dd>macOS: &#x2325;F9</dd>\\n' +\n  \"  <dt>Enfocar la barra d'eines</dt>\\n\" +\n  '  <dd>Windows o Linux: Alt+F10</dd>\\n' +\n  '  <dd>macOS: &#x2325;F10</dd>\\n' +\n  '  <dt>Enfocar el peu de pàgina</dt>\\n' +\n  '  <dd>Windows o Linux: Alt+F11</dd>\\n' +\n  '  <dd>macOS: &#x2325;F11</dd>\\n' +\n  '  <dt>Enfocar la notificació</dt>\\n' +\n  '  <dd>Windows o Linux: Alt+F12</dd>\\n' +\n  '  <dd>macOS: &#x2325;F12</dd>\\n' +\n  \"  <dt>Enfocar una barra d'eines contextual</dt>\\n\" +\n  '  <dd>Windows, Linux o macOS: Ctrl+F9</dd>\\n' +\n  '</dl>\\n' +\n  '\\n' +\n  \"<p>La navegació començarà en el primer element de la interfície d'usuari, que es ressaltarà o subratllarà per al primer element a\\n\" +\n  \"  la ruta de l'element de peu de pàgina.</p>\\n\" +\n  '\\n' +\n  \"<h1>Navegació entre seccions de la interfície d'usuari</h1>\\n\" +\n  '\\n' +\n  \"<p>Per desplaçar-vos des d'una secció de la interfície d'usuari a la següent, premeu la tecla <strong>Tab</strong>.</p>\\n\" +\n  '\\n' +\n  \"<p>Per desplaçar-vos des d'una secció de la interfície d'usuari a l'anterior, premeu les tecles <strong>Maj+Tab</strong>.</p>\\n\" +\n  '\\n' +\n  \"<p>L'ordre en prémer la tecla <strong>Tab</strong> d'aquestes secciones de la interfície d'usuari és:</p>\\n\" +\n  '\\n' +\n  '<ol>\\n' +\n  '  <li>Barra de menús</li>\\n' +\n  \"  <li>Cada grup de la barra d'eines</li>\\n\" +\n  '  <li>Barra lateral</li>\\n' +\n  \"  <li>Ruta de l'element del peu de pàgina</li>\\n\" +\n  '  <li>Botó de commutació de recompte de paraules al peu de pàgina</li>\\n' +\n  '  <li>Enllaç de marca del peu de pàgina</li>\\n' +\n  \"  <li>Control de canvi de mida de l'editor al peu de pàgina</li>\\n\" +\n  '</ol>\\n' +\n  '\\n' +\n  \"<p>Si no hi ha una secció de la interfície d'usuari, s'ometrà.</p>\\n\" +\n  '\\n' +\n  '<p>Si el peu de pàgina té el focus de navegació del teclat i no hi ha cap barra lateral visible, en prémer <strong>Maj+Tab</strong>\\n' +\n  \"  el focus es mou al primer grup de la barra d'eines, no l'últim.</p>\\n\" +\n  '\\n' +\n  \"<h1>Navegació dins de les seccions de la interfície d'usuari</h1>\\n\" +\n  '\\n' +\n  \"<p>Per desplaçar-vos des d'un element de la interfície d'usuari al següent, premeu la tecla de <strong>Fletxa</strong> adequada.</p>\\n\" +\n  '\\n' +\n  '<p>Les tecles de fletxa <strong>Esquerra</strong> i <strong>Dreta</strong></p>\\n' +\n  '\\n' +\n  '<ul>\\n' +\n  '  <li>us permeten desplaçar-vos entre menús de la barra de menús.</li>\\n' +\n  '  <li>obren un submenú en un menú.</li>\\n' +\n  \"  <li>us permeten desplaçar-vos entre botons d'un grup de la barra d'eines.</li>\\n\" +\n  \"  <li>us permeten desplaçar-vos entre elements de la ruta d'elements del peu de pàgina.</li>\\n\" +\n  '</ul>\\n' +\n  '\\n' +\n  '<p>Les tecles de fletxa <strong>Avall</strong> i <strong>Amunt</strong></p>\\n' +\n  '\\n' +\n  '<ul>\\n' +\n  \"  <li>us permeten desplaçar-vos entre elements de menú d'un menú.</li>\\n\" +\n  \"  <li>us permeten desplaçar-vos entre elements d'un menú emergent de la barra d'eines.</li>\\n\" +\n  '</ul>\\n' +\n  '\\n' +\n  \"<p>Les tecles de <strong>Fletxa</strong> us permeten desplaçar-vos dins de la secció de la interfície d'usuari que té el focus.</p>\\n\" +\n  '\\n' +\n  '<p>Per tancar un menú, un submenú o un menú emergent oberts, premeu la tecla <strong>Esc</strong>.</p>\\n' +\n  '\\n' +\n  \"<p>Si el focus actual es troba a la ‘part superior’ d'una secció específica de la interfície d'usuari, en prémer la tecla <strong>Esc</strong> també es tanca\\n\" +\n  '  completament la navegació amb el teclat.</p>\\n' +\n  '\\n' +\n  \"<h1>Execució d'un element de menú o d'un botó de la barra d'eines</h1>\\n\" +\n  '\\n' +\n  \"<p>Quan l'element del menú o el botó de la barra d'eines que desitgeu estigui ressaltat, premeu <strong>Retorn</strong>, <strong>Intro</strong>\\n\" +\n  \"  o la <strong>barra d'espai</strong> per executar l'element.</p>\\n\" +\n  '\\n' +\n  '<h1>Navegació per quadres de diàleg sense pestanyes</h1>\\n' +\n  '\\n' +\n  \"<p>En els quadres de diàleg sense pestanyes, el primer component interactiu pren el focus quan s'obre el quadre diàleg.</p>\\n\" +\n  '\\n' +\n  '<p>Premeu la tecla <strong>Tab</strong> o les tecles <strong>Maj+Tab</strong> per desplaçar-vos entre components interactius del quadre de diàleg.</p>\\n' +\n  '\\n' +\n  '<h1>Navegació per quadres de diàleg amb pestanyes</h1>\\n' +\n  '\\n' +\n  \"<p>En els quadres de diàleg amb pestanyes, el primer botó del menú de la pestanya pren el focus quan s'obre el quadre diàleg.</p>\\n\" +\n  '\\n' +\n  \"<p>Per desplaçar-vos entre components interactius d'aquest quadre de diàleg, premeu la tecla <strong>Tab</strong> o\\n\" +\n  '  les tecles <strong>Maj+Tab</strong>.</p>\\n' +\n  '\\n' +\n  \"<p>Canvieu a la pestanya d'un altre quadre de diàleg, tot enfocant el menú de la pestanya, i després premeu la tecla <strong>Fletxa</strong> adequada\\n\" +\n  '  per canviar entre les pestanyes disponibles.</p>\\n');"
  },
  {
    "path": "apps/web-antd/public/tinymce/plugins/help/js/i18n/keynav/cs.js",
    "content": "tinymce.Resource.add('tinymce.html-i18n.help-keynav.cs',\n'<h1>Začínáme navigovat pomocí klávesnice</h1>\\n' +\n  '\\n' +\n  '<dl>\\n' +\n  '  <dt>Přejít na řádek nabídek</dt>\\n' +\n  '  <dd>Windows nebo Linux: Alt+F9</dd>\\n' +\n  '  <dd>macOS: &#x2325;F9</dd>\\n' +\n  '  <dt>Přejít na panel nástrojů</dt>\\n' +\n  '  <dd>Windows nebo Linux: Alt+F10</dd>\\n' +\n  '  <dd>macOS: &#x2325;F10</dd>\\n' +\n  '  <dt>Přejít na zápatí</dt>\\n' +\n  '  <dd>Windows nebo Linux: Alt+F11</dd>\\n' +\n  '  <dd>macOS: &#x2325;F11</dd>\\n' +\n  '  <dt>Přejít na oznámení</dt>\\n' +\n  '  <dd>Windows nebo Linux: Alt+F12</dd>\\n' +\n  '  <dd>macOS: &#x2325;F12</dd>\\n' +\n  '  <dt>Přejít na kontextový panel nástrojů</dt>\\n' +\n  '  <dd>Windows, Linux nebo macOS: Ctrl+F9</dd>\\n' +\n  '</dl>\\n' +\n  '\\n' +\n  '<p>Navigace začne u první položky uživatelského rozhraní, která bude zvýrazněna nebo v případě první položky\\n' +\n  '  cesty k prvku zápatí podtržena.</p>\\n' +\n  '\\n' +\n  '<h1>Navigace mezi oddíly uživatelského rozhraní</h1>\\n' +\n  '\\n' +\n  '<p>Stisknutím klávesy <strong>Tab</strong> se posunete z jednoho oddílu uživatelského rozhraní na další.</p>\\n' +\n  '\\n' +\n  '<p>Stisknutím kláves <strong>Shift+Tab</strong> se posunete z jednoho oddílu uživatelského rozhraní na předchozí.</p>\\n' +\n  '\\n' +\n  '<p>Pořadí přepínání mezi oddíly uživatelského rozhraní pomocí klávesy <strong>Tab</strong>:</p>\\n' +\n  '\\n' +\n  '<ol>\\n' +\n  '  <li>Řádek nabídek</li>\\n' +\n  '  <li>Každá skupina panelu nástrojů</li>\\n' +\n  '  <li>Boční panel</li>\\n' +\n  '  <li>Cesta k prvku v zápatí.</li>\\n' +\n  '  <li>Tlačítko přepínače počtu slov v zápatí</li>\\n' +\n  '  <li>Odkaz na informace o značce v zápatí</li>\\n' +\n  '  <li>Úchyt pro změnu velikosti editoru v zápatí</li>\\n' +\n  '</ol>\\n' +\n  '\\n' +\n  '<p>Pokud nějaký oddíl uživatelského rozhraní není přítomen, je přeskočen.</p>\\n' +\n  '\\n' +\n  '<p>Pokud je zápatí vybrané pro navigaci pomocí klávesnice a není zobrazen žádný boční panel, stisknutím kláves <strong>Shift+Tab</strong>\\n' +\n  '  přejdete na první skupinu panelu nástrojů, nikoli na poslední.</p>\\n' +\n  '\\n' +\n  '<h1>Navigace v rámci oddílů uživatelského rozhraní</h1>\\n' +\n  '\\n' +\n  '<p>Chcete-li se přesunout z jednoho prvku uživatelského rozhraní na další, stiskněte příslušnou klávesu s <strong>šipkou</strong>.</p>\\n' +\n  '\\n' +\n  '<p>Klávesy s šipkou <strong>vlevo</strong> a <strong>vpravo</strong></p>\\n' +\n  '\\n' +\n  '<ul>\\n' +\n  '  <li>umožňují přesun mezi nabídkami na řádku nabídek;</li>\\n' +\n  '  <li>otevírají podnabídku nabídky;</li>\\n' +\n  '  <li>umožňují přesun mezi tlačítky ve skupině panelu nástrojů;</li>\\n' +\n  '  <li>umožňují přesun mezi položkami cesty prvku v zápatí.</li>\\n' +\n  '</ul>\\n' +\n  '\\n' +\n  '<p>Klávesy se šipkou <strong>dolů</strong> a <strong>nahoru</strong></p>\\n' +\n  '\\n' +\n  '<ul>\\n' +\n  '  <li>umožňují přesun mezi položkami nabídky;</li>\\n' +\n  '  <li>umožňují přesun mezi položkami místní nabídky panelu nástrojů.</li>\\n' +\n  '</ul>\\n' +\n  '\\n' +\n  '<p><strong>Šipky</strong> provádí přepínání v rámci vybraného oddílu uživatelského rozhraní.</p>\\n' +\n  '\\n' +\n  '<p>Chcete-li zavřít otevřenou nabídku, podnabídku nebo místní nabídku, stiskněte klávesu <strong>Esc</strong>.</p>\\n' +\n  '\\n' +\n  '<p>Pokud je aktuálně vybrána horní část oddílu uživatelského rozhraní, stisknutím klávesy <strong>Esc</strong> zcela ukončíte také\\n' +\n  '  navigaci pomocí klávesnice.</p>\\n' +\n  '\\n' +\n  '<h1>Provedení příkazu položky nabídky nebo tlačítka panelu nástrojů</h1>\\n' +\n  '\\n' +\n  '<p>Pokud je zvýrazněna požadovaná položka nabídky nebo tlačítko panelu nástrojů, stisknutím klávesy <strong>Return</strong>, <strong>Enter</strong>\\n' +\n  '  nebo <strong>mezerníku</strong> provedete příslušný příkaz.</p>\\n' +\n  '\\n' +\n  '<h1>Navigace v dialogových oknech bez záložek</h1>\\n' +\n  '\\n' +\n  '<p>Při otevření dialogových oken bez záložek přejdete na první interaktivní komponentu.</p>\\n' +\n  '\\n' +\n  '<p>Přecházet mezi interaktivními komponentami dialogového okna můžete stisknutím klávesy <strong>Tab</strong> nebo kombinace <strong>Shift+Tab</strong>.</p>\\n' +\n  '\\n' +\n  '<h1>Navigace v dialogových oknech se záložkami</h1>\\n' +\n  '\\n' +\n  '<p>Při otevření dialogových oken se záložkami přejdete na první tlačítko v nabídce záložek.</p>\\n' +\n  '\\n' +\n  '<p>Přecházet mezi interaktivními komponentami této záložky dialogového okna můžete stisknutím klávesy <strong>Tab</strong> nebo\\n' +\n  '  kombinace <strong>Shift+Tab</strong>.</p>\\n' +\n  '\\n' +\n  '<p>Chcete-li přepnout na další záložku dialogového okna, přejděte na nabídku záložek a poté můžete stisknutím požadované <strong>šipky</strong>\\n' +\n  '  přepínat mezi dostupnými záložkami.</p>\\n');"
  },
  {
    "path": "apps/web-antd/public/tinymce/plugins/help/js/i18n/keynav/da.js",
    "content": "tinymce.Resource.add('tinymce.html-i18n.help-keynav.da',\n'<h1>Start tastaturnavigation</h1>\\n' +\n  '\\n' +\n  '<dl>\\n' +\n  '  <dt>Fokuser på menulinjen</dt>\\n' +\n  '  <dd>Windows eller Linux: Alt+F9</dd>\\n' +\n  '  <dd>macOS: &#x2325;F9</dd>\\n' +\n  '  <dt>Fokuser på værktøjslinjen</dt>\\n' +\n  '  <dd>Windows eller Linux: Alt+F10</dd>\\n' +\n  '  <dd>macOS: &#x2325;F10</dd>\\n' +\n  '  <dt>Fokuser på sidefoden</dt>\\n' +\n  '  <dd>Windows eller Linux: Alt+F11</dd>\\n' +\n  '  <dd>macOS: &#x2325;F11</dd>\\n' +\n  '  <dt>Fokuser på meddelelsen</dt>\\n' +\n  '  <dd>Windows eller Linux: Alt+F12</dd>\\n' +\n  '  <dd>macOS: &#x2325;F12</dd>\\n' +\n  '  <dt>Fokuser på kontekstuel værktøjslinje</dt>\\n' +\n  '  <dd>Windows, Linux eller macOS: Ctrl+F9</dd>\\n' +\n  '</dl>\\n' +\n  '\\n' +\n  '<p>Navigationen starter ved det første UI-element, som fremhæves eller understreges hvad angår det første element i\\n' +\n  '  sidefodens sti til elementet.</p>\\n' +\n  '\\n' +\n  '<h1>Naviger mellem UI-sektioner</h1>\\n' +\n  '\\n' +\n  '<p>Gå fra én UI-sektion til den næste ved at trykke på <strong>Tab</strong>.</p>\\n' +\n  '\\n' +\n  '<p>Gå fra én UI-sektion til den forrige ved at trykke på <strong>Shift+Tab</strong>.</p>\\n' +\n  '\\n' +\n  '<p><strong>Tab</strong>-rækkefølgen af disse UI-sektioner er:</p>\\n' +\n  '\\n' +\n  '<ol>\\n' +\n  '  <li>Menulinje</li>\\n' +\n  '  <li>Hver værktøjsgruppe</li>\\n' +\n  '  <li>Sidepanel</li>\\n' +\n  '  <li>Sti til elementet i sidefoden</li>\\n' +\n  '  <li>Til/fra-knap for ordoptælling i sidefoden</li>\\n' +\n  '  <li>Brandinglink i sidefoden</li>\\n' +\n  '  <li>Tilpasningshåndtag for editor i sidefoden</li>\\n' +\n  '</ol>\\n' +\n  '\\n' +\n  '<p>Hvis en UI-sektion ikke er til stede, springes den over.</p>\\n' +\n  '\\n' +\n  '<p>Hvis sidefoden har fokus til tastaturnavigation, og der ikke er noget synligt sidepanel, kan der trykkes på <strong>Shift+Tab</strong>\\n' +\n  '  for at flytte fokus til den første værktøjsgruppe, ikke den sidste.</p>\\n' +\n  '\\n' +\n  '<h1>Naviger inden for UI-sektioner</h1>\\n' +\n  '\\n' +\n  '<p>Gå fra ét UI-element til det næste ved at trykke på den relevante <strong>piletast</strong>.</p>\\n' +\n  '\\n' +\n  '<p><strong>Venstre</strong> og <strong>højre</strong> piletast</p>\\n' +\n  '\\n' +\n  '<ul>\\n' +\n  '  <li>flytter mellem menuerne i menulinjen.</li>\\n' +\n  '  <li>åbner en undermenu i en menu.</li>\\n' +\n  '  <li>flytter mellem knapperne i en værktøjsgruppe.</li>\\n' +\n  '  <li>flytter mellem elementer i sidefodens sti til elementet.</li>\\n' +\n  '</ul>\\n' +\n  '\\n' +\n  '<p>Pil <strong>ned</strong> og <strong>op</strong></p>\\n' +\n  '\\n' +\n  '<ul>\\n' +\n  '  <li>flytter mellem menupunkterne i en menu.</li>\\n' +\n  '  <li>flytter mellem punkterne i en genvejsmenu i værktøjslinjen.</li>\\n' +\n  '</ul>\\n' +\n  '\\n' +\n  '<p><strong>Piletasterne</strong> kører rundt inden for UI-sektionen, der fokuseres på.</p>\\n' +\n  '\\n' +\n  '<p>For at lukke en åben menu, en åben undermenu eller en åben genvejsmenu trykkes der på <strong>Esc</strong>-tasten.</p>\\n' +\n  '\\n' +\n  \"<p>Hvis det aktuelle fokus er i 'toppen' af en bestemt UI-sektion, vil tryk på <strong>Esc</strong>-tasten også afslutte\\n\" +\n  '  tastaturnavigationen helt.</p>\\n' +\n  '\\n' +\n  '<h1>Udfør et menupunkt eller en værktøjslinjeknap</h1>\\n' +\n  '\\n' +\n  '<p>Når det ønskede menupunkt eller den ønskede værktøjslinjeknap er fremhævet, trykkes der på <strong>Retur</strong>, <strong>Enter</strong>\\n' +\n  '  eller <strong>mellemrumstasten</strong> for at udføre elementet.</p>\\n' +\n  '\\n' +\n  '<h1>Naviger i ikke-faneopdelte dialogbokse</h1>\\n' +\n  '\\n' +\n  '<p>I ikke-faneopdelte dialogbokse får den første interaktive komponent fokus, når dialogboksen åbnes.</p>\\n' +\n  '\\n' +\n  '<p>Naviger mellem interaktive dialogbokskomponenter ved at trykke på <strong>Tab</strong> eller <strong>Shift+Tab</strong>.</p>\\n' +\n  '\\n' +\n  '<h1>Naviger i faneopdelte dialogbokse</h1>\\n' +\n  '\\n' +\n  '<p>I faneopdelte dialogbokse får den første knap i fanemenuen fokus, når dialogboksen åbnes.</p>\\n' +\n  '\\n' +\n  '<p>Naviger mellem interaktive komponenter i denne dialogboksfane ved at trykke på <strong>Tab</strong> eller\\n' +\n  '  <strong>Shift+Tab</strong>.</p>\\n' +\n  '\\n' +\n  '<p>Skift til en anden dialogboksfane ved at fokusere på fanemenuen og derefter trykke på den relevante <strong>piletast</strong>\\n' +\n  '  for at køre igennem de tilgængelige faner.</p>\\n');"
  },
  {
    "path": "apps/web-antd/public/tinymce/plugins/help/js/i18n/keynav/de.js",
    "content": "tinymce.Resource.add('tinymce.html-i18n.help-keynav.de',\n'<h1>Grundlagen der Tastaturnavigation</h1>\\n' +\n  '\\n' +\n  '<dl>\\n' +\n  '  <dt>Fokus auf Menüleiste</dt>\\n' +\n  '  <dd>Windows oder Linux: ALT+F9</dd>\\n' +\n  '  <dd>macOS: &#x2325;F9</dd>\\n' +\n  '  <dt>Fokus auf Symbolleiste</dt>\\n' +\n  '  <dd>Windows oder Linux: ALT+F10</dd>\\n' +\n  '  <dd>macOS: &#x2325;F10</dd>\\n' +\n  '  <dt>Fokus auf Fußzeile</dt>\\n' +\n  '  <dd>Windows oder Linux: ALT+F11</dd>\\n' +\n  '  <dd>macOS: &#x2325;F11</dd>\\n' +\n  '  <dt>Benachrichtigung fokussieren</dt>\\n' +\n  '  <dd>Windows oder Linux: ALT+F12</dd>\\n' +\n  '  <dd>macOS: &#x2325;F12</dd>\\n' +\n  '  <dt>Fokus auf kontextbezogene Symbolleiste</dt>\\n' +\n  '  <dd>Windows, Linux oder macOS: STRG+F9</dd>\\n' +\n  '</dl>\\n' +\n  '\\n' +\n  '<p>Die Navigation beginnt beim ersten Benutzeroberflächenelement, welches hervorgehoben ist. Falls sich das erste Element im Pfad der Fußzeile befindet,\\n' +\n  '  ist es unterstrichen.</p>\\n' +\n  '\\n' +\n  '<h1>Zwischen Abschnitten der Benutzeroberfläche navigieren</h1>\\n' +\n  '\\n' +\n  '<p>Um von einem Abschnitt der Benutzeroberfläche zum nächsten zu wechseln, drücken Sie <strong>TAB</strong>.</p>\\n' +\n  '\\n' +\n  '<p>Um von einem Abschnitt der Benutzeroberfläche zum vorherigen zu wechseln, drücken Sie <strong>UMSCHALT+TAB</strong>.</p>\\n' +\n  '\\n' +\n  '<p>Die Abschnitte der Benutzeroberfläche haben folgende <strong>TAB</strong>-Reihenfolge:</p>\\n' +\n  '\\n' +\n  '<ol>\\n' +\n  '  <li>Menüleiste</li>\\n' +\n  '  <li>Einzelne Gruppen der Symbolleiste</li>\\n' +\n  '  <li>Randleiste</li>\\n' +\n  '  <li>Elementpfad in der Fußzeile</li>\\n' +\n  '  <li>Umschaltfläche „Wörter zählen“ in der Fußzeile</li>\\n' +\n  '  <li>Branding-Link in der Fußzeile</li>\\n' +\n  '  <li>Editor-Ziehpunkt zur Größenänderung in der Fußzeile</li>\\n' +\n  '</ol>\\n' +\n  '\\n' +\n  '<p>Falls ein Abschnitt der Benutzeroberflächen nicht vorhanden ist, wird er übersprungen.</p>\\n' +\n  '\\n' +\n  '<p>Wenn in der Fußzeile die Tastaturnavigation fokussiert ist und keine Randleiste angezeigt wird, wechselt der Fokus durch Drücken von <strong>UMSCHALT+TAB</strong>\\n' +\n  '  zur ersten Gruppe der Symbolleiste, nicht zur letzten.</p>\\n' +\n  '\\n' +\n  '<h1>Innerhalb von Abschnitten der Benutzeroberfläche navigieren</h1>\\n' +\n  '\\n' +\n  '<p>Um von einem Element der Benutzeroberfläche zum nächsten zu wechseln, drücken Sie die entsprechende <strong>Pfeiltaste</strong>.</p>\\n' +\n  '\\n' +\n  '<p>Die Pfeiltasten <strong>Links</strong> und <strong>Rechts</strong></p>\\n' +\n  '\\n' +\n  '<ul>\\n' +\n  '  <li>wechseln zwischen Menüs in der Menüleiste.</li>\\n' +\n  '  <li>öffnen das Untermenü eines Menüs.</li>\\n' +\n  '  <li>wechseln zwischen Schaltflächen in einer Gruppe der Symbolleiste.</li>\\n' +\n  '  <li>wechseln zwischen Elementen im Elementpfad der Fußzeile.</li>\\n' +\n  '</ul>\\n' +\n  '\\n' +\n  '<p>Die Pfeiltasten <strong>Abwärts</strong> und <strong>Aufwärts</strong></p>\\n' +\n  '\\n' +\n  '<ul>\\n' +\n  '  <li>wechseln zwischen Menüelementen in einem Menü.</li>\\n' +\n  '  <li>wechseln zwischen Elementen in einem Popupmenü der Symbolleiste.</li>\\n' +\n  '</ul>\\n' +\n  '\\n' +\n  '<p>Die <strong>Pfeiltasten</strong> rotieren innerhalb des fokussierten Abschnitts der Benutzeroberfläche.</p>\\n' +\n  '\\n' +\n  '<p>Um ein geöffnetes Menü, ein geöffnetes Untermenü oder ein geöffnetes Popupmenü zu schließen, drücken Sie die <strong>ESC</strong>-Taste.</p>\\n' +\n  '\\n' +\n  '<p>Wenn sich der aktuelle Fokus ganz oben in einem bestimmten Abschnitt der Benutzeroberfläche befindet, wird durch Drücken der <strong>ESC</strong>-Taste auch\\n' +\n  '  die Tastaturnavigation beendet.</p>\\n' +\n  '\\n' +\n  '<h1>Ein Menüelement oder eine Symbolleistenschaltfläche ausführen</h1>\\n' +\n  '\\n' +\n  '<p>Wenn das gewünschte Menüelement oder die gewünschte Symbolleistenschaltfläche hervorgehoben ist, drücken Sie <strong>Zurück</strong>, <strong>Eingabe</strong>\\n' +\n  '  oder die <strong>Leertaste</strong>, um das Element auszuführen.</p>\\n' +\n  '\\n' +\n  '<h1>In Dialogfeldern ohne Registerkarten navigieren</h1>\\n' +\n  '\\n' +\n  '<p>In Dialogfeldern ohne Registerkarten ist beim Öffnen eines Dialogfelds die erste interaktive Komponente fokussiert.</p>\\n' +\n  '\\n' +\n  '<p>Navigieren Sie zwischen den interaktiven Komponenten eines Dialogfelds, indem Sie <strong>TAB</strong> oder <strong>UMSCHALT+TAB</strong> drücken.</p>\\n' +\n  '\\n' +\n  '<h1>In Dialogfeldern mit Registerkarten navigieren</h1>\\n' +\n  '\\n' +\n  '<p>In Dialogfeldern mit Registerkarten ist beim Öffnen eines Dialogfelds die erste Schaltfläche eines Registerkartenmenüs fokussiert.</p>\\n' +\n  '\\n' +\n  '<p>Navigieren Sie zwischen den interaktiven Komponenten auf dieser Registerkarte des Dialogfelds, indem Sie <strong>TAB</strong> oder\\n' +\n  '  <strong>UMSCHALT+TAB</strong> drücken.</p>\\n' +\n  '\\n' +\n  '<p>Wechseln Sie zu einer anderen Registerkarte des Dialogfelds, indem Sie den Fokus auf das Registerkartenmenü legen und dann die entsprechende <strong>Pfeiltaste</strong>\\n' +\n  '  drücken, um durch die verfügbaren Registerkarten zu rotieren.</p>\\n');"
  },
  {
    "path": "apps/web-antd/public/tinymce/plugins/help/js/i18n/keynav/el.js",
    "content": "tinymce.Resource.add('tinymce.html-i18n.help-keynav.el',\n'<h1>Έναρξη πλοήγησης μέσω πληκτρολογίου</h1>\\n' +\n  '\\n' +\n  '<dl>\\n' +\n  '  <dt>Εστίαση στη γραμμή μενού</dt>\\n' +\n  '  <dd>Windows ή Linux: Alt+F9</dd>\\n' +\n  '  <dd>macOS: &#x2325;F9</dd>\\n' +\n  '  <dt>Εστίαση στη γραμμή εργαλείων</dt>\\n' +\n  '  <dd>Windows ή Linux: Alt+F10</dd>\\n' +\n  '  <dd>macOS: &#x2325;F10</dd>\\n' +\n  '  <dt>Εστίαση στο υποσέλιδο</dt>\\n' +\n  '  <dd>Windows ή Linux: Alt+F11</dd>\\n' +\n  '  <dd>macOS: &#x2325;F11</dd>\\n' +\n  '  <dt>Εστίαση στην ειδοποίηση</dt>\\n' +\n  '  <dd>Windows ή Linux: Alt+F12</dd>\\n' +\n  '  <dd>macOS: &#x2325;F12</dd>\\n' +\n  '  <dt>Εστίαση σε γραμμή εργαλείων βάσει περιεχομένου</dt>\\n' +\n  '  <dd>Windows, Linux ή macOS: Ctrl+F9</dd>\\n' +\n  '</dl>\\n' +\n  '\\n' +\n  '<p>Η πλοήγηση θα ξεκινήσει από το πρώτο στοιχείο περιβάλλοντος χρήστη, που θα επισημαίνεται ή θα είναι υπογραμμισμένο,\\n' +\n  '  όπως στην περίπτωση της διαδρομής του στοιχείου Υποσέλιδου.</p>\\n' +\n  '\\n' +\n  '<h1>Πλοήγηση μεταξύ ενοτήτων του περιβάλλοντος χρήστη</h1>\\n' +\n  '\\n' +\n  '<p>Για να μετακινηθείτε από μια ενότητα περιβάλλοντος χρήστη στην επόμενη, πιέστε το πλήκτρο <strong>Tab</strong>.</p>\\n' +\n  '\\n' +\n  '<p>Για να μετακινηθείτε από μια ενότητα περιβάλλοντος χρήστη στην προηγούμενη, πιέστε τα πλήκτρα <strong>Shift+Tab</strong>.</p>\\n' +\n  '\\n' +\n  '<p>Η σειρά <strong>Tab</strong> αυτών των ενοτήτων περιβάλλοντος χρήστη είναι η εξής:</p>\\n' +\n  '\\n' +\n  '<ol>\\n' +\n  '  <li>Γραμμή μενού</li>\\n' +\n  '  <li>Κάθε ομάδα γραμμής εργαλείων</li>\\n' +\n  '  <li>Πλαϊνή γραμμή</li>\\n' +\n  '  <li>Διαδρομή στοιχείου στο υποσέλιδο</li>\\n' +\n  '  <li>Κουμπί εναλλαγής μέτρησης λέξεων στο υποσέλιδο</li>\\n' +\n  '  <li>Σύνδεσμος επωνυμίας στο υποσέλιδο</li>\\n' +\n  '  <li>Λαβή αλλαγής μεγέθους προγράμματος επεξεργασίας στο υποσέλιδο</li>\\n' +\n  '</ol>\\n' +\n  '\\n' +\n  '<p>Εάν δεν εμφανίζεται ενότητα περιβάλλοντος χρήστη, παραλείπεται.</p>\\n' +\n  '\\n' +\n  '<p>Εάν η εστίαση πλοήγησης βρίσκεται στο πληκτρολόγιο και δεν υπάρχει εμφανής πλαϊνή γραμμή, εάν πιέσετε <strong>Shift+Tab</strong>\\n' +\n  '  η εστίαση μετακινείται στην πρώτη ομάδα γραμμής εργαλείων, όχι στην τελευταία.</p>\\n' +\n  '\\n' +\n  '<h1>Πλοήγηση εντός των ενοτήτων του περιβάλλοντος χρήστη</h1>\\n' +\n  '\\n' +\n  '<p>Για να μετακινηθείτε από ένα στοιχείο περιβάλλοντος χρήστη στο επόμενο, πιέστε το αντίστοιχο πλήκτρο <strong>βέλους</strong>.</p>\\n' +\n  '\\n' +\n  '<p>Με τα πλήκτρα <strong>αριστερού</strong> και <strong>δεξιού</strong> βέλους</p>\\n' +\n  '\\n' +\n  '<ul>\\n' +\n  '  <li>γίνεται μετακίνηση μεταξύ των μενού στη γραμμή μενού.</li>\\n' +\n  '  <li>ανοίγει ένα υπομενού σε ένα μενού.</li>\\n' +\n  '  <li>γίνεται μετακίνηση μεταξύ κουμπιών σε μια ομάδα γραμμής εργαλείων.</li>\\n' +\n  '  <li>γίνεται μετακίνηση μεταξύ στοιχείων στη διαδρομή στοιχείου στο υποσέλιδο.</li>\\n' +\n  '</ul>\\n' +\n  '\\n' +\n  '<p>Με τα πλήκτρα <strong>επάνω</strong> και <strong>κάτω</strong> βέλους</p>\\n' +\n  '\\n' +\n  '<ul>\\n' +\n  '  <li>γίνεται μετακίνηση μεταξύ των στοιχείων μενού σε ένα μενού.</li>\\n' +\n  '  <li>γίνεται μετακίνηση μεταξύ των στοιχείων μενού σε ένα αναδυόμενο μενού γραμμής εργαλείων.</li>\\n' +\n  '</ul>\\n' +\n  '\\n' +\n  '<p>Με τα πλήκτρα <strong>βέλους</strong> γίνεται κυκλική μετακίνηση εντός της εστιασμένης ενότητας περιβάλλοντος χρήστη.</p>\\n' +\n  '\\n' +\n  '<p>Για να κλείσετε ένα ανοιχτό μενού, ένα ανοιχτό υπομενού ή ένα ανοιχτό αναδυόμενο μενού, πιέστε το πλήκτρο <strong>Esc</strong>.</p>\\n' +\n  '\\n' +\n  '<p>Εάν η τρέχουσα εστίαση βρίσκεται στην κορυφή μιας ενότητας περιβάλλοντος χρήστη, πιέζοντας το πλήκτρο <strong>Esc</strong>,\\n' +\n  '  γίνεται επίσης πλήρης έξοδος από την πλοήγηση μέσω πληκτρολογίου.</p>\\n' +\n  '\\n' +\n  '<h1>Εκτέλεση ενός στοιχείου μενού ή κουμπιού γραμμής εργαλείων</h1>\\n' +\n  '\\n' +\n  '<p>Όταν το επιθυμητό στοιχείο μενού ή κουμπί γραμμής εργαλείων είναι επισημασμένο, πιέστε τα πλήκτρα <strong>Return</strong>, <strong>Enter</strong>,\\n' +\n  '  ή το <strong>πλήκτρο διαστήματος</strong> για να εκτελέσετε το στοιχείο.</p>\\n' +\n  '\\n' +\n  '<h1>Πλοήγηση σε παράθυρα διαλόγου χωρίς καρτέλες</h1>\\n' +\n  '\\n' +\n  '<p>Σε παράθυρα διαλόγου χωρίς καρτέλες, το πρώτο αλληλεπιδραστικό στοιχείο λαμβάνει την εστίαση όταν ανοίγει το παράθυρο διαλόγου.</p>\\n' +\n  '\\n' +\n  '<p>Μπορείτε να πλοηγηθείτε μεταξύ των αλληλεπιδραστικών στοιχείων παραθύρων διαλόγων πιέζοντας τα πλήκτρα <strong>Tab</strong> ή <strong>Shift+Tab</strong>.</p>\\n' +\n  '\\n' +\n  '<h1>Πλοήγηση σε παράθυρα διαλόγου με καρτέλες</h1>\\n' +\n  '\\n' +\n  '<p>Σε παράθυρα διαλόγου με καρτέλες, το πρώτο κουμπί στο μενού καρτέλας λαμβάνει την εστίαση όταν ανοίγει το παράθυρο διαλόγου.</p>\\n' +\n  '\\n' +\n  '<p>Μπορείτε να πλοηγηθείτε μεταξύ των αλληλεπιδραστικών στοιχείων αυτής της καρτέλα διαλόγου πιέζοντας τα πλήκτρα <strong>Tab</strong> ή\\n' +\n  '  <strong>Shift+Tab</strong>.</p>\\n' +\n  '\\n' +\n  '<p>Μπορείτε να κάνετε εναλλαγή σε άλλη καρτέλα του παραθύρου διαλόγου, μεταφέροντας την εστίαση στο μενού καρτέλας και πιέζοντας το κατάλληλο πλήκτρο <strong>βέλους</strong>\\n' +\n  '  για να μετακινηθείτε κυκλικά στις διαθέσιμες καρτέλες.</p>\\n');"
  },
  {
    "path": "apps/web-antd/public/tinymce/plugins/help/js/i18n/keynav/en.js",
    "content": "tinymce.Resource.add('tinymce.html-i18n.help-keynav.en',\n'<h1>Begin keyboard navigation</h1>\\n' +\n  '\\n' +\n  '<dl>\\n' +\n  '  <dt>Focus the Menu bar</dt>\\n' +\n  '  <dd>Windows or Linux: Alt+F9</dd>\\n' +\n  '  <dd>macOS: &#x2325;F9</dd>\\n' +\n  '  <dt>Focus the Toolbar</dt>\\n' +\n  '  <dd>Windows or Linux: Alt+F10</dd>\\n' +\n  '  <dd>macOS: &#x2325;F10</dd>\\n' +\n  '  <dt>Focus the footer</dt>\\n' +\n  '  <dd>Windows or Linux: Alt+F11</dd>\\n' +\n  '  <dd>macOS: &#x2325;F11</dd>\\n' +\n  '  <dt>Focus the notification</dt>\\n' +\n  '  <dd>Windows or Linux: Alt+F12</dd>\\n' +\n  '  <dd>macOS: &#x2325;F12</dd>\\n' +\n  '  <dt>Focus a contextual toolbar</dt>\\n' +\n  '  <dd>Windows, Linux or macOS: Ctrl+F9</dd>\\n' +\n  '</dl>\\n' +\n  '\\n' +\n  '<p>Navigation will start at the first UI item, which will be highlighted, or underlined in the case of the first item in\\n' +\n  '  the Footer element path.</p>\\n' +\n  '\\n' +\n  '<h1>Navigate between UI sections</h1>\\n' +\n  '\\n' +\n  '<p>To move from one UI section to the next, press <strong>Tab</strong>.</p>\\n' +\n  '\\n' +\n  '<p>To move from one UI section to the previous, press <strong>Shift+Tab</strong>.</p>\\n' +\n  '\\n' +\n  '<p>The <strong>Tab</strong> order of these UI sections is:</p>\\n' +\n  '\\n' +\n  '<ol>\\n' +\n  '  <li>Menu bar</li>\\n' +\n  '  <li>Each toolbar group</li>\\n' +\n  '  <li>Sidebar</li>\\n' +\n  '  <li>Element path in the footer</li>\\n' +\n  '  <li>Word count toggle button in the footer</li>\\n' +\n  '  <li>Branding link in the footer</li>\\n' +\n  '  <li>Editor resize handle in the footer</li>\\n' +\n  '</ol>\\n' +\n  '\\n' +\n  '<p>If a UI section is not present, it is skipped.</p>\\n' +\n  '\\n' +\n  '<p>If the footer has keyboard navigation focus, and there is no visible sidebar, pressing <strong>Shift+Tab</strong>\\n' +\n  '  moves focus to the first toolbar group, not the last.</p>\\n' +\n  '\\n' +\n  '<h1>Navigate within UI sections</h1>\\n' +\n  '\\n' +\n  '<p>To move from one UI element to the next, press the appropriate <strong>Arrow</strong> key.</p>\\n' +\n  '\\n' +\n  '<p>The <strong>Left</strong> and <strong>Right</strong> arrow keys</p>\\n' +\n  '\\n' +\n  '<ul>\\n' +\n  '  <li>move between menus in the menu bar.</li>\\n' +\n  '  <li>open a sub-menu in a menu.</li>\\n' +\n  '  <li>move between buttons in a toolbar group.</li>\\n' +\n  '  <li>move between items in the footer’s element path.</li>\\n' +\n  '</ul>\\n' +\n  '\\n' +\n  '<p>The <strong>Down</strong> and <strong>Up</strong> arrow keys</p>\\n' +\n  '\\n' +\n  '<ul>\\n' +\n  '  <li>move between menu items in a menu.</li>\\n' +\n  '  <li>move between items in a toolbar pop-up menu.</li>\\n' +\n  '</ul>\\n' +\n  '\\n' +\n  '<p><strong>Arrow</strong> keys cycle within the focused UI section.</p>\\n' +\n  '\\n' +\n  '<p>To close an open menu, an open sub-menu, or an open pop-up menu, press the <strong>Esc</strong> key.</p>\\n' +\n  '\\n' +\n  '<p>If the current focus is at the ‘top’ of a particular UI section, pressing the <strong>Esc</strong> key also exits\\n' +\n  '  keyboard navigation entirely.</p>\\n' +\n  '\\n' +\n  '<h1>Execute a menu item or toolbar button</h1>\\n' +\n  '\\n' +\n  '<p>When the desired menu item or toolbar button is highlighted, press <strong>Return</strong>, <strong>Enter</strong>,\\n' +\n  '  or the <strong>Space bar</strong> to execute the item.</p>\\n' +\n  '\\n' +\n  '<h1>Navigate non-tabbed dialogs</h1>\\n' +\n  '\\n' +\n  '<p>In non-tabbed dialogs, the first interactive component takes focus when the dialog opens.</p>\\n' +\n  '\\n' +\n  '<p>Navigate between interactive dialog components by pressing <strong>Tab</strong> or <strong>Shift+Tab</strong>.</p>\\n' +\n  '\\n' +\n  '<h1>Navigate tabbed dialogs</h1>\\n' +\n  '\\n' +\n  '<p>In tabbed dialogs, the first button in the tab menu takes focus when the dialog opens.</p>\\n' +\n  '\\n' +\n  '<p>Navigate between interactive components of this dialog tab by pressing <strong>Tab</strong> or\\n' +\n  '  <strong>Shift+Tab</strong>.</p>\\n' +\n  '\\n' +\n  '<p>Switch to another dialog tab by giving the tab menu focus and then pressing the appropriate <strong>Arrow</strong>\\n' +\n  '  key to cycle through the available tabs.</p>\\n');"
  },
  {
    "path": "apps/web-antd/public/tinymce/plugins/help/js/i18n/keynav/es.js",
    "content": "tinymce.Resource.add('tinymce.html-i18n.help-keynav.es',\n'<h1>Iniciar la navegación con el teclado</h1>\\n' +\n  '\\n' +\n  '<dl>\\n' +\n  '  <dt>Enfocar la barra de menús</dt>\\n' +\n  '  <dd>Windows o Linux: Alt+F9</dd>\\n' +\n  '  <dd>macOS: &#x2325;F9</dd>\\n' +\n  '  <dt>Enfocar la barra de herramientas</dt>\\n' +\n  '  <dd>Windows o Linux: Alt+F10</dd>\\n' +\n  '  <dd>macOS: &#x2325;F10</dd>\\n' +\n  '  <dt>Enfocar el pie de página</dt>\\n' +\n  '  <dd>Windows o Linux: Alt+F11</dd>\\n' +\n  '  <dd>macOS: &#x2325;F11</dd>\\n' +\n  '  <dt>Enfocar la notificación</dt>\\n' +\n  '  <dd>Windows o Linux: Alt+F12</dd>\\n' +\n  '  <dd>macOS: &#x2325;F12</dd>\\n' +\n  '  <dt>Enfocar una barra de herramientas contextual</dt>\\n' +\n  '  <dd>Windows, Linux o macOS: Ctrl+F9</dd>\\n' +\n  '</dl>\\n' +\n  '\\n' +\n  '<p>La navegación comenzará por el primer elemento de la interfaz de usuario (IU), de tal manera que se resaltará, o bien se subrayará si se trata del primer elemento de\\n' +\n  '  la ruta de elemento del pie de página.</p>\\n' +\n  '\\n' +\n  '<h1>Navegar entre las secciones de la IU</h1>\\n' +\n  '\\n' +\n  '<p>Para pasar de una sección de la IU a la siguiente, pulse la tecla <strong>Tab</strong>.</p>\\n' +\n  '\\n' +\n  '<p>Para pasar de una sección de la IU a la anterior, pulse <strong>Mayús+Tab</strong>.</p>\\n' +\n  '\\n' +\n  '<p>El orden de <strong>tabulación</strong> de estas secciones de la IU es:</p>\\n' +\n  '\\n' +\n  '<ol>\\n' +\n  '  <li>Barra de menús</li>\\n' +\n  '  <li>Cada grupo de barra de herramientas</li>\\n' +\n  '  <li>Barra lateral</li>\\n' +\n  '  <li>Ruta del elemento en el pie de página</li>\\n' +\n  '  <li>Botón de alternancia de recuento de palabras en el pie de página</li>\\n' +\n  '  <li>Enlace de personalización de marca en el pie de página</li>\\n' +\n  '  <li>Controlador de cambio de tamaño en el pie de página</li>\\n' +\n  '</ol>\\n' +\n  '\\n' +\n  '<p>Si una sección de la IU no está presente, esta se omite.</p>\\n' +\n  '\\n' +\n  '<p>Si el pie de página tiene un enfoque de navegación con el teclado y no hay ninguna barra lateral visible, al pulsar <strong>Mayús+Tab</strong>,\\n' +\n  '  el enfoque se moverá al primer grupo de barra de herramientas, en lugar de al último.</p>\\n' +\n  '\\n' +\n  '<h1>Navegar dentro de las secciones de la IU</h1>\\n' +\n  '\\n' +\n  '<p>Para pasar de un elemento de la IU al siguiente, pulse la tecla de <strong>flecha</strong> correspondiente.</p>\\n' +\n  '\\n' +\n  '<p>Las teclas de flecha <strong>izquierda</strong> y <strong>derecha</strong> permiten</p>\\n' +\n  '\\n' +\n  '<ul>\\n' +\n  '  <li>desplazarse entre los menús de la barra de menús.</li>\\n' +\n  '  <li>abrir el submenú de un menú.</li>\\n' +\n  '  <li>desplazarse entre los botones de un grupo de barra de herramientas.</li>\\n' +\n  '  <li>desplazarse entre los elementos de la ruta de elemento del pie de página.</li>\\n' +\n  '</ul>\\n' +\n  '\\n' +\n  '<p>Las teclas de flecha <strong>abajo</strong> y <strong>arriba</strong> permiten</p>\\n' +\n  '\\n' +\n  '<ul>\\n' +\n  '  <li>desplazarse entre los elementos de menú de un menú.</li>\\n' +\n  '  <li>desplazarse entre los elementos de un menú emergente de una barra de herramientas.</li>\\n' +\n  '</ul>\\n' +\n  '\\n' +\n  '<p>Las teclas de <strong>flecha</strong> van cambiando dentro de la sección de la IU enfocada.</p>\\n' +\n  '\\n' +\n  '<p>Para cerrar un menú, un submenú o un menú emergente que estén abiertos, pulse la tecla <strong>Esc</strong>.</p>\\n' +\n  '\\n' +\n  '<p>Si el enfoque actual se encuentra en la parte superior de una sección de la IU determinada, al pulsar la tecla <strong>Esc</strong> saldrá\\n' +\n  '  de la navegación con el teclado por completo.</p>\\n' +\n  '\\n' +\n  '<h1>Ejecutar un elemento de menú o un botón de barra de herramientas</h1>\\n' +\n  '\\n' +\n  '<p>Si el elemento de menú o el botón de barra de herramientas deseado está resaltado, pulse la tecla <strong>Retorno</strong> o <strong>Entrar</strong>,\\n' +\n  '  o la <strong>barra espaciadora</strong> para ejecutar el elemento.</p>\\n' +\n  '\\n' +\n  '<h1>Navegar por cuadros de diálogo sin pestañas</h1>\\n' +\n  '\\n' +\n  '<p>En los cuadros de diálogo sin pestañas, el primer componente interactivo se enfoca al abrirse el cuadro de diálogo.</p>\\n' +\n  '\\n' +\n  '<p>Para navegar entre los componentes interactivos del cuadro de diálogo, pulse las teclas <strong>Tab</strong> o <strong>Mayús+Tab</strong>.</p>\\n' +\n  '\\n' +\n  '<h1>Navegar por cuadros de diálogo con pestañas</h1>\\n' +\n  '\\n' +\n  '<p>En los cuadros de diálogo con pestañas, el primer botón del menú de pestaña se enfoca al abrirse el cuadro de diálogo.</p>\\n' +\n  '\\n' +\n  '<p>Para navegar entre componentes interactivos de esta pestaña del cuadro de diálogo, pulse las teclas <strong>Tab</strong> o\\n' +\n  '  <strong>Mayús+Tab</strong>.</p>\\n' +\n  '\\n' +\n  '<p>Si desea cambiar a otra pestaña del cuadro de diálogo, enfoque el menú de pestañas y, a continuación, pulse la tecla de <strong>flecha</strong>\\n' +\n  '  correspondiente para moverse por las pestañas disponibles.</p>\\n');"
  },
  {
    "path": "apps/web-antd/public/tinymce/plugins/help/js/i18n/keynav/eu.js",
    "content": "tinymce.Resource.add('tinymce.html-i18n.help-keynav.eu',\n'<h1>Hasi teklatuaren nabigazioa</h1>\\n' +\n  '\\n' +\n  '<dl>\\n' +\n  '  <dt>Fokuratu menu-barra</dt>\\n' +\n  '  <dd>Windows edo Linux: Alt+F9</dd>\\n' +\n  '  <dd>macOS: &#x2325;F9</dd>\\n' +\n  '  <dt>Fokuratu tresna-barra</dt>\\n' +\n  '  <dd>Windows edo Linux: Alt+F10</dd>\\n' +\n  '  <dd>macOS: &#x2325;F10</dd>\\n' +\n  '  <dt>Fokuratu orri-oina</dt>\\n' +\n  '  <dd>Windows edo Linux: Alt+F11</dd>\\n' +\n  '  <dd>macOS: &#x2325;F11</dd>\\n' +\n  '  <dt>Fokuratu jakinarazpena</dt>\\n' +\n  '  <dd>Windows edo Linux: Alt+F12</dd>\\n' +\n  '  <dd>macOS: &#x2325;F12</dd>\\n' +\n  '  <dt>Fokuratu testuinguruaren tresna-barra</dt>\\n' +\n  '  <dd>Windows, Linux edo macOS: Ktrl+F9</dd>\\n' +\n  '</dl>\\n' +\n  '\\n' +\n  '<p>Nabigazioa EIko lehen elementuan hasiko da: elementu hori nabarmendu egingo da, edo azpimarratu lehen elementua bada\\n' +\n  '  orri-oineko elementuaren bidea.</p>\\n' +\n  '\\n' +\n  '<h1>Nabigatu EIko atalen artean</h1>\\n' +\n  '\\n' +\n  '<p>EIko atal batetik hurrengora mugitzeko, sakatu <strong>Tabuladorea</strong>.</p>\\n' +\n  '\\n' +\n  '<p>EIko atal batetik aurrekora mugitzeko, sakatu <strong>Maius+Tabuladorea</strong>.</p>\\n' +\n  '\\n' +\n  '<p>EIko atal hauen <strong>Tabuladorea</strong> da:</p>\\n' +\n  '\\n' +\n  '<ol>\\n' +\n  '  <li>Menu-barra</li>\\n' +\n  '  <li>Tresna-barraren talde bakoitza</li>\\n' +\n  '  <li>Alboko barra</li>\\n' +\n  '  <li>Orri-oineko elementuaren bidea</li>\\n' +\n  '  <li>Orri-oneko urrats-kontaketa txandakatzeko botoia</li>\\n' +\n  '  <li>Orri-oineko marken esteka</li>\\n' +\n  '  <li>Orri-oineko editorearen tamaina aldatzeko heldulekua</li>\\n' +\n  '</ol>\\n' +\n  '\\n' +\n  '<p>EIko atal bat ez badago, saltatu egin da.</p>\\n' +\n  '\\n' +\n  '<p>Orri-oinak teklatuaren nabigazioa fokuratuta badago, eta alboko barra ikusgai ez badago, <strong>Maius+Tabuladorea</strong> sakatuz gero,\\n' +\n  '  fokua tresna-barrako lehen taldera eramaten da, ez azkenera.</p>\\n' +\n  '\\n' +\n  '<h1>Nabigatu EIko atalen barruan</h1>\\n' +\n  '\\n' +\n  '<p>EIko elementu batetik hurrengora mugitzeko, sakatu dagokion <strong>Gezia</strong> tekla.</p>\\n' +\n  '\\n' +\n  '<p><strong>Ezkerrera</strong> eta <strong>Eskuinera</strong> gezi-teklak</p>\\n' +\n  '\\n' +\n  '<ul>\\n' +\n  '  <li>menu-barrako menuen artean mugitzen da.</li>\\n' +\n  '  <li>ireki azpimenu bat menuan.</li>\\n' +\n  '  <li>mugitu botoi batetik bestera tresna-barren talde batean.</li>\\n' +\n  '  <li>mugitu orri-oineko elementuaren bideko elementu batetik bestera.</li>\\n' +\n  '</ul>\\n' +\n  '\\n' +\n  '<p><strong>Gora</strong> eta <strong>Behera</strong> gezi-teklak</p>\\n' +\n  '\\n' +\n  '<ul>\\n' +\n  '  <li>mugitu menu bateko menu-elementuen artean.</li>\\n' +\n  '  <li>mugitu tresna-barrako menu gainerakor bateko menu-elementuen artean.</li>\\n' +\n  '</ul>\\n' +\n  '\\n' +\n  '<p><strong>Gezia</strong> teklen zikloa nabarmendutako EI atalen barruan.</p>\\n' +\n  '\\n' +\n  '<p>Irekitako menu bat ixteko, ireki azpimenua, edo ireki menu gainerakorra, sakatu <strong>Ihes</strong> tekla.</p>\\n' +\n  '\\n' +\n  '<p>Une horretan fokuratzea EIko atal jakin baten \"goialdean\" badago, <strong>Ihes</strong> tekla sakatuz gero\\n' +\n  '  teklatuaren nabigaziotik irtengo zara.</p>\\n' +\n  '\\n' +\n  '<h1>Exekutatu menuko elementu bat edo tresna-barrako botoi bat</h1>\\n' +\n  '\\n' +\n  '<p>Nahi den menuaren elementua edo tresna-barraren botoia nabarmenduta dagoenean, sakatu <strong>Itzuli</strong>, <strong>Sartu</strong>\\n' +\n  '  edo <strong>Zuriune-barra</strong> elementua exekutatzeko.</p>\\n' +\n  '\\n' +\n  '<h1>Nabigatu fitxarik gabeko elkarrizketak</h1>\\n' +\n  '\\n' +\n  '<p>Fitxarik gabeko elkarrizketetan, lehen osagai interaktiboa fokuratzen da elkarrizketa irekitzen denean.</p>\\n' +\n  '\\n' +\n  '<p>Nabigatu elkarrizketa interaktiboko osagai batetik bestera <strong>Tabuladorea</strong> edo <strong>Maius+Tabuladorea</strong> sakatuta.</p>\\n' +\n  '\\n' +\n  '<h1>Nabigatu fitxadun elkarrizketak</h1>\\n' +\n  '\\n' +\n  '<p>Fitxadun elkarrizketetan, fitxa-menuko lehen botoia fokuratzen da elkarrizketa irekitzen denean.</p>\\n' +\n  '\\n' +\n  '<p>Nabigatu elkarrizketa-fitxa honen interaktiboko osagai batetik bestera <strong>Tabuladorea</strong> edo\\n' +\n  '  <strong>Maius+Tabuladorea</strong> sakatuta.</p>\\n' +\n  '\\n' +\n  '<p>Aldatu beste elkarrizketa-fitxa batera fitxa-menua fokuratu eta dagokion <strong>Gezia</strong>\\n' +\n  '  tekla sakatzeko, erabilgarri dauden fitxa batetik bestera txandakatzeko.</p>\\n');"
  },
  {
    "path": "apps/web-antd/public/tinymce/plugins/help/js/i18n/keynav/fa.js",
    "content": "tinymce.Resource.add('tinymce.html-i18n.help-keynav.fa',\n'<h1>شروع پیمایش صفحه‌کلید</h1>\\n' +\n  '\\n' +\n  '<dl>\\n' +\n  '  <dt>تمرکز بر نوار منو</dt>\\n' +\n  '  <dd>Windows یا Linux:‎‏: Alt+F9</dd>\\n' +\n  '  <dd>‎‏macOS: &#x2325;F9‎‏</dd>\\n' +\n  '  <dt>تمرکز بر نوار ابزار</dt>\\n' +\n  '  <dd>Windows یا Linux‎‏: Alt+F10</dd>\\n' +\n  '  <dd>‎‏macOS: &#x2325;F10‎‏</dd>\\n' +\n  '  <dt>تمرکز بر پانویس</dt>\\n' +\n  '  <dd>Windows یا Linux‎‏: Alt+F11</dd>\\n' +\n  '  <dd>‎‏macOS: &#x2325;F11‎‏</dd>\\n' +\n  '  <dt>تمرکز اعلان</dt>\\n' +\n  '  <dd>ویندوز یا لینوکس: Alt+F12</dd>\\n' +\n  '  <dd>macOS: &#x2325;F12</dd>\\n' +\n  '  <dt>تمرکز بر نوار ابزار بافتاری</dt>\\n' +\n  '  <dd>Windows ،Linux یا macOS:‏ Ctrl+F9</dd>\\n' +\n  '</dl>\\n' +\n  '\\n' +\n  '<p>پیمایش در اولین مورد رابط کاربری شروع می‌شود و درخصوص اولین مورد در\\n' +\n  '  مسیر عنصر پانویس، برجسته یا زیرخط‌دار می‌شود.</p>\\n' +\n  '\\n' +\n  '<h1>پیمایش بین بخش‌های رابط کاربری</h1>\\n' +\n  '\\n' +\n  '<p>برای جابجایی از یک بخش رابط کاربری به بخش بعدی، <strong>Tab</strong> را فشار دهید.</p>\\n' +\n  '\\n' +\n  '<p>برای جابجایی از یک بخش رابط کاربری به بخش قبلی، <strong>Shift+Tab</strong> را فشار دهید.</p>\\n' +\n  '\\n' +\n  '<p>ترتیب <strong>Tab</strong> این بخش‌های رابط کاربری عبارتند از:</p>\\n' +\n  '\\n' +\n  '<ol>\\n' +\n  '  <li>نوار منو</li>\\n' +\n  '  <li>هر گروه نوار ابزار</li>\\n' +\n  '  <li>نوار کناری</li>\\n' +\n  '  <li>مسیر عنصر در پانویس</li>\\n' +\n  '  <li>دکمه تغییر وضعیت تعداد کلمات در پانویس</li>\\n' +\n  '  <li>پیوند نمانام‌سازی در پانویس</li>\\n' +\n  '  <li>دسته تغییر اندازه ویرایشگر در پانویس</li>\\n' +\n  '</ol>\\n' +\n  '\\n' +\n  '<p>اگر بخشی از رابط کاربری موجود نباشد، رد می‌شود.</p>\\n' +\n  '\\n' +\n  '<p>اگر پانویس دارای تمرکز بر پیمایش صفحه‌کلید باشد،‌ و نوار کناری قابل‌مشاهده وجود ندارد، فشردن <strong>Shift+Tab</strong>\\n' +\n  '  تمرکز را به گروه نوار ابزار اول می‌برد، نه آخر.</p>\\n' +\n  '\\n' +\n  '<h1>پیمایش در بخش‌های رابط کاربری</h1>\\n' +\n  '\\n' +\n  '<p>برای جابجایی از یک عنصر رابط کاربری به بعدی، کلید <strong>جهت‌نمای</strong> مناسب را فشار دهید.</p>\\n' +\n  '\\n' +\n  '<p>کلیدهای جهت‌نمای <strong>چپ</strong> و <strong>راست</strong></p>\\n' +\n  '\\n' +\n  '<ul>\\n' +\n  '  <li>جابجایی بین منوها در نوار منو.</li>\\n' +\n  '  <li>باز کردن منوی فرعی در یک منو.</li>\\n' +\n  '  <li>جابجایی بین دکمه‌ها در یک گروه نوار ابزار.</li>\\n' +\n  '  <li>جابجایی بین موارد در مسیر عنصر پانویس.</li>\\n' +\n  '</ul>\\n' +\n  '\\n' +\n  '<p>کلیدهای جهت‌نمای <strong>پایین</strong> و <strong>بالا</strong></p>\\n' +\n  '\\n' +\n  '<ul>\\n' +\n  '  <li>جابجایی بین موارد منو در یک منو.</li>\\n' +\n  '  <li>جابجایی بین موارد در یک منوی بازشوی نوار ابزار.</li>\\n' +\n  '</ul>\\n' +\n  '\\n' +\n  '<p>کلیدهای<strong>جهت‌نما</strong> در بخش رابط کاربری متمرکز می‌چرخند.</p>\\n' +\n  '\\n' +\n  '<p>برای بستن یک منوی باز، یک منوی فرعی باز، یا یک منوی بازشوی باز، کلید <strong>Esc</strong> را فشار دهید.</p>\\n' +\n  '\\n' +\n  '<p>اگر تمرکز فعلی در «بالای» یک بخش رابط کاربری خاص است، فشردن کلید <strong>Esc</strong> نیز موجب\\n' +\n  '  خروج کامل از پیمایش صفحه‌کلید می‌شود.</p>\\n' +\n  '\\n' +\n  '<h1>اجرای یک مورد منو یا دکمه نوار ابزار</h1>\\n' +\n  '\\n' +\n  '<p>وقتی مورد منو یا دکمه نوار ابزار مورد نظر هایلایت شد، دکمه <strong>بازگشت</strong>، <strong>Enter</strong>،\\n' +\n  '  یا <strong>نوار Space</strong> را فشار دهید تا مورد را اجرا کنید.</p>\\n' +\n  '\\n' +\n  '<h1>پیمایش در کادرهای گفتگوی بدون زبانه</h1>\\n' +\n  '\\n' +\n  '<p>در کادرهای گفتگوی بدون زبانه، وقتی کادر گفتگو باز می‌شود، اولین جزء تعاملی متمرکز می‌شود.</p>\\n' +\n  '\\n' +\n  '<p>با فشردن <strong>Tab</strong> یا <strong>Shift+Tab</strong>، بین اجزای کادر گفتگوی تعاملی پیمایش کنید.</p>\\n' +\n  '\\n' +\n  '<h1>پیمایش کادرهای گفتگوی زبانه‌دار</h1>\\n' +\n  '\\n' +\n  '<p>در کادرهای گفتگوی زبانه‌دار، وقتی کادر گفتگو باز می‌شود، اولین دکمه در منوی زبانه متمرکز می‌شود.</p>\\n' +\n  '\\n' +\n  '<p>با فشردن <strong>Tab</strong> یا\\n' +\n  '  <strong>Shift+Tab</strong>، بین اجزای تعاملی این زبانه کادر گفتگو پیمایش کنید.</p>\\n' +\n  '\\n' +\n  '<p>با دادن تمرکز به منوی زبانه و سپس فشار دادن کلید <strong>جهت‌نمای</strong>\\n' +\n  '  مناسب برای چرخش میان زبانه‌های موجود، به زبانه کادر گفتگوی دیگری بروید.</p>\\n');"
  },
  {
    "path": "apps/web-antd/public/tinymce/plugins/help/js/i18n/keynav/fi.js",
    "content": "tinymce.Resource.add('tinymce.html-i18n.help-keynav.fi',\n'<h1>Näppäimistönavigoinnin aloittaminen</h1>\\n' +\n  '\\n' +\n  '<dl>\\n' +\n  '  <dt>Siirrä kohdistus valikkopalkkiin</dt>\\n' +\n  '  <dd>Windows tai Linux: Alt+F9</dd>\\n' +\n  '  <dd>macOS: &#x2325;F9</dd>\\n' +\n  '  <dt>Siirrä kohdistus työkalupalkkiin</dt>\\n' +\n  '  <dd>Windows tai Linux: Alt+F10</dd>\\n' +\n  '  <dd>macOS: &#x2325;F10</dd>\\n' +\n  '  <dt>Siirrä kohdistus alatunnisteeseen</dt>\\n' +\n  '  <dd>Windows tai Linux: Alt+F11</dd>\\n' +\n  '  <dd>macOS: &#x2325;F11</dd>\\n' +\n  '  <dt>Keskitä ilmoitukseen</dt>\\n' +\n  '  <dd>Windows ja Linux: Alt + F12</dd>\\n' +\n  '  <dd>macOS: &#x2325;F12</dd>\\n' +\n  '  <dt>Siirrä kohdistus kontekstuaaliseen työkalupalkkiin</dt>\\n' +\n  '  <dd>Windows, Linux tai macOS: Ctrl+F9</dd>\\n' +\n  '</dl>\\n' +\n  '\\n' +\n  '<p>Navigointi aloitetaan ensimmäisestä käyttöliittymän kohteesta, joka joko korostetaan tai alleviivataan, jos\\n' +\n  '  kyseessä on Alatunniste-elementin polun ensimmäinen kohde.</p>\\n' +\n  '\\n' +\n  '<h1>Käyttöliittymän eri osien välillä navigointi</h1>\\n' +\n  '\\n' +\n  '<p>Paina <strong>sarkainnäppäintä</strong> siirtyäksesi käyttöliittymän osasta seuraavaan.</p>\\n' +\n  '\\n' +\n  '<p>Jos haluat siirtyä edelliseen käyttöliittymän osaan, paina <strong>Shift+sarkainnäppäin</strong>.</p>\\n' +\n  '\\n' +\n  '<p><strong>Sarkainnäppäin</strong> siirtää sinua näissä käyttöliittymän osissa tässä järjestyksessä:</p>\\n' +\n  '\\n' +\n  '<ol>\\n' +\n  '  <li>Valikkopalkki</li>\\n' +\n  '  <li>Työkalupalkin ryhmät</li>\\n' +\n  '  <li>Sivupalkki</li>\\n' +\n  '  <li>Elementin polku alatunnisteessa</li>\\n' +\n  '  <li>Sanalaskurin vaihtopainike alatunnisteessa</li>\\n' +\n  '  <li>Brändäyslinkki alatunnisteessa</li>\\n' +\n  '  <li>Editorin koon muuttamisen kahva alatunnisteessa</li>\\n' +\n  '</ol>\\n' +\n  '\\n' +\n  '<p>Jos jotakin käyttöliittymän osaa ei ole, se ohitetaan.</p>\\n' +\n  '\\n' +\n  '<p>Jos kohdistus on siirretty alatunnisteeseen näppäimistönavigoinnilla eikä sivupalkkia ole näkyvissä, <strong>Shift+sarkainnäppäin</strong>\\n' +\n  '  siirtää kohdistuksen työkalupalkin ensimmäiseen ryhmään, eikä viimeiseen.</p>\\n' +\n  '\\n' +\n  '<h1>Käyttöliittymän eri osien sisällä navigointi</h1>\\n' +\n  '\\n' +\n  '<p>Paina <strong>nuolinäppäimiä</strong> siirtyäksesi käyttöliittymäelementistä seuraavaan.</p>\\n' +\n  '\\n' +\n  '<p><strong>Vasen</strong>- ja <strong>Oikea</strong>-nuolinäppäimet</p>\\n' +\n  '\\n' +\n  '<ul>\\n' +\n  '  <li>siirtävät sinua valikkopalkin valikoiden välillä.</li>\\n' +\n  '  <li>avaavat valikon alavalikon.</li>\\n' +\n  '  <li>siirtävät sinua työkalupalkin ryhmän painikkeiden välillä.</li>\\n' +\n  '  <li>siirtävät sinua kohteiden välillä alatunnisteen elementin polussa.</li>\\n' +\n  '</ul>\\n' +\n  '\\n' +\n  '<p><strong>Alas</strong>- ja <strong>Ylös</strong>-nuolinäppäimet</p>\\n' +\n  '\\n' +\n  '<ul>\\n' +\n  '  <li>siirtävät sinua valikon valikkokohteiden välillä.</li>\\n' +\n  '  <li>siirtävät sinua työkalupalkin ponnahdusvalikon kohteiden välillä.</li>\\n' +\n  '</ul>\\n' +\n  '\\n' +\n  '<p><strong>Nuolinäppäimet</strong> siirtävät sinua käyttöliittymän korostetun osan sisällä syklissä.</p>\\n' +\n  '\\n' +\n  '<p>Paina <strong>Esc</strong>-näppäintä sulkeaksesi avoimen valikon, avataksesi alavalikon tai avataksesi ponnahdusvalikon.</p>\\n' +\n  '\\n' +\n  '<p>Jos kohdistus on käyttöliittymän tietyn osion ylälaidassa, <strong>Esc</strong>-näppäimen painaminen\\n' +\n  '  poistuu myös näppäimistönavigoinnista kokonaan.</p>\\n' +\n  '\\n' +\n  '<h1>Suorita valikkokohde tai työkalupalkin painike</h1>\\n' +\n  '\\n' +\n  '<p>Kun haluamasi valikkokohde tai työkalupalkin painike on korostettuna, paina <strong>Return</strong>-, <strong>Enter</strong>-\\n' +\n  '  tai <strong>välilyöntinäppäintä</strong> suorittaaksesi kohteen.</p>\\n' +\n  '\\n' +\n  '<h1>Välilehdittömissä valintaikkunoissa navigointi</h1>\\n' +\n  '\\n' +\n  '<p>Kun välilehdetön valintaikkuna avautuu, kohdistus siirtyy sen ensimmäiseen interaktiiviseen komponenttiin.</p>\\n' +\n  '\\n' +\n  '<p>Voit siirtyä valintaikkunan interaktiivisten komponenttien välillä painamalla <strong>sarkainnäppäintä</strong> tai <strong>Shift+sarkainnäppäin</strong>.</p>\\n' +\n  '\\n' +\n  '<h1>Välilehdellisissä valintaikkunoissa navigointi</h1>\\n' +\n  '\\n' +\n  '<p>Kun välilehdellinen valintaikkuna avautuu, kohdistus siirtyy välilehtivalikon ensimmäiseen painikkeeseen.</p>\\n' +\n  '\\n' +\n  '<p>Voit siirtyä valintaikkunan välilehden interaktiivisen komponenttien välillä painamalla <strong>sarkainnäppäintä</strong> tai\\n' +\n  '  <strong>Shift+sarkainnäppäin</strong>.</p>\\n' +\n  '\\n' +\n  '<p>Voit siirtyä valintaikkunan toiseen välilehteen siirtämällä kohdistuksen välilehtivalikkoon ja painamalla sopivaa <strong>nuolinäppäintä</strong>\\n' +\n  '  siirtyäksesi käytettävissä olevien välilehtien välillä syklissä.</p>\\n');"
  },
  {
    "path": "apps/web-antd/public/tinymce/plugins/help/js/i18n/keynav/fr_FR.js",
    "content": "tinymce.Resource.add('tinymce.html-i18n.help-keynav.fr_FR',\n'<h1>Débuter la navigation au clavier</h1>\\n' +\n  '\\n' +\n  '<dl>\\n' +\n  '  <dt>Cibler la barre du menu</dt>\\n' +\n  '  <dd>Windows ou Linux : Alt+F9</dd>\\n' +\n  '  <dd>macOS : &#x2325;F9</dd>\\n' +\n  \"  <dt>Cibler la barre d'outils</dt>\\n\" +\n  '  <dd>Windows ou Linux : Alt+F10</dd>\\n' +\n  '  <dd>macOS : &#x2325;F10</dd>\\n' +\n  '  <dt>Cibler le pied de page</dt>\\n' +\n  '  <dd>Windows ou Linux : Alt+F11</dd>\\n' +\n  '  <dd>macOS : &#x2325;F11</dd>\\n' +\n  '  <dt>Cibler la notification</dt>\\n' +\n  '  <dd>Windows ou Linux : Alt+F12</dd>\\n' +\n  '  <dd>macOS : &#x2325;F12</dd>\\n' +\n  \"  <dt>Cibler une barre d'outils contextuelle</dt>\\n\" +\n  '  <dd>Windows, Linux ou macOS : Ctrl+F9</dd>\\n' +\n  '</dl>\\n' +\n  '\\n' +\n  \"<p>La navigation débutera sur le premier élément de l'interface utilisateur, qui sera mis en surbrillance ou bien souligné dans le cas du premier élément du\\n\" +\n  \"  chemin d'éléments du pied de page.</p>\\n\" +\n  '\\n' +\n  \"<h1>Naviguer entre les sections de l'interface utilisateur</h1>\\n\" +\n  '\\n' +\n  \"<p>Pour passer d'une section de l'interface utilisateur à la suivante, appuyez sur <strong>Tabulation</strong>.</p>\\n\" +\n  '\\n' +\n  \"<p>Pour passer d'une section de l'interface utilisateur à la précédente, appuyez sur <strong>Maj+Tabulation</strong>.</p>\\n\" +\n  '\\n' +\n  \"<p>L'ordre de <strong>Tabulation</strong> de ces sections de l'interface utilisateur est le suivant :</p>\\n\" +\n  '\\n' +\n  '<ol>\\n' +\n  '  <li>Barre du menu</li>\\n' +\n  \"  <li>Chaque groupe de barres d'outils</li>\\n\" +\n  '  <li>Barre latérale</li>\\n' +\n  \"  <li>Chemin d'éléments du pied de page</li>\\n\" +\n  \"  <li>Bouton d'activation du compteur de mots dans le pied de page</li>\\n\" +\n  '  <li>Lien de marque dans le pied de page</li>\\n' +\n  \"  <li>Poignée de redimensionnement de l'éditeur dans le pied de page</li>\\n\" +\n  '</ol>\\n' +\n  '\\n' +\n  \"<p>Si une section de l'interface utilisateur n'est pas présente, elle sera ignorée.</p>\\n\" +\n  '\\n' +\n  \"<p>Si le pied de page comporte un ciblage par navigation au clavier et qu'il n'y a aucune barre latérale visible, appuyer sur <strong>Maj+Tabulation</strong>\\n\" +\n  \"  déplace le ciblage vers le premier groupe de barres d'outils et non le dernier.</p>\\n\" +\n  '\\n' +\n  \"<h1>Naviguer au sein des sections de l'interface utilisateur</h1>\\n\" +\n  '\\n' +\n  \"<p>Pour passer d'un élément de l'interface utilisateur au suivant, appuyez sur la <strong>Flèche</strong> appropriée.</p>\\n\" +\n  '\\n' +\n  '<p>Les touches fléchées <strong>Gauche</strong> et <strong>Droite</strong></p>\\n' +\n  '\\n' +\n  '<ul>\\n' +\n  '  <li>se déplacent entre les menus de la barre des menus.</li>\\n' +\n  \"  <li>ouvrent un sous-menu au sein d'un menu.</li>\\n\" +\n  \"  <li>se déplacent entre les boutons d'un groupe de barres d'outils.</li>\\n\" +\n  \"  <li>se déplacent entre les éléments du chemin d'éléments du pied de page.</li>\\n\" +\n  '</ul>\\n' +\n  '\\n' +\n  '<p>Les touches fléchées <strong>Bas</strong> et <strong>Haut</strong></p>\\n' +\n  '\\n' +\n  '<ul>\\n' +\n  \"  <li>se déplacent entre les éléments de menu au sein d'un menu.</li>\\n\" +\n  \"  <li>se déplacent entre les éléments au sein d'un menu contextuel de barre d'outils.</li>\\n\" +\n  '</ul>\\n' +\n  '\\n' +\n  \"<p>Les <strong>Flèches</strong> parcourent la section de l'interface utilisateur ciblée.</p>\\n\" +\n  '\\n' +\n  '<p>Pour fermer un menu ouvert, un sous-menu ouvert ou un menu contextuel ouvert, appuyez sur <strong>Echap</strong>.</p>\\n' +\n  '\\n' +\n  \"<p>Si l'actuel ciblage se trouve en « haut » d'une section spécifique de l'interface utilisateur, appuyer sur <strong>Echap</strong> permet également de quitter\\n\" +\n  '  entièrement la navigation au clavier.</p>\\n' +\n  '\\n' +\n  \"<h1>Exécuter un élément de menu ou un bouton de barre d'outils</h1>\\n\" +\n  '\\n' +\n  \"<p>Lorsque l'élément de menu ou le bouton de barre d'outils désiré est mis en surbrillance, appuyez sur la touche <strong>Retour arrière</strong>, <strong>Entrée</strong>\\n\" +\n  \"  ou la <strong>Barre d'espace</strong> pour exécuter l'élément.</p>\\n\" +\n  '\\n' +\n  '<h1>Naviguer au sein de dialogues sans onglets</h1>\\n' +\n  '\\n' +\n  \"<p>Dans les dialogues sans onglets, le premier composant interactif est ciblé lorsque le dialogue s'ouvre.</p>\\n\" +\n  '\\n' +\n  '<p>Naviguez entre les composants du dialogue interactif en appuyant sur <strong>Tabulation</strong> ou <strong>Maj+Tabulation</strong>.</p>\\n' +\n  '\\n' +\n  '<h1>Naviguer au sein de dialogues avec onglets</h1>\\n' +\n  '\\n' +\n  \"<p>Dans les dialogues avec onglets, le premier bouton du menu de l'onglet est ciblé lorsque le dialogue s'ouvre.</p>\\n\" +\n  '\\n' +\n  '<p>Naviguez entre les composants interactifs de cet onglet de dialogue en appuyant sur <strong>Tabulation</strong> ou\\n' +\n  '  <strong>Maj+Tabulation</strong>.</p>\\n' +\n  '\\n' +\n  \"<p>Passez à un autre onglet de dialogue en ciblant le menu de l'onglet et en appuyant sur la <strong>Flèche</strong>\\n\" +\n  '  appropriée pour parcourir les onglets disponibles.</p>\\n');"
  },
  {
    "path": "apps/web-antd/public/tinymce/plugins/help/js/i18n/keynav/he_IL.js",
    "content": "tinymce.Resource.add('tinymce.html-i18n.help-keynav.he_IL',\n'<h1>התחל ניווט במקלדת</h1>\\n' +\n  '\\n' +\n  '<dl>\\n' +\n  '  <dt>התמקד בשורת התפריטים</dt>\\n' +\n  '  <dd>Windows או Linux:‏ Alt+F9</dd>\\n' +\n  '  <dd>macOS: &#x2325;F9</dd>\\n' +\n  '  <dt>העבר מיקוד לסרגל הכלים</dt>\\n' +\n  '  <dd>Windows או Linux:‏ Alt+F10</dd>\\n' +\n  '  <dd>macOS: &#x2325;F10</dd>\\n' +\n  '  <dt>העבר מיקוד לכותרת התחתונה</dt>\\n' +\n  '  <dd>Windows או Linux:‏ Alt+F11</dd>\\n' +\n  '  <dd>macOS: &#x2325;F11</dd>\\n' +\n  '  <dt>העבר מיקוד להודעה</dt>\\n' +\n  '  <dd>Windows או Linux:‏ Alt+F12</dd>\\n' +\n  '  <dd>macOS: &#x2325;F12</dd>\\n' +\n  '  <dt>העבר מיקוד לסרגל כלים הקשרי</dt>\\n' +\n  '  <dd>Windows‏, Linux או macOS:‏ Ctrl+F9</dd>\\n' +\n  '</dl>\\n' +\n  '\\n' +\n  '<p>הניווט יתחיל ברכיב הראשון במשך, שיודגש או שיהיה מתחתיו קו תחתון במקרה של הפריט הראשון\\n' +\n  '  הנתיב של רכיב הכותרת התחתונה.</p>\\n' +\n  '\\n' +\n  '<h1>עבור בין מקטעים במסך</h1>\\n' +\n  '\\n' +\n  '<p>כדי לעבור בין המקטעים במסך, הקש <strong>Tab</strong>.</p>\\n' +\n  '\\n' +\n  '<p>כדי לעבור למקטע הקודם במסך, הקש <strong>Shift+Tab</strong>.</p>\\n' +\n  '\\n' +\n  '<p>הסדר מבחינת מקש <strong>Tab</strong> של הרכיבים במסך:</p>\\n' +\n  '\\n' +\n  '<ol>\\n' +\n  '  <li>שורת התפריטים</li>\\n' +\n  '  <li>כל קבוצה בסרגל הכלים</li>\\n' +\n  '  <li>הסרגל הצידי</li>\\n' +\n  '  <li>נתיב של רכיב בכותרת התחתונה</li>\\n' +\n  '  <li>לחצן לספירת מילים בכותרת התחתונה</li>\\n' +\n  '  <li>קישור של המותג בכותרת התחתונה</li>\\n' +\n  '  <li>ידית לשינוי גודל עבור העורך בכותרת התחתונה</li>\\n' +\n  '</ol>\\n' +\n  '\\n' +\n  '<p>אם רכיב כלשהו במסך לא מופיע, המערכת תדלג עליו.</p>\\n' +\n  '\\n' +\n  '<p>אם בכותרת התחתונה יש מיקוד של ניווט במקלדת, ולא מופיע סרגל בצד, יש להקיש <strong>Shift+Tab</strong>\\n' +\n  '  מעביר את המיקוד לקבוצה הראשונה בסרגל הכלים, לא האחרונה.</p>\\n' +\n  '\\n' +\n  '<h1>עבור בתוך מקטעים במסך</h1>\\n' +\n  '\\n' +\n  '<p>כדי לעבור מרכיב אחד לרכיב אחר במסך, הקש על מקש <strong>החץ</strong> המתאים.</p>\\n' +\n  '\\n' +\n  '<p>מקשי החיצים <strong>שמאלה</strong> ו<strong>ימינה</strong></p>\\n' +\n  '\\n' +\n  '<ul>\\n' +\n  '  <li>עבור בין תפריטים בשורת התפריטים.</li>\\n' +\n  '  <li>פתח תפריט משני בתפריט.</li>\\n' +\n  '  <li>עבור בין לחצנים בקבוצה בסרגל הכלים.</li>\\n' +\n  '  <li>עבור בין פריטים ברכיב בכותרת התחתונה.</li>\\n' +\n  '</ul>\\n' +\n  '\\n' +\n  '<p>מקשי החיצים <strong>למטה</strong> ו<strong>למעלה</strong></p>\\n' +\n  '\\n' +\n  '<ul>\\n' +\n  '  <li>עבור בין פריטים בתפריט.</li>\\n' +\n  '  <li>עבור בין פריטים בחלון הקובץ של סרגל הכלים.</li>\\n' +\n  '</ul>\\n' +\n  '\\n' +\n  '<p>מקשי <strong>החצים</strong> משתנים בתוך המקטע במסך שעליו נמצא המיקוד.</p>\\n' +\n  '\\n' +\n  '<p>כדי לסגור תפריט פתוח, תפריט משני פתוח או חלון קופץ, הקש על <strong>Esc</strong>.</p>\\n' +\n  '\\n' +\n  \"<p>אם המיקוד הוא על החלק 'העליון' של מקטע מסוים במסך, הקשה על <strong>Esc</strong> מביאה גם ליציאה\\n\" +\n  '  מהניווט במקלדת לחלוטין.</p>\\n' +\n  '\\n' +\n  '<h1>הפעל פריט בתפריט או לחצן בסרגל הכלים</h1>\\n' +\n  '\\n' +\n  '<p>כאשר הפריט הרצוי בתפריט או הלחצן בסרגל הכלים מודגשים, הקש על <strong>Return</strong>, <strong>Enter</strong>,\\n' +\n  '  או על <strong>מקש הרווח</strong> כדי להפעיל את הפריט.</p>\\n' +\n  '\\n' +\n  '<h1>ניווט בחלונות דו-שיח בלי כרטיסיות</h1>\\n' +\n  '\\n' +\n  '<p>בחלונות דו-שיח בלי כרטיסיות, הרכיב האינטראקטיבי הראשון מקבל את המיקוד כאשר החלון נפתח.</p>\\n' +\n  '\\n' +\n  '<p>עבור בין רכיבים אינטראקטיביים בחלון על ידי הקשה על <strong>Tab</strong> או <strong>Shift+Tab</strong>.</p>\\n' +\n  '\\n' +\n  '<h1>ניווט בחלונות דו-שיח עם כרטיסיות</h1>\\n' +\n  '\\n' +\n  '<p>בחלונות דו-שיח עם כרטיסיות, הלחצן הראשון בתפריט מקבל את המיקוד כאשר החלון נפתח.</p>\\n' +\n  '\\n' +\n  '<p>עבור בין רכיבים אינטראקטיביים בחלון על ידי הקשה על <strong>Tab</strong> או\\n' +\n  '  <strong>Shift+Tab</strong>.</p>\\n' +\n  '\\n' +\n  '<p>עבור לכרטיסיה אחרת בחלון על ידי העברת המיקוד לתפריט הכרטיסיות והקשה על <strong>החץ</strong>המתאים\\n' +\n  '  כדי לעבור בין הכרטיסיות הזמינות.</p>\\n');"
  },
  {
    "path": "apps/web-antd/public/tinymce/plugins/help/js/i18n/keynav/hi.js",
    "content": "tinymce.Resource.add('tinymce.html-i18n.help-keynav.hi',\n'<h1>कीबोर्ड नेविगेशन शुरू करें</h1>\\n' +\n  '\\n' +\n  '<dl>\\n' +\n  '  <dt>मेन्यू बार पर फ़ोकस करें</dt>\\n' +\n  '  <dd>Windows या Linux: Alt+F9</dd>\\n' +\n  '  <dd>macOS: &#x2325;F9</dd>\\n' +\n  '  <dt>टूलबार पर फ़ोकस करें</dt>\\n' +\n  '  <dd>Windows या Linux: Alt+F10</dd>\\n' +\n  '  <dd>macOS: &#x2325;F10</dd>\\n' +\n  '  <dt>फ़ुटर पर फ़ोकस करें</dt>\\n' +\n  '  <dd>Windows या Linux: Alt+F11</dd>\\n' +\n  '  <dd>macOS: &#x2325;F11</dd>\\n' +\n  '  <dt>नोटिफ़िकेशन फ़ोकस</dt>\\n' +\n  '  <dd>Windows या Linux: Alt+F12</dd>\\n' +\n  '  <dd>macOS: &#x2325;F12</dd>\\n' +\n  '  <dt>प्रासंगिक टूलबार पर फ़ोकस करें</dt>\\n' +\n  '  <dd>Windows, Linux या macOS: Ctrl+F9</dd>\\n' +\n  '</dl>\\n' +\n  '\\n' +\n  '<p>नेविगेशन पहले UI आइटम पर शुरू होगा, जिसे हाइलाइट किया जाएगा या पहले आइटम के मामले में फ़ुटर तत्व पथ में\\n' +\n  '  रेखांकित किया जाएगा।</p>\\n' +\n  '\\n' +\n  '<h1>UI सेक्शन के बीच नेविगेट करें</h1>\\n' +\n  '\\n' +\n  '<p>एक UI सेक्शन से दूसरे सेक्शन में जाने के लिए, <strong>Tab</strong> दबाएं।</p>\\n' +\n  '\\n' +\n  '<p>एक UI सेक्शन से पिछले सेक्शन में जाने के लिए, <strong>Shift+Tab</strong> दबाएं।</p>\\n' +\n  '\\n' +\n  '<p>इन UI सेक्शन का <strong>Tab</strong> क्रम नीचे दिया गया है:</p>\\n' +\n  '\\n' +\n  '<ol>\\n' +\n  '  <li>मेन्यू बार</li>\\n' +\n  '  <li>प्रत्येक टूलबार समूह</li>\\n' +\n  '  <li>साइडबार</li>\\n' +\n  '  <li>फ़ुटर में तत्व पथ</li>\\n' +\n  '  <li>फ़ुटर में शब्द गणना टॉगल बटन</li>\\n' +\n  '  <li>फ़ुटर में ब्रांडिंग लिंक</li>\\n' +\n  '  <li>फ़ुटर में संपादक का आकार बदलने का हैंडल</li>\\n' +\n  '</ol>\\n' +\n  '\\n' +\n  '<p>अगर कोई UI सेक्शन मौजूद नहीं है, तो उसे छोड़ दिया जाता है।</p>\\n' +\n  '\\n' +\n  '<p>अगर फ़ुटर में कीबोर्ड नेविगेशन फ़ोकस है, और कोई दिखा देने वाला साइडबार नहीं है, तो <strong>Shift+Tab</strong> दबाने से\\n' +\n  '  फ़ोकस पहले टूलबार समूह पर चला जाता है, पिछले पर नहीं।</p>\\n' +\n  '\\n' +\n  '<h1>UI सेक्शन के भीतर नेविगेट करें</h1>\\n' +\n  '\\n' +\n  '<p>एक UI तत्व से दूसरे में जाने के लिए उपयुक्त <strong>ऐरो</strong> कुंजी दबाएं।</p>\\n' +\n  '\\n' +\n  '<p><strong>बाएं</strong> और <strong>दाएं</strong> ऐरो कुंजियां</p>\\n' +\n  '\\n' +\n  '<ul>\\n' +\n  '  <li>मेन्यू बार में मेन्यू के बीच ले जाती हैं।</li>\\n' +\n  '  <li>मेन्यू में एक सब-मेन्यू खोलें।</li>\\n' +\n  '  <li>टूलबार समूह में बटनों के बीच ले जाएं।</li>\\n' +\n  '  <li>फ़ुटर के तत्व पथ में आइटम के बीच ले जाएं।</li>\\n' +\n  '</ul>\\n' +\n  '\\n' +\n  '<p><strong>नीचे</strong> और <strong>ऊपर</strong> ऐरो कुंजियां</p>\\n' +\n  '\\n' +\n  '<ul>\\n' +\n  '  <li>मेन्यू में मेन्यू आइटम के बीच ले जाती हैं।</li>\\n' +\n  '  <li>टूलबार पॉप-अप मेन्यू में आइटम के बीच ले जाएं।</li>\\n' +\n  '</ul>\\n' +\n  '\\n' +\n  '<p>फ़ोकस वाले UI सेक्शन के भीतर <strong>ऐरो</strong> कुंजियां चलाती रहती हैं।</p>\\n' +\n  '\\n' +\n  '<p>कोई खुला मेन्यू, कोई खुला सब-मेन्यू या कोई खुला पॉप-अप मेन्यू बंद करने के लिए <strong>Esc</strong> कुंजी दबाएं।</p>\\n' +\n  '\\n' +\n  \"<p>अगर मौजूदा फ़ोकस किसी विशेष UI सेक्शन के 'शीर्ष' पर है, तो <strong>Esc</strong> कुंजी दबाने से भी\\n\" +\n  '  कीबोर्ड नेविगेशन पूरी तरह से बाहर हो जाता है।</p>\\n' +\n  '\\n' +\n  '<h1>मेन्यू आइटम या टूलबार बटन निष्पादित करें</h1>\\n' +\n  '\\n' +\n  '<p>जब वांछित मेन्यू आइटम या टूलबार बटन हाइलाइट किया जाता है, तो आइटम को निष्पादित करने के लिए <strong>Return</strong>, <strong>Enter</strong>,\\n' +\n  '  या <strong>Space bar</strong> दबाएं।</p>\\n' +\n  '\\n' +\n  '<h1>गैर-टैब वाले डायलॉग पर नेविगेट करें</h1>\\n' +\n  '\\n' +\n  '<p>गैर-टैब वाले डायलॉग में, डायलॉग खुलने पर पहला इंटरैक्टिव घटक फ़ोकस लेता है।</p>\\n' +\n  '\\n' +\n  '<p><strong>Tab</strong> or <strong>Shift+Tab</strong> दबाकर इंटरैक्टिव डायलॉग घटकों के बीच नेविगेट करें।</p>\\n' +\n  '\\n' +\n  '<h1>टैब किए गए डायलॉग पर नेविगेट करें</h1>\\n' +\n  '\\n' +\n  '<p>टैब किए गए डायलॉग में, डायलॉग खुलने पर टैब मेन्यू में पहला बटन फ़ोकस लेता है।</p>\\n' +\n  '\\n' +\n  '<p>इस डायलॉग टैब के इंटरैक्टिव घटकों के बीच नेविगेट करने के लिए <strong>Tab</strong> या\\n' +\n  '  <strong>Shift+Tab</strong> दबाएं।</p>\\n' +\n  '\\n' +\n  '<p>टैब मेन्यू को फ़ोकस देकर और फिर उपलब्ध टैब में के बीच जाने के लिए उपयुक्त <strong>ऐरो</strong>\\n' +\n  '  कुंजी दबाकर दूसरे डायलॉग टैब पर स्विच करें।</p>\\n');"
  },
  {
    "path": "apps/web-antd/public/tinymce/plugins/help/js/i18n/keynav/hr.js",
    "content": "tinymce.Resource.add('tinymce.html-i18n.help-keynav.hr',\n'<h1>Početak navigacije na tipkovnici</h1>\\n' +\n  '\\n' +\n  '<dl>\\n' +\n  '  <dt>Fokusiranje trake izbornika</dt>\\n' +\n  '  <dd>Windows ili Linux: Alt+F9</dd>\\n' +\n  '  <dd>macOS: &#x2325;F9</dd>\\n' +\n  '  <dt>Fokusiranje alatne trake</dt>\\n' +\n  '  <dd>Windows ili Linux: Alt+F10</dd>\\n' +\n  '  <dd>macOS: &#x2325;F10</dd>\\n' +\n  '  <dt>Fokusiranje podnožja</dt>\\n' +\n  '  <dd>Windows ili Linux: Alt+F11</dd>\\n' +\n  '  <dd>macOS: &#x2325;F11</dd>\\n' +\n  '  <dt>Fokusiranje obavijesti</dt>\\n' +\n  '  <dd>Windows ili Linux: Alt + F12</dd>\\n' +\n  '  <dd>macOS: &#x2325;F12</dd>\\n' +\n  '  <dt>Fokusiranje kontekstne alatne trake</dt>\\n' +\n  '  <dd>Windows, Linux ili macOS: Ctrl+F9</dd>\\n' +\n  '</dl>\\n' +\n  '\\n' +\n  '<p>Navigacija će započeti kod prve stavke na korisničkom sučelju, koja će biti istaknuta ili podcrtana ako se radi o prvoj stavci u\\n' +\n  '  putu elementa u podnožju.</p>\\n' +\n  '\\n' +\n  '<h1>Navigacija između dijelova korisničkog sučelja</h1>\\n' +\n  '\\n' +\n  '<p>Za pomicanje s jednog dijela korisničkog sučelja na drugi pritisnite <strong>tabulator</strong>.</p>\\n' +\n  '\\n' +\n  '<p>Za pomicanje s jednog dijela korisničkog sučelja na prethodni pritisnite <strong>Shift + tabulator</strong>.</p>\\n' +\n  '\\n' +\n  '<p>Ovo je redoslijed pomicanja <strong>tabulatora</strong> po dijelovima korisničkog sučelja:</p>\\n' +\n  '\\n' +\n  '<ol>\\n' +\n  '  <li>Traka izbornika</li>\\n' +\n  '  <li>Pojedinačne grupe na alatnoj traci</li>\\n' +\n  '  <li>Bočna traka</li>\\n' +\n  '  <li>Put elemenata u podnožju</li>\\n' +\n  '  <li>Gumb za pomicanje po broju riječi u podnožju</li>\\n' +\n  '  <li>Veza na brand u podnožju</li>\\n' +\n  '  <li>Značajka za promjenu veličine alata za uređivanje u podnožju</li>\\n' +\n  '</ol>\\n' +\n  '\\n' +\n  '<p>Ako neki dio korisničkog sučelja nije naveden, on se preskače.</p>\\n' +\n  '\\n' +\n  '<p>Ako u podnožju postoji fokus za navigaciju na tipkovnici, a nema vidljive bočne trake, pritiskom na <strong>Shift + tabulator</strong>\\n' +\n  '  fokus se prebacuje na prvu skupinu na alatnoj traci, ne na zadnju.</p>\\n' +\n  '\\n' +\n  '<h1>Navigacija unutar dijelova korisničkog sučelja</h1>\\n' +\n  '\\n' +\n  '<p>Za pomicanje s jednog elementa korisničkog sučelja na drugi pritisnite tipku s odgovarajućom <strong>strelicom</strong>.</p>\\n' +\n  '\\n' +\n  '<p>Tipke s <strong>lijevom</strong> i <strong>desnom</strong> strelicom</p>\\n' +\n  '\\n' +\n  '<ul>\\n' +\n  '  <li>služe za pomicanje između izbornika na alatnoj traci.</li>\\n' +\n  '  <li>otvaraju podizbornik unutar izbornika.</li>\\n' +\n  '  <li>služe za pomicanje između gumba unutar skupina na alatnoj traci.</li>\\n' +\n  '  <li>služe za pomicanje između stavki na elementu puta u podnožju.</li>\\n' +\n  '</ul>\\n' +\n  '\\n' +\n  '<p>Tipke s <strong>donjom</strong> i <strong>gornjom</strong> strelicom</p>\\n' +\n  '\\n' +\n  '<ul>\\n' +\n  '  <li>služe za pomicanje između stavki unutar izbornika.</li>\\n' +\n  '  <li>služe za pomicanje između stavki na alatnoj traci skočnog izbornika.</li>\\n' +\n  '</ul>\\n' +\n  '\\n' +\n  '<p>Tipkama <strong>strelica</strong> kružno se pomičete unutar dijela korisničkog sučelja koji je u fokusu.</p>\\n' +\n  '\\n' +\n  '<p>Za zatvaranje otvorenog izbornika, otvorenog podizbornika ili otvorenog skočnog izbornika pritisnite tipku <strong>Esc</strong>.</p>\\n' +\n  '\\n' +\n  '<p>Ako je fokus trenutačno postavljen na vrh pojedinačnog dijela korisničkog sučelja, pritiskom na tipku <strong>Esc</strong> također\\n' +\n  '  u potpunosti zatvarate navigaciju na tipkovnici.</p>\\n' +\n  '\\n' +\n  '<h1>Izvršavanje radnji putem stavki izbornika ili gumba na alatnoj traci</h1>\\n' +\n  '\\n' +\n  '<p>Nakon što se istakne stavka izbornika ili gumb na alatnoj traci s radnjom koju želite izvršiti, pritisnite tipku <strong>Return</strong>, <strong>Enter</strong>\\n' +\n  '  ili <strong>razmak</strong> da biste pokrenuli željenu radnju.</p>\\n' +\n  '\\n' +\n  '<h1>Navigacija dijaloškim okvirima izvan kartica</h1>\\n' +\n  '\\n' +\n  '<p>Prilikom otvaranja dijaloških okvira izvan kartica fokus se nalazi na prvoj interaktivnoj komponenti.</p>\\n' +\n  '\\n' +\n  '<p>Navigaciju između interaktivnih dijaloških komponenata vršite pritiskom na <strong>tabulator</strong> ili <strong>Shift + tabulator</strong>.</p>\\n' +\n  '\\n' +\n  '<h1>Navigacija dijaloškim okvirima u karticama</h1>\\n' +\n  '\\n' +\n  '<p>Prilikom otvaranja dijaloških okvira u karticama fokus se nalazi na prvom gumbu u izborniku unutar kartice.</p>\\n' +\n  '\\n' +\n  '<p>Navigaciju između interaktivnih komponenata dijaloškog okvira u kartici vršite pritiskom na <strong>tabulator</strong> ili\\n' +\n  '  <strong>Shift + tabulator</strong>.</p>\\n' +\n  '\\n' +\n  '<p>Na karticu s drugim dijaloškim okvirom možete se prebaciti tako da stavite fokus na izbornik kartice pa pritisnete tipku s odgovarajućom <strong>strelicom</strong>\\n' +\n  '  za kružno pomicanje između dostupnih kartica.</p>\\n');"
  },
  {
    "path": "apps/web-antd/public/tinymce/plugins/help/js/i18n/keynav/hu_HU.js",
    "content": "tinymce.Resource.add('tinymce.html-i18n.help-keynav.hu_HU',\n'<h1>Billentyűzetes navigáció indítása</h1>\\n' +\n  '\\n' +\n  '<dl>\\n' +\n  '  <dt>Fókusz a menüsávra</dt>\\n' +\n  '  <dd>Windows és Linux: Alt+F9</dd>\\n' +\n  '  <dd>macOS: &#x2325;F9</dd>\\n' +\n  '  <dt>Fókusz az eszköztárra</dt>\\n' +\n  '  <dd>Windows és Linux: Alt+F10</dd>\\n' +\n  '  <dd>macOS: &#x2325;F10</dd>\\n' +\n  '  <dt>Fókusz a láblécre</dt>\\n' +\n  '  <dd>Windows és Linux: Alt+F11</dd>\\n' +\n  '  <dd>macOS: &#x2325;F11</dd>\\n' +\n  '  <dt>Ráközelítés az értesítésre</dt>\\n' +\n  '  <dd>Windows vagy Linux: Alt+F12</dd>\\n' +\n  '  <dd>macOS: &#x2325;F12</dd>\\n' +\n  '  <dt>Fókusz egy környezetfüggő eszköztárra</dt>\\n' +\n  '  <dd>Windows, Linux és macOS: Ctrl+F9</dd>\\n' +\n  '</dl>\\n' +\n  '\\n' +\n  '<p>A navigáció az első felhasználói felületi elemnél kezdődik, amelyet a rendszer kiemel, illetve aláhúz, amennyiben az az első elem\\n' +\n  '  a lábléc elemútvonalán.</p>\\n' +\n  '\\n' +\n  '<h1>Navigálás a felhasználói felület szakaszai között</h1>\\n' +\n  '\\n' +\n  '<p>A felhasználói felület következő szakaszára váltáshoz nyomja meg a <strong>Tab</strong> billentyűt.</p>\\n' +\n  '\\n' +\n  '<p>A felhasználói felület előző szakaszára váltáshoz nyomja meg a <strong>Shift+Tab</strong> billentyűt.</p>\\n' +\n  '\\n' +\n  '<p>A <strong>Tab</strong> billentyűvel a felhasználói felület szakaszai között a következő sorrendben vált:</p>\\n' +\n  '\\n' +\n  '<ol>\\n' +\n  '  <li>Menüsáv</li>\\n' +\n  '  <li>Az egyes eszköztárcsoportok</li>\\n' +\n  '  <li>Oldalsáv</li>\\n' +\n  '  <li>Elemútvonal a láblécen</li>\\n' +\n  '  <li>Szószámátkapcsoló gomb a láblécen</li>\\n' +\n  '  <li>Márkalink a láblécen</li>\\n' +\n  '  <li>Szerkesztő átméretezési fogópontja a láblécen</li>\\n' +\n  '</ol>\\n' +\n  '\\n' +\n  '<p>Ha a felhasználói felület valamelyik eleme nincs jelen, a rendszer kihagyja.</p>\\n' +\n  '\\n' +\n  '<p>Ha a billentyűzetes navigáció fókusza a láblécen van, és nincs látható oldalsáv, a <strong>Shift+Tab</strong>\\n' +\n  '  billentyűkombináció lenyomásakor az első eszköztárcsoportra ugrik a fókusz, nem az utolsóra.</p>\\n' +\n  '\\n' +\n  '<h1>Navigálás a felhasználói felület szakaszain belül</h1>\\n' +\n  '\\n' +\n  '<p>A felhasználói felület következő elemére váltáshoz nyomja meg a megfelelő <strong>nyílbillentyűt</strong>.</p>\\n' +\n  '\\n' +\n  '<p>A <strong>bal</strong> és a <strong>jobb</strong> nyílgomb</p>\\n' +\n  '\\n' +\n  '<ul>\\n' +\n  '  <li>a menüsávban a menük között vált.</li>\\n' +\n  '  <li>a menükben megnyit egy almenüt.</li>\\n' +\n  '  <li>az eszköztárcsoportban a gombok között vált.</li>\\n' +\n  '  <li>a lábléc elemútvonalán az elemek között vált.</li>\\n' +\n  '</ul>\\n' +\n  '\\n' +\n  '<p>A <strong>le</strong> és a <strong>fel</strong> nyílgomb</p>\\n' +\n  '\\n' +\n  '<ul>\\n' +\n  '  <li>a menükben a menüpontok között vált.</li>\\n' +\n  '  <li>az eszköztár előugró menüjében az elemek között vált.</li>\\n' +\n  '</ul>\\n' +\n  '\\n' +\n  '<p>A <strong>nyílbillentyűk</strong> lenyomásával körkörösen lépkedhet a fókuszban lévő felhasználói felületi szakasz elemei között.</p>\\n' +\n  '\\n' +\n  '<p>A megnyitott menüket, almenüket és előugró menüket az <strong>Esc</strong> billentyűvel zárhatja be.</p>\\n' +\n  '\\n' +\n  '<p>Ha a fókusz az aktuális felületi elem „felső” részén van, az <strong>Esc</strong> billentyűvel az egész\\n' +\n  '  billentyűzetes navigációból kilép.</p>\\n' +\n  '\\n' +\n  '<h1>Menüpont vagy eszköztárgomb aktiválása</h1>\\n' +\n  '\\n' +\n  '<p>Amikor a kívánt menüelem vagy eszköztárgomb van kijelölve, nyomja meg a <strong>Return</strong>, az <strong>Enter</strong>\\n' +\n  '  vagy a <strong>Szóköz</strong> billentyűt az adott elem vagy gomb aktiválásához.</p>\\n' +\n  '\\n' +\n  '<h1>Navigálás a lapokkal nem rendelkező párbeszédablakokban</h1>\\n' +\n  '\\n' +\n  '<p>A lapokkal nem rendelkező párbeszédablakokban az első interaktív összetevő kapja a fókuszt, amikor a párbeszédpanel megnyílik.</p>\\n' +\n  '\\n' +\n  '<p>A párbeszédpanelek interaktív összetevői között a <strong>Tab</strong> vagy a <strong>Shift+Tab</strong> billentyűvel navigálhat.</p>\\n' +\n  '\\n' +\n  '<h1>Navigálás a lapokkal rendelkező párbeszédablakokban</h1>\\n' +\n  '\\n' +\n  '<p>A lapokkal rendelkező párbeszédablakokban a lapmenü első gombja kapja a fókuszt, amikor a párbeszédpanel megnyílik.</p>\\n' +\n  '\\n' +\n  '<p>A párbeszédpanel e lapjának interaktív összetevői között a <strong>Tab</strong> vagy\\n' +\n  '  <strong>Shift+Tab</strong> billentyűvel navigálhat.</p>\\n' +\n  '\\n' +\n  '<p>A párbeszédablak másik lapjára úgy léphet, hogy a fókuszt a lapmenüre állítja, majd lenyomja a megfelelő <strong>nyílbillentyűt</strong>\\n' +\n  '  a rendelkezésre álló lapok közötti lépkedéshez.</p>\\n');"
  },
  {
    "path": "apps/web-antd/public/tinymce/plugins/help/js/i18n/keynav/id.js",
    "content": "tinymce.Resource.add('tinymce.html-i18n.help-keynav.id',\n'<h1>Memulai navigasi keyboard</h1>\\n' +\n  '\\n' +\n  '<dl>\\n' +\n  '  <dt>Fokus pada bilah Menu</dt>\\n' +\n  '  <dd>Windows atau Linux: Alt+F9</dd>\\n' +\n  '  <dd>macOS: &#x2325;F9</dd>\\n' +\n  '  <dt>Fokus pada Bilah Alat</dt>\\n' +\n  '  <dd>Windows atau Linux: Alt+F10</dd>\\n' +\n  '  <dd>macOS: &#x2325;F10</dd>\\n' +\n  '  <dt>Fokus pada footer</dt>\\n' +\n  '  <dd>Windows atau Linux: Alt+F11</dd>\\n' +\n  '  <dd>macOS: &#x2325;F11</dd>\\n' +\n  '  <dt>Fokuskan pemberitahuan</dt>\\n' +\n  '  <dd>Windows atau Linux: Alt+F12</dd>\\n' +\n  '  <dd>macOS: &#x2325;F12</dd>\\n' +\n  '  <dt>Fokus pada bilah alat kontekstual</dt>\\n' +\n  '  <dd>Windows, Linux, atau macOS: Ctrl+F9</dd>\\n' +\n  '</dl>\\n' +\n  '\\n' +\n  '<p>Navigasi akan dimulai dari item pertama UI, yang akan disorot atau digarisbawahi di\\n' +\n  '  alur elemen Footer.</p>\\n' +\n  '\\n' +\n  '<h1>Berpindah antar-bagian UI</h1>\\n' +\n  '\\n' +\n  '<p>Untuk berpindah dari satu bagian UI ke bagian berikutnya, tekan <strong>Tab</strong>.</p>\\n' +\n  '\\n' +\n  '<p>Untuk berpindah dari satu bagian UI ke bagian sebelumnya, tekan <strong>Shift+Tab</strong>.</p>\\n' +\n  '\\n' +\n  '<p>Urutan <strong>Tab</strong> bagian-bagian UI ini adalah:</p>\\n' +\n  '\\n' +\n  '<ol>\\n' +\n  '  <li>Bilah menu</li>\\n' +\n  '  <li>Tiap grup bilah alat</li>\\n' +\n  '  <li>Bilah sisi</li>\\n' +\n  '  <li>Alur elemen di footer</li>\\n' +\n  '  <li>Tombol aktifkan/nonaktifkan jumlah kata di footer</li>\\n' +\n  '  <li>Tautan merek di footer</li>\\n' +\n  '  <li>Pengatur pengubahan ukuran editor di footer</li>\\n' +\n  '</ol>\\n' +\n  '\\n' +\n  '<p>Jika suatu bagian UI tidak ada, bagian tersebut dilewati.</p>\\n' +\n  '\\n' +\n  '<p>Jika fokus navigasi keyboard ada pada footer, tetapi tidak ada bilah sisi yang terlihat, menekan <strong>Shift+Tab</strong>\\n' +\n  '  akan memindahkan fokus ke grup bilah alat pertama, bukan yang terakhir.</p>\\n' +\n  '\\n' +\n  '<h1>Berpindah di dalam bagian-bagian UI</h1>\\n' +\n  '\\n' +\n  '<p>Untuk berpindah dari satu elemen UI ke elemen berikutnya, tekan tombol <strong>Panah</strong> yang sesuai.</p>\\n' +\n  '\\n' +\n  '<p>Tombol panah <strong>Kiri</strong> dan <strong>Kanan</strong> untuk</p>\\n' +\n  '\\n' +\n  '<ul>\\n' +\n  '  <li>berpindah-pindah antar-menu di dalam bilah menu.</li>\\n' +\n  '  <li>membuka sub-menu di dalam menu.</li>\\n' +\n  '  <li>berpindah-pindah antar-tombol di dalam grup bilah alat.</li>\\n' +\n  '  <li>berpindah-pindah antar-item di dalam alur elemen footer.</li>\\n' +\n  '</ul>\\n' +\n  '\\n' +\n  '<p>Tombol panah <strong>Bawah</strong> dan <strong>Atas</strong> untuk</p>\\n' +\n  '\\n' +\n  '<ul>\\n' +\n  '  <li>berpindah-pindah antar-item menu di dalam menu.</li>\\n' +\n  '  <li>berpindah-pindah antar-item di dalam menu pop-up bilah alat.</li>\\n' +\n  '</ul>\\n' +\n  '\\n' +\n  '<p>Tombol <strong>Panah</strong> hanya bergerak di dalam bagian UI yang difokuskan.</p>\\n' +\n  '\\n' +\n  '<p>Untuk menutup menu, sub-menu, atau menu pop-up yang terbuka, tekan tombol <strong>Esc</strong>.</p>\\n' +\n  '\\n' +\n  '<p>Jika fokus sedang berada di ‘atas’ bagian UI tertentu, menekan tombol <strong>Esc</strong> juga dapat mengeluarkan fokus\\n' +\n  '  dari seluruh navigasi keyboard.</p>\\n' +\n  '\\n' +\n  '<h1>Menjalankan item menu atau tombol bilah alat</h1>\\n' +\n  '\\n' +\n  '<p>Jika item menu atau tombol bilah alat yang diinginkan tersorot, tekan <strong>Return</strong>, <strong>Enter</strong>,\\n' +\n  '  atau <strong>Spasi</strong> untuk menjalankan item.</p>\\n' +\n  '\\n' +\n  '<h1>Berpindah dalam dialog tanpa tab</h1>\\n' +\n  '\\n' +\n  '<p>Dalam dialog tanpa tab, fokus diarahkan pada komponen interaktif pertama saat dialog terbuka.</p>\\n' +\n  '\\n' +\n  '<p>Berpindah di antara komponen dalam dialog interaktif dengan menekan <strong>Tab</strong> atau <strong>Shift+Tab</strong>.</p>\\n' +\n  '\\n' +\n  '<h1>Berpindah dalam dialog dengan tab</h1>\\n' +\n  '\\n' +\n  '<p>Dalam dialog yang memiliki tab, fokus diarahkan pada tombol pertama di dalam menu saat dialog terbuka.</p>\\n' +\n  '\\n' +\n  '<p>Berpindah di antara komponen-komponen interaktif pada tab dialog ini dengan menekan <strong>Tab</strong> atau\\n' +\n  '  <strong>Shift+Tab</strong>.</p>\\n' +\n  '\\n' +\n  '<p>Beralih ke tab dialog lain dengan mengarahkan fokus pada menu tab lalu tekan tombol <strong>Panah</strong>\\n' +\n  '  yang sesuai untuk berpindah ke berbagai tab yang tersedia.</p>\\n');"
  },
  {
    "path": "apps/web-antd/public/tinymce/plugins/help/js/i18n/keynav/it.js",
    "content": "tinymce.Resource.add('tinymce.html-i18n.help-keynav.it',\n'<h1>Iniziare la navigazione tramite tastiera</h1>\\n' +\n  '\\n' +\n  '<dl>\\n' +\n  '  <dt>Impostare lo stato attivo per la barra dei menu</dt>\\n' +\n  '  <dd>Windows o Linux: ALT+F9</dd>\\n' +\n  '  <dd>macOS: &#x2325;F9</dd>\\n' +\n  '  <dt>Impostare lo stato attivo per la barra degli strumenti</dt>\\n' +\n  '  <dd>Windows o Linux: ALT+F10</dd>\\n' +\n  '  <dd>macOS: &#x2325;F10</dd>\\n' +\n  '  <dt>Impostare lo stato attivo per il piè di pagina</dt>\\n' +\n  '  <dd>Windows o Linux: ALT+F11</dd>\\n' +\n  '  <dd>macOS: &#x2325;F11</dd>\\n' +\n  '  <dt>Metti a fuoco la notifica</dt>\\n' +\n  '  <dd>Windows o Linux: ALT+F12</dd>\\n' +\n  '  <dd>macOS: &#x2325;F12</dd>\\n' +\n  '  <dt>Impostare lo stato attivo per la barra degli strumenti contestuale</dt>\\n' +\n  '  <dd>Windows, Linux o macOS: CTRL+F9</dd>\\n' +\n  '</dl>\\n' +\n  '\\n' +\n  \"<p>La navigazione inizierà dalla prima voce dell'interfaccia utente, che sarà evidenziata o sottolineata nel caso della prima voce\\n\" +\n  \"  nel percorso dell'elemento del piè di pagina.</p>\\n\" +\n  '\\n' +\n  \"<h1>Navigare tra le sezioni dell'interfaccia utente</h1>\\n\" +\n  '\\n' +\n  \"<p>Per passare da una sezione dell'interfaccia utente alla successiva, premere <strong>TAB</strong>.</p>\\n\" +\n  '\\n' +\n  \"<p>Per passare da una sezione dell'interfaccia utente alla precedente, premere <strong>MAIUSC+TAB</strong>.</p>\\n\" +\n  '\\n' +\n  \"<p>L'ordine di <strong>tabulazione</strong> di queste sezioni dell'interfaccia utente è:</p>\\n\" +\n  '\\n' +\n  '<ol>\\n' +\n  '  <li>Barra dei menu</li>\\n' +\n  '  <li>Ogni gruppo di barre degli strumenti</li>\\n' +\n  '  <li>Barra laterale</li>\\n' +\n  \"  <li>Percorso dell'elemento nel piè di pagina</li>\\n\" +\n  '  <li>Pulsante di attivazione/disattivazione del conteggio delle parole nel piè di pagina</li>\\n' +\n  '  <li>Collegamento al marchio nel piè di pagina</li>\\n' +\n  \"  <li>Quadratino di ridimensionamento dell'editor nel piè di pagina</li>\\n\" +\n  '</ol>\\n' +\n  '\\n' +\n  \"<p>Se una sezione dell'interfaccia utente non è presente, viene saltata.</p>\\n\" +\n  '\\n' +\n  '<p>Se il piè di pagina ha lo stato attivo per la navigazione tramite tastiera e non è presente alcuna barra laterale visibile, premendo <strong>MAIUSC+TAB</strong>\\n' +\n  \"  si sposta lo stato attivo sul primo gruppo di barre degli strumenti, non sull'ultimo.</p>\\n\" +\n  '\\n' +\n  \"<h1>Navigare all'interno delle sezioni dell'interfaccia utente</h1>\\n\" +\n  '\\n' +\n  \"<p>Per passare da un elemento dell'interfaccia utente al successivo, premere il tasto <strong>freccia</strong> appropriato.</p>\\n\" +\n  '\\n' +\n  '<p>I tasti freccia <strong>Sinistra</strong> e <strong>Destra</strong></p>\\n' +\n  '\\n' +\n  '<ul>\\n' +\n  '  <li>consentono di spostarsi tra i menu della barra dei menu.</li>\\n' +\n  '  <li>aprono un sottomenu in un menu.</li>\\n' +\n  '  <li>consentono di spostarsi tra i pulsanti di un gruppo di barre degli strumenti.</li>\\n' +\n  \"  <li>consentono di spostarsi tra le voci nel percorso dell'elemento del piè di pagina.</li>\\n\" +\n  '</ul>\\n' +\n  '\\n' +\n  '<p>I tasti freccia <strong>Giù</strong> e <strong>Su</strong></p>\\n' +\n  '\\n' +\n  '<ul>\\n' +\n  '  <li>consentono di spostarsi tra le voci di un menu.</li>\\n' +\n  '  <li>consentono di spostarsi tra le voci di un menu a comparsa della barra degli strumenti.</li>\\n' +\n  '</ul>\\n' +\n  '\\n' +\n  \"<p>I tasti <strong>freccia</strong> consentono di spostarsi all'interno della sezione dell'interfaccia utente con stato attivo.</p>\\n\" +\n  '\\n' +\n  '<p>Per chiudere un menu aperto, un sottomenu aperto o un menu a comparsa aperto, premere il tasto <strong>ESC</strong>.</p>\\n' +\n  '\\n' +\n  \"<p>Se lo stato attivo corrente si trova nella parte superiore di una particolare sezione dell'interfaccia utente, premendo il tasto <strong>ESC</strong> si esce\\n\" +\n  '  completamente dalla navigazione tramite tastiera.</p>\\n' +\n  '\\n' +\n  '<h1>Eseguire una voce di menu o un pulsante della barra degli strumenti</h1>\\n' +\n  '\\n' +\n  '<p>Quando la voce di menu o il pulsante della barra degli strumenti desiderati sono evidenziati, premere il tasto di<strong>ritorno a capo</strong>, il tasto <strong>Invio</strong>\\n' +\n  '  o la <strong>barra spaziatrice</strong> per eseguirli.</p>\\n' +\n  '\\n' +\n  '<h1>Navigare nelle finestre di dialogo non a schede</h1>\\n' +\n  '\\n' +\n  \"<p>Nelle finestre di dialogo non a schede, all'apertura della finestra di dialogo diventa attivo il primo componente interattivo.</p>\\n\" +\n  '\\n' +\n  '<p>Per spostarsi tra i componenti interattivi della finestra di dialogo, premere <strong>TAB</strong> o <strong>MAIUSC+TAB</strong>.</p>\\n' +\n  '\\n' +\n  '<h1>Navigare nelle finestre di dialogo a schede</h1>\\n' +\n  '\\n' +\n  \"<p>Nelle finestre di dialogo a schede, all'apertura della finestra di dialogo diventa attivo il primo pulsante del menu della scheda.</p>\\n\" +\n  '\\n' +\n  '<p>Per spostarsi tra i componenti interattivi di questa scheda della finestra di dialogo, premere <strong>TAB</strong> o\\n' +\n  '  <strong>MAIUSC+TAB</strong>.</p>\\n' +\n  '\\n' +\n  \"<p>Per passare a un'altra scheda della finestra di dialogo, attivare il menu della scheda e premere il tasto <strong>freccia</strong>\\n\" +\n  '  appropriato per scorrere le schede disponibili.</p>\\n');"
  },
  {
    "path": "apps/web-antd/public/tinymce/plugins/help/js/i18n/keynav/ja.js",
    "content": "tinymce.Resource.add('tinymce.html-i18n.help-keynav.ja',\n'<h1>キーボード ナビゲーションの開始</h1>\\n' +\n  '\\n' +\n  '<dl>\\n' +\n  '  <dt>メニュー バーをフォーカス</dt>\\n' +\n  '  <dd>Windows または Linux: Alt+F9</dd>\\n' +\n  '  <dd>macOS: &#x2325;F9</dd>\\n' +\n  '  <dt>ツール バーをフォーカス</dt>\\n' +\n  '  <dd>Windows または Linux: Alt+F10</dd>\\n' +\n  '  <dd>macOS: &#x2325;F10</dd>\\n' +\n  '  <dt>フッターをフォーカス</dt>\\n' +\n  '  <dd>Windows または Linux: Alt+F11</dd>\\n' +\n  '  <dd>macOS: &#x2325;F11</dd>\\n' +\n  '  <dt>通知にフォーカス</dt>\\n' +\n  '  <dd>Windows または Linux: Alt+F12</dd>\\n' +\n  '  <dd>macOS: &#x2325;F12</dd>\\n' +\n  '  <dt>コンテキスト ツール バーをフォーカス</dt>\\n' +\n  '  <dd>Windows、Linux または macOS: Ctrl+F9</dd>\\n' +\n  '</dl>\\n' +\n  '\\n' +\n  '<p>ナビゲーションは最初の UI 項目から開始され、強調表示されるか、フッターの要素パスにある最初の項目の場合は\\n' +\n  '  下線が引かれます。</p>\\n' +\n  '\\n' +\n  '<h1>UI セクション間の移動</h1>\\n' +\n  '\\n' +\n  '<p>次の UI セクションに移動するには、<strong>Tab</strong> を押します。</p>\\n' +\n  '\\n' +\n  '<p>前の UI セクションに移動するには、<strong>Shift+Tab</strong> を押します。</p>\\n' +\n  '\\n' +\n  '<p>これらの UI セクションの <strong>Tab</strong> の順序:</p>\\n' +\n  '\\n' +\n  '<ol>\\n' +\n  '  <li>メニュー バー</li>\\n' +\n  '  <li>各ツール バー グループ</li>\\n' +\n  '  <li>サイド バー</li>\\n' +\n  '  <li>フッターの要素パス</li>\\n' +\n  '  <li>フッターの単語数切り替えボタン</li>\\n' +\n  '  <li>フッターのブランド リンク</li>\\n' +\n  '  <li>フッターのエディター サイズ変更ハンドル</li>\\n' +\n  '</ol>\\n' +\n  '\\n' +\n  '<p>UI セクションが存在しない場合は、スキップされます。</p>\\n' +\n  '\\n' +\n  '<p>フッターにキーボード ナビゲーション フォーカスがあり、表示可能なサイド バーがない場合、<strong>Shift+Tab</strong> を押すと、\\n' +\n  '  フォーカスが最後ではなく最初のツール バー グループに移動します。</p>\\n' +\n  '\\n' +\n  '<h1>UI セクション内の移動</h1>\\n' +\n  '\\n' +\n  '<p>次の UI 要素に移動するには、適切な<strong>矢印</strong>キーを押します。</p>\\n' +\n  '\\n' +\n  '<p><strong>左矢印</strong>と<strong>右矢印</strong>のキー</p>\\n' +\n  '\\n' +\n  '<ul>\\n' +\n  '  <li>メニュー バーのメニュー間で移動します。</li>\\n' +\n  '  <li>メニュー内のサブメニューを開きます。</li>\\n' +\n  '  <li>ツール バー グループのボタン間で移動します。</li>\\n' +\n  '  <li>フッターの要素パスの項目間で移動します。</li>\\n' +\n  '</ul>\\n' +\n  '\\n' +\n  '<p><strong>下矢印</strong>と<strong>上矢印</strong>のキー</p>\\n' +\n  '\\n' +\n  '<ul>\\n' +\n  '  <li>メニュー内のメニュー項目間で移動します。</li>\\n' +\n  '  <li>ツール バー ポップアップ メニュー内のメニュー項目間で移動します。</li>\\n' +\n  '</ul>\\n' +\n  '\\n' +\n  '<p><strong>矢印</strong>キーで、フォーカスされた UI セクション内で循環します。</p>\\n' +\n  '\\n' +\n  '<p>開いたメニュー、開いたサブメニュー、開いたポップアップ メニューを閉じるには、<strong>Esc</strong> キーを押します。</p>\\n' +\n  '\\n' +\n  '<p>現在のフォーカスが特定の UI セクションの「一番上」にある場合、<strong>Esc</strong> キーを押すと\\n' +\n  '  キーボード ナビゲーションも完全に閉じられます。</p>\\n' +\n  '\\n' +\n  '<h1>メニュー項目またはツール バー ボタンの実行</h1>\\n' +\n  '\\n' +\n  '<p>目的のメニュー項目やツール バー ボタンが強調表示されている場合、<strong>リターン</strong>、<strong>Enter</strong>、\\n' +\n  '  または<strong>スペース キー</strong>を押して項目を実行します。</p>\\n' +\n  '\\n' +\n  '<h1>タブのないダイアログの移動</h1>\\n' +\n  '\\n' +\n  '<p>タブのないダイアログでは、ダイアログが開くと最初の対話型コンポーネントがフォーカスされます。</p>\\n' +\n  '\\n' +\n  '<p><strong>Tab</strong> または <strong>Shift+Tab</strong> を押して、対話型ダイアログ コンポーネント間で移動します。</p>\\n' +\n  '\\n' +\n  '<h1>タブ付きダイアログの移動</h1>\\n' +\n  '\\n' +\n  '<p>タブ付きダイアログでは、ダイアログが開くとタブ メニューの最初のボタンがフォーカスされます。</p>\\n' +\n  '\\n' +\n  '<p><strong>Tab</strong> または\\n' +\n  '  <strong>Shift+Tab</strong> を押して、このダイアログ タブの対話型コンポーネント間で移動します。</p>\\n' +\n  '\\n' +\n  '<p>タブ メニューをフォーカスしてから適切な<strong>矢印</strong>キーを押して表示可能なタブを循環して、\\n' +\n  '  別のダイアログに切り替えます。</p>\\n');"
  },
  {
    "path": "apps/web-antd/public/tinymce/plugins/help/js/i18n/keynav/kk.js",
    "content": "tinymce.Resource.add('tinymce.html-i18n.help-keynav.kk',\n'<h1>Пернетақта навигациясын бастау</h1>\\n' +\n  '\\n' +\n  '<dl>\\n' +\n  '  <dt>Мәзір жолағын фокустау</dt>\\n' +\n  '  <dd>Windows немесе Linux: Alt+F9</dd>\\n' +\n  '  <dd>macOS: &#x2325;F9</dd>\\n' +\n  '  <dt>Құралдар тақтасын фокустау</dt>\\n' +\n  '  <dd>Windows немесе Linux: Alt+F10</dd>\\n' +\n  '  <dd>macOS: &#x2325;F10</dd>\\n' +\n  '  <dt>Төменгі деректемені фокустау</dt>\\n' +\n  '  <dd>Windows немесе Linux: Alt+F11</dd>\\n' +\n  '  <dd>macOS: &#x2325;F11</dd>\\n' +\n  '  <dt>Хабарландыруды белгілеу</dt>\\n' +\n  '  <dd>Windows немесе Linux: Alt+F12</dd>\\n' +\n  '  <dd>macOS: &#x2325;F12</dd>\\n' +\n  '  <dt>Мәтінмәндік құралдар тақтасын фокустау</dt>\\n' +\n  '  <dd>Windows, Linux немесе macOS: Ctrl+F9</dd>\\n' +\n  '</dl>\\n' +\n  '\\n' +\n  '<p>Навигация бөлектелетін немесе Төменгі деректеме элементінің жолындағы бірінші элемент жағдайында асты сызылатын\\n' +\n  '  бірінші ПИ элементінен басталады.</p>\\n' +\n  '\\n' +\n  '<h1>ПИ бөлімдері арасында навигациялау</h1>\\n' +\n  '\\n' +\n  '<p>Бір ПИ бөлімінен келесісіне өту үшін <strong>Tab</strong> пернесін басыңыз.</p>\\n' +\n  '\\n' +\n  '<p>Бір ПИ бөлімінен алдыңғысына өту үшін <strong>Shift+Tab</strong> пернесін басыңыз.</p>\\n' +\n  '\\n' +\n  '<p>Осы ПИ бөлімдерінің <strong>Tab</strong> реті:</p>\\n' +\n  '\\n' +\n  '<ol>\\n' +\n  '  <li>Мәзір жолағы</li>\\n' +\n  '  <li>Әрбір құралдар тақтасы тобы</li>\\n' +\n  '  <li>Бүйірлік жолақ</li>\\n' +\n  '  <li>Төменгі деректемедегі элемент жолы</li>\\n' +\n  '  <li>Төменгі деректемедегі сөздер санын ауыстыру түймесі</li>\\n' +\n  '  <li>Төменгі деректемедегі брендингтік сілтеме</li>\\n' +\n  '  <li>Төменгі деректемедегі редактор өлшемін өзгерту тұтқасы</li>\\n' +\n  '</ol>\\n' +\n  '\\n' +\n  '<p>ПИ бөлімі көрсетілмесе, ол өткізіп жіберіледі.</p>\\n' +\n  '\\n' +\n  '<p>Төменгі деректемеде пернетақта навигациясының фокусы болса және бүйірлік жолақ көрінбесе, <strong>Shift+Tab</strong> тіркесімін басу әрекеті\\n' +\n  '  фокусты соңғысы емес, бірінші құралдар тақтасы тобына жылжытады.</p>\\n' +\n  '\\n' +\n  '<h1>ПИ бөлімдерінде навигациялау</h1>\\n' +\n  '\\n' +\n  '<p>Бір ПИ элементінен келесісіне өту үшін <strong>Arrow</strong> (Көрсеткі) пернесін басыңыз.</p>\\n' +\n  '\\n' +\n  '<p><strong>Left</strong> (Сол жақ) және <strong>Right</strong> (Оң жақ) көрсеткі пернелері</p>\\n' +\n  '\\n' +\n  '<ul>\\n' +\n  '  <li>мәзір жолағындағы мәзірлер арасында жылжыту.</li>\\n' +\n  '  <li>мәзірде ішкі мәзірді ашу.</li>\\n' +\n  '  <li>құралдар тақтасы тобындағы түймелер арасында жылжыту.</li>\\n' +\n  '  <li>төменгі деректеме элементінің жолындағы элементтер арасында жылжыту.</li>\\n' +\n  '</ul>\\n' +\n  '\\n' +\n  '<p><strong>Down</strong> (Төмен) және <strong>Up</strong> (Жоғары) көрсеткі пернелері</p>\\n' +\n  '\\n' +\n  '<ul>\\n' +\n  '  <li>мәзірдегі мәзір элементтері арасында жылжыту.</li>\\n' +\n  '  <li>құралдар тақтасының ашылмалы мәзіріндегі мәзір элементтері арасында жылжыту.</li>\\n' +\n  '</ul>\\n' +\n  '\\n' +\n  '<p>Фокусталған ПИ бөліміндегі <strong>Arrow</strong> (Көрсеткі) пернелерінің циклі.</p>\\n' +\n  '\\n' +\n  '<p>Ашық мәзірді жабу үшін ішкі мәзірді ашып немесе ашылмалы мәзірді ашып, <strong>Esc</strong> пернесін басыңыз.</p>\\n' +\n  '\\n' +\n  '<p>Ағымдағы фокус белгілі бір ПИ бөлімінің «үстінде» болса, <strong>Esc</strong> пернесін басу әрекеті пернетақта\\n' +\n  '  навигациясын толығымен жабады.</p>\\n' +\n  '\\n' +\n  '<h1>Мәзір элементін немесе құралдар тақтасы түймесін орындау</h1>\\n' +\n  '\\n' +\n  '<p>Қажетті мәзір элементі немесе құралдар тақтасы түймесі бөлектелген кезде, элементті орындау үшін <strong>Return</strong> (Қайтару), <strong>Enter</strong> (Енгізу)\\n' +\n  '  немесе <strong>Space bar</strong> (Бос орын) пернесін басыңыз.</p>\\n' +\n  '\\n' +\n  '<h1>Белгіленбеген диалог терезелерін навигациялау</h1>\\n' +\n  '\\n' +\n  '<p>Белгіленбеген диалог терезелерінде диалог терезесі ашылған кезде бірінші интерактивті құрамдас фокусталады.</p>\\n' +\n  '\\n' +\n  '<p><strong>Tab</strong> немесе <strong>Shift+Tab</strong> пернесін басу арқылы интерактивті диалог терезесінің құрамдастары арасында навигациялаңыз.</p>\\n' +\n  '\\n' +\n  '<h1>Белгіленген диалог терезелерін навигациялау</h1>\\n' +\n  '\\n' +\n  '<p>Белгіленген диалог терезелерінде диалог терезесі ашылған кезде қойынды мәзіріндегі бірінші түйме фокусталады.</p>\\n' +\n  '\\n' +\n  '<p><strong>Tab</strong> немесе\\n' +\n  '  <strong>Shift+Tab</strong> пернесін басу арқылы осы диалог терезесі қойындысының интерактивті құрамдастары арасында навигациялаңыз.</p>\\n' +\n  '\\n' +\n  '<p>Қойынды мәзірінің фокусын беру арқылы басқа диалог терезесінің қойындысына ауысып, тиісті <strong>Arrow</strong> (Көрсеткі)\\n' +\n  '  пернесін басу арқылы қолжетімді қойындылар арасында айналдыруға болады.</p>\\n');"
  },
  {
    "path": "apps/web-antd/public/tinymce/plugins/help/js/i18n/keynav/ko_KR.js",
    "content": "tinymce.Resource.add('tinymce.html-i18n.help-keynav.ko_KR',\n'<h1>키보드 탐색 시작</h1>\\n' +\n  '\\n' +\n  '<dl>\\n' +\n  '  <dt>메뉴 모음 포커스 표시</dt>\\n' +\n  '  <dd>Windows 또는 Linux: Alt+F9</dd>\\n' +\n  '  <dd>macOS: &#x2325;F9</dd>\\n' +\n  '  <dt>도구 모음 포커스 표시</dt>\\n' +\n  '  <dd>Windows 또는 Linux: Alt+F10</dd>\\n' +\n  '  <dd>macOS: &#x2325;F10</dd>\\n' +\n  '  <dt>푸터 포커스 표시</dt>\\n' +\n  '  <dd>Windows 또는 Linux: Alt+F11</dd>\\n' +\n  '  <dd>macOS: &#x2325;F11</dd>\\n' +\n  '  <dt>알림 포커스</dt>\\n' +\n  '  <dd>Windows 또는 Linux: Alt+F12</dd>\\n' +\n  '  <dd>macOS: &#x2325;F12</dd>\\n' +\n  '  <dt>컨텍스트 도구 모음에 포커스 표시</dt>\\n' +\n  '  <dd>Windows, Linux 또는 macOS: Ctrl+F9</dd>\\n' +\n  '</dl>\\n' +\n  '\\n' +\n  '<p>첫 번째 UI 항목에서 탐색이 시작되며, 이때 첫 번째 항목이 강조 표시되거나 푸터 요소 경로에 있는\\n' +\n  '  경우 밑줄 표시됩니다.</p>\\n' +\n  '\\n' +\n  '<h1>UI 섹션 간 탐색</h1>\\n' +\n  '\\n' +\n  '<p>한 UI 섹션에서 다음 UI 섹션으로 이동하려면 <strong>Tab(탭)</strong>을 누릅니다.</p>\\n' +\n  '\\n' +\n  '<p>한 UI 섹션에서 이전 UI 섹션으로 돌아가려면 <strong>Shift+Tab(시프트+탭)</strong>을 누릅니다.</p>\\n' +\n  '\\n' +\n  '<p>이 UI 섹션의 <strong>Tab(탭)</strong> 순서는 다음과 같습니다.</p>\\n' +\n  '\\n' +\n  '<ol>\\n' +\n  '  <li>메뉴 바</li>\\n' +\n  '  <li>각 도구 모음 그룹</li>\\n' +\n  '  <li>사이드바</li>\\n' +\n  '  <li>푸터의 요소 경로</li>\\n' +\n  '  <li>푸터의 단어 수 토글 버튼</li>\\n' +\n  '  <li>푸터의 브랜딩 링크</li>\\n' +\n  '  <li>푸터의 에디터 크기 변경 핸들</li>\\n' +\n  '</ol>\\n' +\n  '\\n' +\n  '<p>UI 섹션이 없는 경우 건너뛰기합니다.</p>\\n' +\n  '\\n' +\n  '<p>푸터에 키보드 탐색 포커스가 있고 사이드바는 보이지 않는 경우 <strong>Shift+Tab(시프트+탭)</strong>을 누르면\\n' +\n  '  포커스 표시가 마지막이 아닌 첫 번째 도구 모음 그룹으로 이동합니다.</p>\\n' +\n  '\\n' +\n  '<h1>UI 섹션 내 탐색</h1>\\n' +\n  '\\n' +\n  '<p>한 UI 요소에서 다음 UI 요소로 이동하려면 적절한 <strong>화살표</strong> 키를 누릅니다.</p>\\n' +\n  '\\n' +\n  '<p><strong>왼쪽</strong>과 <strong>오른쪽</strong> 화살표 키의 용도:</p>\\n' +\n  '\\n' +\n  '<ul>\\n' +\n  '  <li>메뉴 모음에서 메뉴 항목 사이를 이동합니다.</li>\\n' +\n  '  <li>메뉴에서 하위 메뉴를 엽니다.</li>\\n' +\n  '  <li>도구 모음 그룹에서 버튼 사이를 이동합니다.</li>\\n' +\n  '  <li>푸터의 요소 경로에서 항목 간에 이동합니다.</li>\\n' +\n  '</ul>\\n' +\n  '\\n' +\n  '<p><strong>아래</strong>와 <strong>위</strong> 화살표 키의 용도:</p>\\n' +\n  '\\n' +\n  '<ul>\\n' +\n  '  <li>메뉴에서 메뉴 항목 사이를 이동합니다.</li>\\n' +\n  '  <li>도구 모음 팝업 메뉴에서 메뉴 항목 사이를 이동합니다.</li>\\n' +\n  '</ul>\\n' +\n  '\\n' +\n  '<p><strong>화살표</strong> 키는 포커스 표시 UI 섹션 내에서 순환됩니다.</p>\\n' +\n  '\\n' +\n  '<p>열려 있는 메뉴, 열려 있는 하위 메뉴 또는 열려 있는 팝업 메뉴를 닫으려면 <strong>Esc</strong> 키를 누릅니다.</p>\\n' +\n  '\\n' +\n  \"<p>현재 포커스 표시가 특정 UI 섹션 '상단'에 있는 경우 이때도 <strong>Esc</strong> 키를 누르면\\n\" +\n  '  키보드 탐색이 완전히 종료됩니다.</p>\\n' +\n  '\\n' +\n  '<h1>메뉴 항목 또는 도구 모음 버튼 실행</h1>\\n' +\n  '\\n' +\n  '<p>원하는 메뉴 항목 또는 도구 모음 버튼이 강조 표시되어 있을 때 <strong>Return(리턴)</strong>, <strong>Enter(엔터)</strong>,\\n' +\n  '  또는 <strong>Space bar(스페이스바)</strong>를 눌러 해당 항목을 실행합니다.</p>\\n' +\n  '\\n' +\n  '<h1>탭이 없는 대화 탐색</h1>\\n' +\n  '\\n' +\n  '<p>탭이 없는 대화의 경우, 첫 번째 대화형 요소가 포커스 표시된 상태로 대화가 열립니다.</p>\\n' +\n  '\\n' +\n  '<p>대화형 요소들 사이를 이동할 때는 <strong>Tab(탭)</strong> 또는 <strong>Shift+Tab(시프트+탭)</strong>을 누릅니다.</p>\\n' +\n  '\\n' +\n  '<h1>탭이 있는 대화 탐색</h1>\\n' +\n  '\\n' +\n  '<p>탭이 있는 대화의 경우, 탭 메뉴에서 첫 번째 버튼이 포커스 표시된 상태로 대화가 열립니다.</p>\\n' +\n  '\\n' +\n  '<p>이 대화 탭의 대화형 요소들 사이를 이동할 때는 <strong>Tab(탭)</strong> 또는\\n' +\n  '  <strong>Shift+Tab(시프트+탭)</strong>을 누릅니다.</p>\\n' +\n  '\\n' +\n  '<p>다른 대화 탭으로 이동하려면 탭 메뉴를 포커스 표시한 다음 적절한 <strong>화살표</strong>\\n' +\n  '  키를 눌러 사용 가능한 탭들을 지나 원하는 탭으로 이동합니다.</p>\\n');"
  },
  {
    "path": "apps/web-antd/public/tinymce/plugins/help/js/i18n/keynav/ms.js",
    "content": "tinymce.Resource.add('tinymce.html-i18n.help-keynav.ms',\n'<h1>Mulakan navigasi papan kekunci</h1>\\n' +\n  '\\n' +\n  '<dl>\\n' +\n  '  <dt>Fokus bar Menu</dt>\\n' +\n  '  <dd>Windows atau Linux: Alt+F9</dd>\\n' +\n  '  <dd>macOS: &#x2325;F9</dd>\\n' +\n  '  <dt>Fokus Bar Alat</dt>\\n' +\n  '  <dd>Windows atau Linux: Alt+F10</dd>\\n' +\n  '  <dd>macOS: &#x2325;F10</dd>\\n' +\n  '  <dt>Fokus pengaki</dt>\\n' +\n  '  <dd>Windows atau Linux: Alt+F11</dd>\\n' +\n  '  <dd>macOS: &#x2325;F11</dd>\\n' +\n  '  <dt>Tumpu kepada pemberitahuan</dt>\\n' +\n  '  <dd>Windows atau Linux: Alt+F12</dd>\\n' +\n  '  <dd>macOS: &#x2325;F12</dd>\\n' +\n  '  <dt>Fokus bar alat kontekstual</dt>\\n' +\n  '  <dd>Windows, Linux atau macOS: Ctrl+F9</dd>\\n' +\n  '</dl>\\n' +\n  '\\n' +\n  '<p>Navigasi akan bermula pada item UI pertama, yang akan diserlahkan atau digaris bawah dalam saiz item pertama dalam\\n' +\n  '  laluan elemen Pengaki.</p>\\n' +\n  '\\n' +\n  '<h1>Navigasi antara bahagian UI</h1>\\n' +\n  '\\n' +\n  '<p>Untuk bergerak dari satu bahagian UI ke yang seterusnya, tekan <strong>Tab</strong>.</p>\\n' +\n  '\\n' +\n  '<p>Untuk bergerak dari satu bahagian UI ke yang sebelumnya, tekan <strong>Shift+Tab</strong>.</p>\\n' +\n  '\\n' +\n  '<p>Tertib <strong>Tab</strong> bahagian UI ini ialah:</p>\\n' +\n  '\\n' +\n  '<ol>\\n' +\n  '  <li>Bar menu</li>\\n' +\n  '  <li>Setiap kumpulan bar alat</li>\\n' +\n  '  <li>Bar sisi</li>\\n' +\n  '  <li>Laluan elemen dalam pengaki</li>\\n' +\n  '  <li>Butang togol kiraan perkataan dalam pengaki</li>\\n' +\n  '  <li>Pautan penjenamaan dalam pengaki</li>\\n' +\n  '  <li>Pemegang saiz semula editor dalam pengaki</li>\\n' +\n  '</ol>\\n' +\n  '\\n' +\n  '<p>Jika bahagian UI tidak wujud, ia dilangkau.</p>\\n' +\n  '\\n' +\n  '<p>Jika pengaki mempunyai fokus navigasi papan kekunci dan tiada bar sisi kelihatan, menekan <strong>Shift+Tab</strong>\\n' +\n  '  akan mengalihkan fokus ke kumpulan bar alat pertama, bukannya yang terakhir.</p>\\n' +\n  '\\n' +\n  '<h1>Navigasi dalam bahagian UI</h1>\\n' +\n  '\\n' +\n  '<p>Untuk bergerak dari satu elemen UI ke yang seterusnya, tekan kekunci <strong>Anak Panah</strong> yang bersesuaian.</p>\\n' +\n  '\\n' +\n  '<p>Kekunci anak panah <strong>Kiri</strong> dan <strong>Kanan</strong></p>\\n' +\n  '\\n' +\n  '<ul>\\n' +\n  '  <li>bergerak antara menu dalam bar menu.</li>\\n' +\n  '  <li>membukan submenu dalam menu.</li>\\n' +\n  '  <li>bergerak antara butang dalam kumpulan bar alat.</li>\\n' +\n  '  <li>Laluan elemen dalam pengaki.</li>\\n' +\n  '</ul>\\n' +\n  '\\n' +\n  '<p>Kekunci anak panah <strong>Bawah</strong> dan <strong>Atas</strong></p>\\n' +\n  '\\n' +\n  '<ul>\\n' +\n  '  <li>bergerak antara item menu dalam menu.</li>\\n' +\n  '  <li>bergerak antara item dalam menu timbul bar alat.</li>\\n' +\n  '</ul>\\n' +\n  '\\n' +\n  '<p>Kekunci <strong>Anak Panah</strong> berkitar dalam bahagian UI difokuskan.</p>\\n' +\n  '\\n' +\n  '<p>Untuk menutup menu buka, submenu terbuka atau menu timbul terbuka, tekan kekunci <strong>Esc</strong>.</p>\\n' +\n  '\\n' +\n  \"<p>Jika fokus semasa berada di bahagian 'atas' bahagian UI tertentu, menekan kekunci <strong>Esc</strong> juga akan keluar daripada\\n\" +\n  '  navigasi papan kekunci sepenuhnya.</p>\\n' +\n  '\\n' +\n  '<h1>Laksanakan item menu atau butang bar alat</h1>\\n' +\n  '\\n' +\n  '<p>Apabila item menu atau butang bar alat yang diinginkan diserlahkan, tekan <strong>Return</strong>, <strong>Enter</strong>,\\n' +\n  '  atau <strong>bar Space</strong> untuk melaksanakan item.</p>\\n' +\n  '\\n' +\n  '<h1>Navigasi ke dialog tidak bertab</h1>\\n' +\n  '\\n' +\n  '<p>Dalam dialog tidak bertab, komponen interaksi pertama difokuskan apabila dialog dibuka.</p>\\n' +\n  '\\n' +\n  '<p>Navigasi antara komponen dialog interaktif dengan menekan <strong>Tab</strong> atau <strong>Shift+Tab</strong>.</p>\\n' +\n  '\\n' +\n  '<h1>Navigasi ke dialog bertab</h1>\\n' +\n  '\\n' +\n  '<p>Dalam dialog bertab, butang pertama dalam menu tab difokuskan apabila dialog dibuka.</p>\\n' +\n  '\\n' +\n  '<p>Navigasi antara komponen interaktif tab dialog ini dengan menekan <strong>Tab</strong> atau\\n' +\n  '  <strong>Shift+Tab</strong>.</p>\\n' +\n  '\\n' +\n  '<p>Tukar kepada tab dialog lain dengan memfokuskan menu tab, kemudian menekan kekunci <strong>Anak Panah</strong> yang bersesuaian\\n' +\n  '  untuk berkitar menerusi tab yang tersedia.</p>\\n');"
  },
  {
    "path": "apps/web-antd/public/tinymce/plugins/help/js/i18n/keynav/nb_NO.js",
    "content": "tinymce.Resource.add('tinymce.html-i18n.help-keynav.nb_NO',\n'<h1>Starte tastaturnavigering</h1>\\n' +\n  '\\n' +\n  '<dl>\\n' +\n  '  <dt>Utheve menylinjen</dt>\\n' +\n  '  <dd>Windows eller Linux: Alt + F9</dd>\\n' +\n  '  <dd>macOS: &#x2325;F9</dd>\\n' +\n  '  <dt>Utheve verktøylinjen</dt>\\n' +\n  '  <dd>Windows eller Linux: Alt + F10</dd>\\n' +\n  '  <dd>macOS: &#x2325;F10</dd>\\n' +\n  '  <dt>Utheve bunnteksten</dt>\\n' +\n  '  <dd>Windows eller Linux: Alt + F11</dd>\\n' +\n  '  <dd>macOS: &#x2325;F11</dd>\\n' +\n  '  <dt>Fokuser på varselet</dt>\\n' +\n  '  <dd>Windows eller Linux: Alt+F12</dd>\\n' +\n  '  <dd>macOS: &#x2325;F12</dd>\\n' +\n  '  <dt>Utheve en kontekstuell verktøylinje</dt>\\n' +\n  '  <dd>Windows, Linux eller macOS: Ctrl + F9</dd>\\n' +\n  '</dl>\\n' +\n  '\\n' +\n  '<p>Navigeringen starter ved det første grensesnittelementet, som utheves, eller understrekes når det gjelder det første elementet i\\n' +\n  '  elementstien i bunnteksten.</p>\\n' +\n  '\\n' +\n  '<h1>Navigere mellom grensesnittdeler</h1>\\n' +\n  '\\n' +\n  '<p>Du kan bevege deg fra én grensesnittdel til den neste ved å trykke på <strong>tabulatortasten</strong>.</p>\\n' +\n  '\\n' +\n  '<p>Du kan bevege deg fra én grensesnittdel til den forrige ved å trykke på <strong>Shift + tabulatortasten</strong>.</p>\\n' +\n  '\\n' +\n  '<p>Rekkefølgen til <strong>tabulatortasten</strong> gjennom grensesnittdelene er:</p>\\n' +\n  '\\n' +\n  '<ol>\\n' +\n  '  <li>Menylinjen</li>\\n' +\n  '  <li>Hver gruppe på verktøylinjen</li>\\n' +\n  '  <li>Sidestolpen</li>\\n' +\n  '  <li>Elementstien i bunnteksten</li>\\n' +\n  '  <li>Veksleknappen for ordantall i bunnteksten</li>\\n' +\n  '  <li>Merkelenken i bunnteksten</li>\\n' +\n  '  <li>Skaleringshåndtaket for redigeringsprogrammet i bunnteksten</li>\\n' +\n  '</ol>\\n' +\n  '\\n' +\n  '<p>Hvis en grensesnittdel ikke er til stede, blir den hoppet over.</p>\\n' +\n  '\\n' +\n  '<p>Hvis tastaturnavigeringen har uthevet bunnteksten og det ikke finnes en synlig sidestolpe, kan du trykke på <strong>Shift + tabulatortasten</strong>\\n' +\n  '  for å flytte fokuset til den første gruppen på verktøylinjen i stedet for den siste.</p>\\n' +\n  '\\n' +\n  '<h1>Navigere innenfor grensesnittdeler</h1>\\n' +\n  '\\n' +\n  '<p>Du kan bevege deg fra ett grensesnittelement til det neste ved å trykke på den aktuelle <strong>piltasten</strong>.</p>\\n' +\n  '\\n' +\n  '<p>De <strong>venstre</strong> og <strong>høyre</strong> piltastene</p>\\n' +\n  '\\n' +\n  '<ul>\\n' +\n  '  <li>beveger deg mellom menyer på menylinjen.</li>\\n' +\n  '  <li>åpner en undermeny i en meny.</li>\\n' +\n  '  <li>beveger deg mellom knapper i en gruppe på verktøylinjen.</li>\\n' +\n  '  <li>beveger deg mellom elementer i elementstien i bunnteksten.</li>\\n' +\n  '</ul>\\n' +\n  '\\n' +\n  '<p><strong>Ned</strong>- og <strong>opp</strong>-piltastene</p>\\n' +\n  '\\n' +\n  '<ul>\\n' +\n  '  <li>beveger deg mellom menyelementer i en meny.</li>\\n' +\n  '  <li>beveger deg mellom elementer i en hurtigmeny på verktøylinjen.</li>\\n' +\n  '</ul>\\n' +\n  '\\n' +\n  '<p>Med <strong>piltastene</strong> kan du bevege deg innenfor den uthevede grensesnittdelen.</p>\\n' +\n  '\\n' +\n  '<p>Du kan lukke en åpen meny, en åpen undermeny eller en åpen hurtigmeny ved å klikke på <strong>Esc</strong>-tasten.</p>\\n' +\n  '\\n' +\n  '<p>Hvis det øverste nivået i en grensesnittdel er uthevet, kan du ved å trykke på <strong>Esc</strong> også avslutte\\n' +\n  '  tastaturnavigeringen helt.</p>\\n' +\n  '\\n' +\n  '<h1>Utføre et menyelement eller en knapp på en verktøylinje</h1>\\n' +\n  '\\n' +\n  '<p>Når det ønskede menyelementet eller verktøylinjeknappen er uthevet, trykker du på <strong>Retur</strong>, <strong>Enter</strong>,\\n' +\n  '  eller <strong>mellomromstasten</strong> for å utføre elementet.</p>\\n' +\n  '\\n' +\n  '<h1>Navigere i dialogbokser uten faner</h1>\\n' +\n  '\\n' +\n  '<p>I dialogbokser uten faner blir den første interaktive komponenten uthevet når dialogboksen åpnes.</p>\\n' +\n  '\\n' +\n  '<p>Naviger mellom interaktive komponenter i dialogboksen ved å trykke på <strong>tabulatortasten</strong> eller <strong>Shift + tabulatortasten</strong>.</p>\\n' +\n  '\\n' +\n  '<h1>Navigere i fanebaserte dialogbokser</h1>\\n' +\n  '\\n' +\n  '<p>I fanebaserte dialogbokser blir den første knappen i fanemenyen uthevet når dialogboksen åpnes.</p>\\n' +\n  '\\n' +\n  '<p>Naviger mellom interaktive komponenter i fanen ved å trykke på <strong>tabulatortasten</strong> eller\\n' +\n  '  <strong>Shift + tabulatortasten</strong>.</p>\\n' +\n  '\\n' +\n  '<p>Veksle til en annen fane i dialogboksen ved å utheve fanemenyen, og trykk deretter på den aktuelle <strong>piltasten</strong>\\n' +\n  '  for å bevege deg mellom de tilgjengelige fanene.</p>\\n');"
  },
  {
    "path": "apps/web-antd/public/tinymce/plugins/help/js/i18n/keynav/nl.js",
    "content": "tinymce.Resource.add('tinymce.html-i18n.help-keynav.nl',\n'<h1>Toetsenbordnavigatie starten</h1>\\n' +\n  '\\n' +\n  '<dl>\\n' +\n  '  <dt>Focus op de menubalk instellen</dt>\\n' +\n  '  <dd>Windows of Linux: Alt+F9</dd>\\n' +\n  '  <dd>macOS: &#x2325;F9</dd>\\n' +\n  '  <dt>Focus op de werkbalk instellen</dt>\\n' +\n  '  <dd>Windows of Linux: Alt+F10</dd>\\n' +\n  '  <dd>macOS: &#x2325;F10</dd>\\n' +\n  '  <dt>Focus op de voettekst instellen</dt>\\n' +\n  '  <dd>Windows of Linux: Alt+F11</dd>\\n' +\n  '  <dd>macOS: &#x2325;F11</dd>\\n' +\n  '  <dt>Focus op de melding instellen</dt>\\n' +\n  '  <dd>Windows of Linux: Alt+F12</dd>\\n' +\n  '  <dd>macOS: &#x2325;F12</dd>\\n' +\n  '  <dt>Focus op een contextuele werkbalk instellen</dt>\\n' +\n  '  <dd>Windows, Linux of macOS: Ctrl+F9</dd>\\n' +\n  '</dl>\\n' +\n  '\\n' +\n  '<p>De navigatie start bij het eerste UI-item, dat wordt gemarkeerd of onderstreept als het eerste item zich in\\n' +\n  '  in het elementenpad van de voettekst bevindt.</p>\\n' +\n  '\\n' +\n  '<h1>Navigeren tussen UI-secties</h1>\\n' +\n  '\\n' +\n  '<p>Druk op <strong>Tab</strong> om naar de volgende UI-sectie te gaan.</p>\\n' +\n  '\\n' +\n  '<p>Druk op <strong>Shift+Tab</strong> om naar de vorige UI-sectie te gaan.</p>\\n' +\n  '\\n' +\n  '<p>De <strong>Tab</strong>-volgorde van deze UI-secties is:</p>\\n' +\n  '\\n' +\n  '<ol>\\n' +\n  '  <li>Menubalk</li>\\n' +\n  '  <li>Elke werkbalkgroep</li>\\n' +\n  '  <li>Zijbalk</li>\\n' +\n  '  <li>Elementenpad in de voettekst</li>\\n' +\n  '  <li>Wisselknop voor aantal woorden in de voettekst</li>\\n' +\n  '  <li>Merkkoppeling in de voettekst</li>\\n' +\n  '  <li>Greep voor het wijzigen van het formaat van de editor in de voettekst</li>\\n' +\n  '</ol>\\n' +\n  '\\n' +\n  '<p>Als een UI-sectie niet aanwezig is, wordt deze overgeslagen.</p>\\n' +\n  '\\n' +\n  '<p>Als de focus van de toetsenbordnavigatie is ingesteld op de voettekst en er geen zichtbare zijbalk is, kun je op <strong>Shift+Tab</strong> drukken\\n' +\n  '  om de focus naar de eerste werkbalkgroep in plaats van de laatste te verplaatsen.</p>\\n' +\n  '\\n' +\n  '<h1>Navigeren binnen UI-secties</h1>\\n' +\n  '\\n' +\n  '<p>Druk op de <strong>pijltjestoets</strong> om naar het betreffende UI-element te gaan.</p>\\n' +\n  '\\n' +\n  '<p>Met de pijltjestoetsen <strong>Links</strong> en <strong>Rechts</strong></p>\\n' +\n  '\\n' +\n  '<ul>\\n' +\n  \"  <li>wissel je tussen menu's in de menubalk.</li>\\n\" +\n  '  <li>open je een submenu in een menu.</li>\\n' +\n  '  <li>wissel je tussen knoppen in een werkbalkgroep.</li>\\n' +\n  '  <li>wissel je tussen items in het elementenpad in de voettekst.</li>\\n' +\n  '</ul>\\n' +\n  '\\n' +\n  '<p>Met de pijltjestoetsen <strong>Omlaag</strong> en <strong>Omhoog</strong></p>\\n' +\n  '\\n' +\n  '<ul>\\n' +\n  '  <li>wissel je tussen menu-items in een menu.</li>\\n' +\n  '  <li>wissel je tussen items in een werkbalkpop-upmenu.</li>\\n' +\n  '</ul>\\n' +\n  '\\n' +\n  '<p>Met de <strong>pijltjestoetsen</strong> wissel je binnen de UI-sectie waarop de focus is ingesteld.</p>\\n' +\n  '\\n' +\n  '<p>Druk op de toets <strong>Esc</strong> om een geopend menu, submenu of pop-upmenu te sluiten.</p>\\n' +\n  '\\n' +\n  \"<p>Als de huidige focus is ingesteld 'bovenaan' een bepaalde UI-sectie, kun je op de toets <strong>Esc</strong> drukken\\n\" +\n  '  om de toetsenbordnavigatie af te sluiten.</p>\\n' +\n  '\\n' +\n  '<h1>Een menu-item of werkbalkknop uitvoeren</h1>\\n' +\n  '\\n' +\n  '<p>Als het gewenste menu-item of de gewenste werkbalkknop is gemarkeerd, kun je op <strong>Return</strong>, <strong>Enter</strong>\\n' +\n  '  of de <strong>spatiebalk</strong> drukken om het item uit te voeren.</p>\\n' +\n  '\\n' +\n  '<h1>Navigeren in dialoogvensters zonder tabblad</h1>\\n' +\n  '\\n' +\n  '<p>Als een dialoogvenster zonder tabblad wordt geopend, wordt de focus ingesteld op het eerste interactieve onderdeel.</p>\\n' +\n  '\\n' +\n  '<p>Je kunt navigeren tussen interactieve onderdelen van een dialoogvenster door op <strong>Tab</strong> of <strong>Shift+Tab</strong> te drukken.</p>\\n' +\n  '\\n' +\n  '<h1>Navigeren in dialoogvensters met tabblad</h1>\\n' +\n  '\\n' +\n  '<p>Als een dialoogvenster met tabblad wordt geopend, wordt de focus ingesteld op de eerste knop in het tabbladmenu.</p>\\n' +\n  '\\n' +\n  '<p>Je kunt navigeren tussen interactieve onderdelen van dit tabblad van het dialoogvenster door op <strong>Tab</strong> of\\n' +\n  '  <strong>Shift+Tab</strong> te drukken.</p>\\n' +\n  '\\n' +\n  '<p>Je kunt overschakelen naar een ander tabblad van het dialoogvenster door de focus in te stellen op het tabbladmenu en vervolgens op de juiste <strong>pijltjestoets</strong>\\n' +\n  '  te drukken om tussen de beschikbare tabbladen te wisselen.</p>\\n');"
  },
  {
    "path": "apps/web-antd/public/tinymce/plugins/help/js/i18n/keynav/pl.js",
    "content": "tinymce.Resource.add('tinymce.html-i18n.help-keynav.pl',\n'<h1>Początek nawigacji przy użyciu klawiatury</h1>\\n' +\n  '\\n' +\n  '<dl>\\n' +\n  '  <dt>Ustaw fokus na pasek menu</dt>\\n' +\n  '  <dd>Windows lub Linux: Alt+F9</dd>\\n' +\n  '  <dd>macOS: &#x2325;F9</dd>\\n' +\n  '  <dt>Ustaw fokus na pasek narzędzi</dt>\\n' +\n  '  <dd>Windows lub Linux: Alt+F10</dd>\\n' +\n  '  <dd>macOS: &#x2325;F10</dd>\\n' +\n  '  <dt>Ustaw fokus na sekcję Footer</dt>\\n' +\n  '  <dd>Windows lub Linux: Alt+F11</dd>\\n' +\n  '  <dd>macOS: &#x2325;F11</dd>\\n' +\n  '  <dt>Skup się na powiadomieniu</dt>\\n' +\n  '  <dd>Windows lub Linux: Alt+F12</dd>\\n' +\n  '  <dd>macOS: &#x2325;F12</dd>\\n' +\n  '  <dt>Ustaw fokus na kontekstowy pasek narzędzi</dt>\\n' +\n  '  <dd>Windows, Linux lub macOS: Ctrl+F9</dd>\\n' +\n  '</dl>\\n' +\n  '\\n' +\n  '<p>Nawigacja zostanie rozpoczęta od pierwszego elementu interfejsu użytkownika, który jest podświetlony lub — w przypadku pierwszego elementu\\n' +\n  '  w ścieżce elementów w sekcji Footer — podkreślony.</p>\\n' +\n  '\\n' +\n  '<h1>Nawigacja pomiędzy sekcjami interfejsu użytkownika</h1>\\n' +\n  '\\n' +\n  '<p>Aby przenieść się z danej sekcji interfejsu użytkownika do następnej, naciśnij <strong>Tab</strong>.</p>\\n' +\n  '\\n' +\n  '<p>Aby przenieść się z danej sekcji interfejsu użytkownika do poprzedniej, naciśnij <strong>Shift+Tab</strong>.</p>\\n' +\n  '\\n' +\n  '<p>Kolejność klawisza <strong>Tab</strong> w takich sekcjach interfejsu użytkownika jest następująca:</p>\\n' +\n  '\\n' +\n  '<ol>\\n' +\n  '  <li>Pasek menu</li>\\n' +\n  '  <li>Każda grupa na pasku narzędzi</li>\\n' +\n  '  <li>Pasek boczny</li>\\n' +\n  '  <li>Ścieżka elementów w sekcji Footer</li>\\n' +\n  '  <li>Przycisk przełączania liczby słów w sekcji Footer</li>\\n' +\n  '  <li>Łącze brandujące w sekcji Footer</li>\\n' +\n  '  <li>Uchwyt zmiany rozmiaru edytora w sekcji Footer</li>\\n' +\n  '</ol>\\n' +\n  '\\n' +\n  '<p>Jeżeli nie ma sekcji interfejsu użytkownika, jest to pomijane.</p>\\n' +\n  '\\n' +\n  '<p>Jeżeli na sekcji Footer jest ustawiony fokus nawigacji przy użyciu klawiatury i nie ma widocznego paska bocznego, naciśnięcie <strong>Shift+Tab</strong>\\n' +\n  '  przenosi fokus na pierwszą grupę paska narzędzi, a nie na ostatnią.</p>\\n' +\n  '\\n' +\n  '<h1>Nawigacja wewnątrz sekcji interfejsu użytkownika</h1>\\n' +\n  '\\n' +\n  '<p>Aby przenieść się z danego elementu interfejsu użytkownika do następnego, naciśnij odpowiedni klawisz <strong>strzałki</strong>.</p>\\n' +\n  '\\n' +\n  '<p>Klawisze strzałek <strong>w prawo</strong> i <strong>w lewo</strong> służą do</p>\\n' +\n  '\\n' +\n  '<ul>\\n' +\n  '  <li>przenoszenia się pomiędzy menu na pasku menu,</li>\\n' +\n  '  <li>otwarcia podmenu w menu,</li>\\n' +\n  '  <li>przenoszenia się pomiędzy przyciskami w grupie paska narzędzi,</li>\\n' +\n  '  <li>przenoszenia się pomiędzy elementami w ścieżce elementów w sekcji Footer.</li>\\n' +\n  '</ul>\\n' +\n  '\\n' +\n  '<p>Klawisze strzałek <strong>w dół</strong> i <strong>w górę</strong> służą do</p>\\n' +\n  '\\n' +\n  '<ul>\\n' +\n  '  <li>przenoszenia się pomiędzy elementami menu w menu,</li>\\n' +\n  '  <li>przenoszenia się pomiędzy elementami w wyskakującym menu paska narzędzi.</li>\\n' +\n  '</ul>\\n' +\n  '\\n' +\n  '<p>Klawisze <strong>strzałek</strong> służą do przemieszczania się w sekcji interfejsu użytkownika z ustawionym fokusem.</p>\\n' +\n  '\\n' +\n  '<p>Aby zamknąć otwarte menu, otwarte podmenu lub otwarte menu wyskakujące, naciśnij klawisz <strong>Esc</strong>.</p>\\n' +\n  '\\n' +\n  '<p>Jeżeli fokus jest ustawiony na górze konkretnej sekcji interfejsu użytkownika, naciśnięcie klawisza <strong>Esc</strong> powoduje wyjście\\n' +\n  '  z nawigacji przy użyciu klawiatury.</p>\\n' +\n  '\\n' +\n  '<h1>Wykonanie elementu menu lub przycisku paska narzędzi</h1>\\n' +\n  '\\n' +\n  '<p>Gdy podświetlony jest żądany element menu lub przycisk paska narzędzi, naciśnij klawisz <strong>Return</strong>, <strong>Enter</strong>\\n' +\n  '  lub <strong>Spacja</strong>, aby go wykonać.</p>\\n' +\n  '\\n' +\n  '<h1>Nawigacja po oknie dialogowym bez kart</h1>\\n' +\n  '\\n' +\n  '<p>Gdy otwiera się okno dialogowe bez kart, fokus ustawiany jest na pierwszą interaktywną część okna.</p>\\n' +\n  '\\n' +\n  '<p>Pomiędzy interaktywnymi częściami okna dialogowego nawiguj, naciskając klawisze <strong>Tab</strong> lub <strong>Shift+Tab</strong>.</p>\\n' +\n  '\\n' +\n  '<h1>Nawigacja po oknie dialogowym z kartami</h1>\\n' +\n  '\\n' +\n  '<p>W przypadku okna dialogowego z kartami po otwarciu okna dialogowego fokus ustawiany jest na pierwszy przycisk w menu karty.</p>\\n' +\n  '\\n' +\n  '<p>Nawigację pomiędzy interaktywnymi częściami karty okna dialogowego prowadzi się poprzez naciskanie klawiszy <strong>Tab</strong> lub\\n' +\n  '  <strong>Shift+Tab</strong>.</p>\\n' +\n  '\\n' +\n  '<p>Przełączenie się na inną kartę okna dialogowego wykonuje się poprzez ustawienie fokusu na menu karty i naciśnięcie odpowiedniego klawisza <strong>strzałki</strong>\\n' +\n  '  w celu przemieszczenia się pomiędzy dostępnymi kartami.</p>\\n');"
  },
  {
    "path": "apps/web-antd/public/tinymce/plugins/help/js/i18n/keynav/pt_BR.js",
    "content": "tinymce.Resource.add('tinymce.html-i18n.help-keynav.pt_BR',\n'<h1>Iniciar navegação pelo teclado</h1>\\n' +\n  '\\n' +\n  '<dl>\\n' +\n  '  <dt>Foco na barra de menus</dt>\\n' +\n  '  <dd>Windows ou Linux: Alt+F9</dd>\\n' +\n  '  <dd>macOS: &#x2325;F9</dd>\\n' +\n  '  <dt>Foco na barra de ferramentas</dt>\\n' +\n  '  <dd>Windows ou Linux: Alt+F10</dd>\\n' +\n  '  <dd>macOS: &#x2325;F10</dd>\\n' +\n  '  <dt>Foco no rodapé</dt>\\n' +\n  '  <dd>Windows ou Linux: Alt+F11</dd>\\n' +\n  '  <dd>macOS: &#x2325;F11</dd>\\n' +\n  '  <dt>Foco na notificação</dt>\\n' +\n  '  <dd>Windows ou Linux: Alt+F12</dd>\\n' +\n  '  <dd>macOS: &#x2325;F12</dd>\\n' +\n  '  <dt>Foco na barra de ferramentas contextual</dt>\\n' +\n  '  <dd>Windows, Linux ou macOS: Ctrl+F9</dd>\\n' +\n  '</dl>\\n' +\n  '\\n' +\n  '<p>A navegação inicia no primeiro item da IU, que será destacado ou sublinhado no caso do primeiro item no\\n' +\n  '  caminho do elemento Rodapé.</p>\\n' +\n  '\\n' +\n  '<h1>Navegar entre seções da IU</h1>\\n' +\n  '\\n' +\n  '<p>Para ir de uma seção da IU para a seguinte, pressione <strong>Tab</strong>.</p>\\n' +\n  '\\n' +\n  '<p>Para ir de uma seção da IU para a anterior, pressione <strong>Shift+Tab</strong>.</p>\\n' +\n  '\\n' +\n  '<p>A ordem de <strong>Tab</strong> destas seções da IU é:</p>\\n' +\n  '\\n' +\n  '<ol>\\n' +\n  '  <li>Barra de menus</li>\\n' +\n  '  <li>Cada grupo da barra de ferramentas</li>\\n' +\n  '  <li>Barra lateral</li>\\n' +\n  '  <li>Caminho do elemento no rodapé</li>\\n' +\n  '  <li>Botão de alternar contagem de palavras no rodapé</li>\\n' +\n  '  <li>Link da marca no rodapé</li>\\n' +\n  '  <li>Alça de redimensionamento do editor no rodapé</li>\\n' +\n  '</ol>\\n' +\n  '\\n' +\n  '<p>Se não houver uma seção da IU, ela será pulada.</p>\\n' +\n  '\\n' +\n  '<p>Se o rodapé tiver o foco da navegação pelo teclado e não houver uma barra lateral visível, pressionar <strong>Shift+Tab</strong>\\n' +\n  '  move o foco para o primeiro grupo da barra de ferramentas, não para o último.</p>\\n' +\n  '\\n' +\n  '<h1>Navegar dentro das seções da IU</h1>\\n' +\n  '\\n' +\n  '<p>Para ir de um elemento da IU para o seguinte, pressione a <strong>Seta</strong> correspondente.</p>\\n' +\n  '\\n' +\n  '<p>As teclas de seta <strong>Esquerda</strong> e <strong>Direita</strong></p>\\n' +\n  '\\n' +\n  '<ul>\\n' +\n  '  <li>movem entre menus na barra de menus.</li>\\n' +\n  '  <li>abrem um submenu em um menu.</li>\\n' +\n  '  <li>movem entre botões em um grupo da barra de ferramentas.</li>\\n' +\n  '  <li>movem entre itens no caminho do elemento do rodapé.</li>\\n' +\n  '</ul>\\n' +\n  '\\n' +\n  '<p>As teclas de seta <strong>Abaixo</strong> e <strong>Acima</strong></p>\\n' +\n  '\\n' +\n  '<ul>\\n' +\n  '  <li>movem entre itens de menu em um menu.</li>\\n' +\n  '  <li>movem entre itens em um menu suspenso da barra de ferramentas.</li>\\n' +\n  '</ul>\\n' +\n  '\\n' +\n  '<p>As teclas de <strong>Seta</strong> alternam dentre a seção da IU em foco.</p>\\n' +\n  '\\n' +\n  '<p>Para fechar um menu aberto, um submenu aberto ou um menu suspenso aberto, pressione <strong>Esc</strong>.</p>\\n' +\n  '\\n' +\n  '<p>Se o foco atual estiver no ‘alto’ de determinada seção da IU, pressionar <strong>Esc</strong> também sai\\n' +\n  '  totalmente da navegação pelo teclado.</p>\\n' +\n  '\\n' +\n  '<h1>Executar um item de menu ou botão da barra de ferramentas</h1>\\n' +\n  '\\n' +\n  '<p>Com o item de menu ou botão da barra de ferramentas desejado destacado, pressione <strong>Return</strong>, <strong>Enter</strong>,\\n' +\n  '  ou a <strong>Barra de espaço</strong> para executar o item.</p>\\n' +\n  '\\n' +\n  '<h1>Navegar por caixas de diálogo sem guias</h1>\\n' +\n  '\\n' +\n  '<p>Em caixas de diálogo sem guias, o primeiro componente interativo recebe o foco quando a caixa de diálogo abre.</p>\\n' +\n  '\\n' +\n  '<p>Navegue entre componentes interativos de caixa de diálogo pressionando <strong>Tab</strong> ou <strong>Shift+Tab</strong>.</p>\\n' +\n  '\\n' +\n  '<h1>Navegar por caixas de diálogo com guias</h1>\\n' +\n  '\\n' +\n  '<p>Em caixas de diálogo com guias, o primeiro botão no menu da guia recebe o foco quando a caixa de diálogo abre.</p>\\n' +\n  '\\n' +\n  '<p>Navegue entre componentes interativos dessa guia da caixa de diálogo pressionando <strong>Tab</strong> ou\\n' +\n  '  <strong>Shift+Tab</strong>.</p>\\n' +\n  '\\n' +\n  '<p>Alterne para outra guia da caixa de diálogo colocando o foco no menu da guia e pressionando a <strong>Seta</strong>\\n' +\n  '  adequada para percorrer as guias disponíveis.</p>\\n');"
  },
  {
    "path": "apps/web-antd/public/tinymce/plugins/help/js/i18n/keynav/pt_PT.js",
    "content": "tinymce.Resource.add('tinymce.html-i18n.help-keynav.pt_PT',\n'<h1>Iniciar navegação com teclado</h1>\\n' +\n  '\\n' +\n  '<dl>\\n' +\n  '  <dt>Foco na barra de menu</dt>\\n' +\n  '  <dd>Windows ou Linux: Alt+F9</dd>\\n' +\n  '  <dd>macOS: &#x2325;F9</dd>\\n' +\n  '  <dt>Foco na barra de ferramentas</dt>\\n' +\n  '  <dd>Windows ou Linux: Alt+F10</dd>\\n' +\n  '  <dd>macOS: &#x2325;F10</dd>\\n' +\n  '  <dt>Foco no rodapé</dt>\\n' +\n  '  <dd>Windows ou Linux: Alt+F11</dd>\\n' +\n  '  <dd>macOS: &#x2325;F11</dd>\\n' +\n  '  <dt>Focar a notificação</dt>\\n' +\n  '  <dd>Windows ou Linux: Alt+F12</dd>\\n' +\n  '  <dd>macOS: &#x2325;F12</dd>\\n' +\n  '  <dt>Foco numa barra de ferramentas contextual</dt>\\n' +\n  '  <dd>Windows, Linux ou macOS: Ctrl+F9</dd>\\n' +\n  '</dl>\\n' +\n  '\\n' +\n  '<p>A navegação começará no primeiro item de IU, que estará realçado ou sublinhado, no caso do primeiro item no\\n' +\n  '  caminho do elemento do rodapé.</p>\\n' +\n  '\\n' +\n  '<h1>Navegar entre secções de IU</h1>\\n' +\n  '\\n' +\n  '<p>Para se mover de uma secção de IU para a seguinte, prima <strong>Tab</strong>.</p>\\n' +\n  '\\n' +\n  '<p>Para se mover de uma secção de IU para a anterior, prima <strong>Shift+Tab</strong>.</p>\\n' +\n  '\\n' +\n  '<p>A ordem de <strong>tabulação</strong> destas secções de IU é:</p>\\n' +\n  '\\n' +\n  '<ol>\\n' +\n  '  <li>Barra de menu</li>\\n' +\n  '  <li>Cada grupo da barra de ferramentas</li>\\n' +\n  '  <li>Barra lateral</li>\\n' +\n  '  <li>Caminho do elemento no rodapé</li>\\n' +\n  '  <li>Botão de alternar da contagem de palavras no rodapé</li>\\n' +\n  '  <li>Ligação da marca no rodapé</li>\\n' +\n  '  <li>Alça de redimensionamento do editor no rodapé</li>\\n' +\n  '</ol>\\n' +\n  '\\n' +\n  '<p>Se uma secção de IU não estiver presente, é ignorada.</p>\\n' +\n  '\\n' +\n  '<p>Se o rodapé tiver foco de navegação com teclado e não existir uma barra lateral visível, premir <strong>Shift+Tab</strong>\\n' +\n  '  move o foco para o primeiro grupo da barra de ferramentas e não para o último.</p>\\n' +\n  '\\n' +\n  '<h1>Navegar nas secções de IU</h1>\\n' +\n  '\\n' +\n  '<p>Para se mover de um elemento de IU para o seguinte, prima a tecla de <strong>seta</strong> adequada.</p>\\n' +\n  '\\n' +\n  '<p>As teclas de seta <strong>Para a esquerda</strong> e <strong>Para a direita</strong></p>\\n' +\n  '\\n' +\n  '<ul>\\n' +\n  '  <li>movem-se entre menus na barra de menu.</li>\\n' +\n  '  <li>abrem um submenu num menu.</li>\\n' +\n  '  <li>movem-se entre botões num grupo da barra de ferramentas.</li>\\n' +\n  '  <li>movem-se entre itens no caminho do elemento do rodapé.</li>\\n' +\n  '</ul>\\n' +\n  '\\n' +\n  '<p>As teclas de seta <strong>Para cima</strong> e <strong>Para baixo</strong></p>\\n' +\n  '\\n' +\n  '<ul>\\n' +\n  '  <li>movem-se entre itens de menu num menu.</li>\\n' +\n  '  <li>movem-se entre itens num menu de pop-up da barra de ferramentas.</li>\\n' +\n  '</ul>\\n' +\n  '\\n' +\n  '<p>As teclas de <strong>seta</strong> deslocam-se ciclicamente na secção de IU em foco.</p>\\n' +\n  '\\n' +\n  '<p>Para fechar um menu aberto, um submenu aberto ou um menu de pop-up aberto, prima a tecla <strong>Esc</strong>.</p>\\n' +\n  '\\n' +\n  '<p>Se o foco atual estiver no \"topo\" de determinada secção de IU, premir a tecla <strong>Esc</strong> também fecha\\n' +\n  '  completamente a navegação com teclado.</p>\\n' +\n  '\\n' +\n  '<h1>Executar um item de menu ou botão da barra de ferramentas</h1>\\n' +\n  '\\n' +\n  '<p>Quando o item de menu ou o botão da barra de ferramentas pretendido estiver realçado, prima <strong>Retrocesso</strong>, <strong>Enter</strong>\\n' +\n  '  ou a <strong>Barra de espaço</strong> para executar o item.</p>\\n' +\n  '\\n' +\n  '<h1>Navegar em diálogos sem separadores</h1>\\n' +\n  '\\n' +\n  '<p>Nos diálogos sem separadores, o primeiro componente interativo fica em foco quando o diálogo abre.</p>\\n' +\n  '\\n' +\n  '<p>Navegue entre componentes interativos do diálogo, premindo <strong>Tab</strong> ou <strong>Shift+Tab</strong>.</p>\\n' +\n  '\\n' +\n  '<h1>Navegar em diálogos com separadores</h1>\\n' +\n  '\\n' +\n  '<p>Nos diálogos com separadores, o primeiro botão no menu do separador fica em foco quando o diálogo abre.</p>\\n' +\n  '\\n' +\n  '<p>Navegue entre os componentes interativos deste separador do diálogo, premindo <strong>Tab</strong> ou\\n' +\n  '  <strong>Shift+Tab</strong>.</p>\\n' +\n  '\\n' +\n  '<p>Mude para outro separador do diálogo colocando o menu do separador em foco e, em seguida, premindo a tecla de <strong>seta</strong>\\n' +\n  '  adequada para se deslocar ciclicamente pelos separadores disponíveis.</p>\\n');"
  },
  {
    "path": "apps/web-antd/public/tinymce/plugins/help/js/i18n/keynav/ro.js",
    "content": "tinymce.Resource.add('tinymce.html-i18n.help-keynav.ro',\n'<h1>Începeți navigarea de la tastatură</h1>\\n' +\n  '\\n' +\n  '<dl>\\n' +\n  '  <dt>Focalizare pe bara de meniu</dt>\\n' +\n  '  <dd>Windows sau Linux: Alt+F9</dd>\\n' +\n  '  <dd>macOS: &#x2325;F9</dd>\\n' +\n  '  <dt>Focalizare pe bara de instrumente</dt>\\n' +\n  '  <dd>Windows sau Linux: Alt+F10</dd>\\n' +\n  '  <dd>macOS: &#x2325;F10</dd>\\n' +\n  '  <dt>Focalizare pe subsol</dt>\\n' +\n  '  <dd>Windows sau Linux: Alt+F11</dd>\\n' +\n  '  <dd>macOS: &#x2325;F11</dd>\\n' +\n  '  <dt>Focalizare pe notificare</dt>\\n' +\n  '  <dd>Windows sau Linux: Alt+F12</dd>\\n' +\n  '  <dd>macOS: &#x2325;F12</dd>\\n' +\n  '  <dt>Focalizare pe o bară de instrumente contextuală</dt>\\n' +\n  '  <dd>Windows, Linux sau macOS: Ctrl+F9</dd>\\n' +\n  '</dl>\\n' +\n  '\\n' +\n  '<p>Navigarea va începe de la primul element al interfeței cu utilizatorul, care va fi evidențiat sau subliniat în cazul primului element din\\n' +\n  '  calea elementului Subsol.</p>\\n' +\n  '\\n' +\n  '<h1>Navigați între secțiunile interfeței cu utilizatorul</h1>\\n' +\n  '\\n' +\n  '<p>Pentru a trece de la o secțiune a interfeței cu utilizatorul la alta, apăsați <strong>Tab</strong>.</p>\\n' +\n  '\\n' +\n  '<p>Pentru a trece de la o secțiune a interfeței cu utilizatorul la cea anterioară, apăsați <strong>Shift+Tab</strong>.</p>\\n' +\n  '\\n' +\n  '<p>Ordinea cu <strong>Tab</strong> a acestor secțiuni ale interfeței cu utilizatorul este următoarea:</p>\\n' +\n  '\\n' +\n  '<ol>\\n' +\n  '  <li>Bara de meniu</li>\\n' +\n  '  <li>Fiecare grup de bare de instrumente</li>\\n' +\n  '  <li>Bara laterală</li>\\n' +\n  '  <li>Calea elementului în subsol</li>\\n' +\n  '  <li>Buton de comutare a numărului de cuvinte în subsol</li>\\n' +\n  '  <li>Link de branding în subsol</li>\\n' +\n  '  <li>Mâner de redimensionare a editorului în subsol</li>\\n' +\n  '</ol>\\n' +\n  '\\n' +\n  '<p>În cazul în care o secțiune a interfeței cu utilizatorul nu este prezentă, aceasta este omisă.</p>\\n' +\n  '\\n' +\n  '<p>În cazul în care subsolul are focalizarea navigației asupra tastaturii și nu există o bară laterală vizibilă, apăsarea butonului <strong>Shift+Tab</strong>\\n' +\n  '  mută focalizarea pe primul grup de bare de instrumente, nu pe ultimul.</p>\\n' +\n  '\\n' +\n  '<h1>Navigați în secțiunile interfeței cu utilizatorul</h1>\\n' +\n  '\\n' +\n  '<p>Pentru a trece de la un element de interfață cu utilizatorul la următorul, apăsați tasta cu <strong>săgeata</strong> corespunzătoare.</p>\\n' +\n  '\\n' +\n  '<p>Tastele cu săgeți către <strong>stânga</strong> și <strong>dreapta</strong></p>\\n' +\n  '\\n' +\n  '<ul>\\n' +\n  '  <li>navighează între meniurile din bara de meniuri.</li>\\n' +\n  '  <li>deschid un sub-meniu dintr-un meniu.</li>\\n' +\n  '  <li>navighează între butoanele dintr-un grup de bare de instrumente.</li>\\n' +\n  '  <li>navighează între elementele din calea elementelor subsolului.</li>\\n' +\n  '</ul>\\n' +\n  '\\n' +\n  '<p>Tastele cu săgeți în <strong>sus</strong> și în <strong>jos</strong></p>\\n' +\n  '\\n' +\n  '<ul>\\n' +\n  '  <li>navighează între elementele de meniu dintr-un meniu.</li>\\n' +\n  '  <li>navighează între elementele unui meniu pop-up din bara de instrumente.</li>\\n' +\n  '</ul>\\n' +\n  '\\n' +\n  '<p>Tastele cu <strong>săgeți</strong> navighează în cadrul secțiunii interfeței cu utilizatorul asupra căreia se focalizează.</p>\\n' +\n  '\\n' +\n  '<p>Pentru a închide un meniu deschis, un sub-meniu deschis sau un meniu pop-up deschis, apăsați tasta <strong>Esc</strong>.</p>\\n' +\n  '\\n' +\n  '<p>Dacă focalizarea curentă este asupra „părții superioare” a unei anumite secțiuni a interfeței cu utilizatorul, prin apăsarea tastei <strong>Esc</strong> se iese, de asemenea,\\n' +\n  '  în întregime din navigarea de la tastatură.</p>\\n' +\n  '\\n' +\n  '<h1>Executarea unui element de meniu sau a unui buton din bara de instrumente</h1>\\n' +\n  '\\n' +\n  '<p>Atunci când elementul de meniu dorit sau butonul dorit din bara de instrumente este evidențiat, apăsați <strong>Return</strong>, <strong>Enter</strong>,\\n' +\n  '  sau <strong>bara de spațiu</strong> pentru a executa elementul.</p>\\n' +\n  '\\n' +\n  '<h1>Navigarea de dialoguri fără file</h1>\\n' +\n  '\\n' +\n  '<p>În dialogurile fără file, prima componentă interactivă beneficiază de focalizare la deschiderea dialogului.</p>\\n' +\n  '\\n' +\n  '<p>Navigați între componentele dialogului interactiv apăsând <strong>Tab</strong> sau <strong>Shift+Tab</strong>.</p>\\n' +\n  '\\n' +\n  '<h1>Navigarea de dialoguri cu file</h1>\\n' +\n  '\\n' +\n  '<p>În dialogurile cu file, primul buton din meniul cu file beneficiază de focalizare la deschiderea dialogului.</p>\\n' +\n  '\\n' +\n  '<p>Navigați între componentele interactive ale acestei file de dialog apăsând <strong>Tab</strong> sau\\n' +\n  '  <strong>Shift+Tab</strong>.</p>\\n' +\n  '\\n' +\n  '<p>Treceți la o altă filă de dialog focalizând asupra meniului cu file și apoi apăsând <strong>săgeata</strong> corespunzătoare\\n' +\n  '  pentru a parcurge filele disponibile.</p>\\n');"
  },
  {
    "path": "apps/web-antd/public/tinymce/plugins/help/js/i18n/keynav/ru.js",
    "content": "tinymce.Resource.add('tinymce.html-i18n.help-keynav.ru',\n'<h1>Начните управление с помощью клавиатуры</h1>\\n' +\n  '\\n' +\n  '<dl>\\n' +\n  '  <dt>Фокус на панели меню</dt>\\n' +\n  '  <dd>Windows или Linux: Alt+F9</dd>\\n' +\n  '  <dd>macOS: &#x2325;F9</dd>\\n' +\n  '  <dt>Фокус на панели инструментов</dt>\\n' +\n  '  <dd>Windows или Linux: Alt+F10</dd>\\n' +\n  '  <dd>macOS: &#x2325;F10</dd>\\n' +\n  '  <dt>Фокус на нижнем колонтитуле</dt>\\n' +\n  '  <dd>Windows или Linux: Alt+F11</dd>\\n' +\n  '  <dd>macOS: &#x2325;F11</dd>\\n' +\n  '  <dt>Фокус на уведомлении</dt>\\n' +\n  '  <dd>Windows или Linux: Alt+12</dd>\\n' +\n  '  <dd>macOS: &#x2325;F12</dd>\\n' +\n  '  <dt>Фокус на контекстной панели инструментов</dt>\\n' +\n  '  <dd>Windows, Linux или macOS: Ctrl+F9</dd>\\n' +\n  '</dl>\\n' +\n  '\\n' +\n  '<p>Первый доступный для управления элемент интерфейса будет выделен цветом или подчеркнут (если он находится\\n' +\n  '  в пути элементов нижнего колонтитула).</p>\\n' +\n  '\\n' +\n  '<h1>Переход между разделами пользовательского интерфейса</h1>\\n' +\n  '\\n' +\n  '<p>Чтобы перейти из текущего раздела интерфейса в следующий, нажмите <strong>Tab</strong>.</p>\\n' +\n  '\\n' +\n  '<p>Чтобы перейти из текущего раздела интерфейса в предыдущий, нажмите <strong>Shift+Tab</strong>.</p>\\n' +\n  '\\n' +\n  '<p><strong>Вкладки</strong> разделов интерфейса расположены в следующем порядке:</p>\\n' +\n  '\\n' +\n  '<ol>\\n' +\n  '  <li>Панель меню</li>\\n' +\n  '  <li>Группы панели инструментов</li>\\n' +\n  '  <li>Боковая панель</li>\\n' +\n  '  <li>Путь элементов нижнего колонтитула</li>\\n' +\n  '  <li>Подсчет слов/символов в нижнем колонтитуле</li>\\n' +\n  '  <li>Брендовая ссылка в нижнем колонтитуле</li>\\n' +\n  '  <li>Угол для изменения размера окна редактора</li>\\n' +\n  '</ol>\\n' +\n  '\\n' +\n  '<p>Если раздел интерфейса отсутствует, он пропускается.</p>\\n' +\n  '\\n' +\n  '<p>Если при управлении с клавиатуры фокус находится на нижнем колонтитуле, а видимая боковая панель отсутствует, то при нажатии сочетания клавиш <strong>Shift+Tab</strong>\\n' +\n  '  фокус переносится на первую группу панели инструментов, а не на последнюю.</p>\\n' +\n  '\\n' +\n  '<h1>Переход между элементами внутри разделов пользовательского интерфейса</h1>\\n' +\n  '\\n' +\n  '<p>Чтобы перейти от текущего элемента интерфейса к следующему, нажмите соответствующую <strong>клавишу со стрелкой</strong>.</p>\\n' +\n  '\\n' +\n  '<p>Клавиши со стрелками <strong>влево</strong> и <strong>вправо</strong> позволяют</p>\\n' +\n  '\\n' +\n  '<ul>\\n' +\n  '  <li>перемещаться между разными меню в панели меню.</li>\\n' +\n  '  <li>открывать разделы меню.</li>\\n' +\n  '  <li>перемещаться между кнопками в группе панели инструментов.</li>\\n' +\n  '  <li>перемещаться между элементами в пути элементов нижнего колонтитула.</li>\\n' +\n  '</ul>\\n' +\n  '\\n' +\n  '<p>Клавиши со стрелками <strong>вниз</strong> и <strong>вверх</strong> позволяют</p>\\n' +\n  '\\n' +\n  '<ul>\\n' +\n  '  <li>перемещаться между элементами одного меню.</li>\\n' +\n  '  <li>перемещаться между элементами всплывающего меню в панели инструментов.</li>\\n' +\n  '</ul>\\n' +\n  '\\n' +\n  '<p>При использовании <strong>клавиш со стрелками</strong> вы будете циклически перемещаться по элементам в пределах выбранного раздела интерфейса.</p>\\n' +\n  '\\n' +\n  '<p>Чтобы закрыть открытое меню, его раздел или всплывающее меню, нажмите клавишу <strong>Esc</strong>.</p>\\n' +\n  '\\n' +\n  '<p>Если фокус находится наверху какого-либо раздела интерфейса, нажатие клавиши <strong>Esc</strong> также приведет\\n' +\n  '  к выходу из режима управления с помощью клавиатуры.</p>\\n' +\n  '\\n' +\n  '<h1>Использование элемента меню или кнопки на панели инструментов</h1>\\n' +\n  '\\n' +\n  '<p>Когда элемент меню или кнопка панели инструментов будут выделены, нажмите <strong>Return</strong>, <strong>Enter</strong>\\n' +\n  '  или <strong>Space</strong>, чтобы их активировать.</p>\\n' +\n  '\\n' +\n  '<h1>Управление в диалоговом окне без вкладок</h1>\\n' +\n  '\\n' +\n  '<p>При открытии диалогового окна без вкладок фокус переносится на первый интерактивный компонент.</p>\\n' +\n  '\\n' +\n  '<p>Для перехода между интерактивными компонентами диалогового окна нажимайте <strong>Tab</strong> или <strong>Shift+Tab</strong>.</p>\\n' +\n  '\\n' +\n  '<h1>Управление в диалоговом окне с вкладками</h1>\\n' +\n  '\\n' +\n  '<p>При открытии диалогового окна с вкладками фокус переносится на первую кнопку в меню вкладок.</p>\\n' +\n  '\\n' +\n  '<p>Для перехода между интерактивными компонентами этой вкладки диалогового окна нажимайте <strong>Tab</strong> или\\n' +\n  '  <strong>Shift+Tab</strong>.</p>\\n' +\n  '\\n' +\n  '<p>Для перехода на другую вкладку диалогового окна переместите фокус на меню вкладок, а затем используйте <strong>клавиши со стрелками</strong>\\n' +\n  '  для циклического переключения между доступными вкладками.</p>\\n');"
  },
  {
    "path": "apps/web-antd/public/tinymce/plugins/help/js/i18n/keynav/sk.js",
    "content": "tinymce.Resource.add('tinymce.html-i18n.help-keynav.sk',\n'<h1>Začíname s navigáciou pomocou klávesnice</h1>\\n' +\n  '\\n' +\n  '<dl>\\n' +\n  '  <dt>Prejsť na panel s ponukami</dt>\\n' +\n  '  <dd>Windows alebo Linux: Alt+F9</dd>\\n' +\n  '  <dd>macOS: &#x2325;F9</dd>\\n' +\n  '  <dt>Prejsť na panel nástrojov</dt>\\n' +\n  '  <dd>Windows alebo Linux: Alt+F10</dd>\\n' +\n  '  <dd>macOS: &#x2325;F10</dd>\\n' +\n  '  <dt>Prejsť na pätičku</dt>\\n' +\n  '  <dd>Windows alebo Linux: Alt+F11</dd>\\n' +\n  '  <dd>macOS: &#x2325;F11</dd>\\n' +\n  '  <dt>Zaostriť na oznámenie</dt>\\n' +\n  '  <dd>Windows alebo Linux: Alt+F12</dd>\\n' +\n  '  <dd>macOS: &#x2325;F12</dd>\\n' +\n  '  <dt>Prejsť na kontextový panel nástrojov</dt>\\n' +\n  '  <dd>Windows, Linux alebo macOS: Ctrl+F9</dd>\\n' +\n  '</dl>\\n' +\n  '\\n' +\n  '<p>Navigácia začne pri prvej položke používateľského rozhrania, ktorá bude zvýraznená alebo v prípade prvej položky\\n' +\n  '  cesty k pätičke podčiarknutá.</p>\\n' +\n  '\\n' +\n  '<h1>Navigácia medzi časťami používateľského rozhrania</h1>\\n' +\n  '\\n' +\n  '<p>Ak sa chcete posunúť z jednej časti používateľského rozhrania do druhej, stlačte tlačidlo <strong>Tab</strong>.</p>\\n' +\n  '\\n' +\n  '<p>Ak sa chcete posunúť z jednej časti používateľského rozhrania do predchádzajúcej, stlačte tlačidlá <strong>Shift + Tab</strong>.</p>\\n' +\n  '\\n' +\n  '<p>Poradie prepínania medzi týmito časťami používateľského rozhrania pri stláčaní tlačidla <strong>Tab</strong>:</p>\\n' +\n  '\\n' +\n  '<ol>\\n' +\n  '  <li>Panel s ponukou</li>\\n' +\n  '  <li>Každá skupina panela nástrojov</li>\\n' +\n  '  <li>Bočný panel</li>\\n' +\n  '  <li>Cesta k prvku v pätičke</li>\\n' +\n  '  <li>Prepínač počtu slov v pätičke</li>\\n' +\n  '  <li>Odkaz na informácie o značke v pätičke</li>\\n' +\n  '  <li>Úchyt na zmenu veľkosti editora v pätičke</li>\\n' +\n  '</ol>\\n' +\n  '\\n' +\n  '<p>Ak nejaká časť používateľského rozhrania nie je prítomná, preskočí sa.</p>\\n' +\n  '\\n' +\n  '<p>Ak je pätička vybratá na navigáciu pomocou klávesnice a nie je viditeľný bočný panel, stlačením klávesov <strong>Shift+Tab</strong>\\n' +\n  '  prejdete na prvú skupinu panela nástrojov, nie na poslednú.</p>\\n' +\n  '\\n' +\n  '<h1>Navigácia v rámci častí používateľského rozhrania</h1>\\n' +\n  '\\n' +\n  '<p>Ak sa chcete posunúť z jedného prvku používateľského rozhrania na ďalší, stlačte príslušný kláves so <strong>šípkou</strong>.</p>\\n' +\n  '\\n' +\n  '<p>Klávesy so šípkami <strong>doľava</strong> a <strong>doprava</strong></p>\\n' +\n  '\\n' +\n  '<ul>\\n' +\n  '  <li>umožňujú presun medzi ponukami na paneli ponúk,</li>\\n' +\n  '  <li>otvárajú podponuku v rámci ponuky,</li>\\n' +\n  '  <li>umožňujú presun medzi tlačidlami v skupine panelov nástrojov,</li>\\n' +\n  '  <li>umožňujú presun medzi položkami cesty prvku v pätičke.</li>\\n' +\n  '</ul>\\n' +\n  '\\n' +\n  '<p>Klávesy so šípkami <strong>dole</strong> a <strong>hore</strong></p>\\n' +\n  '\\n' +\n  '<ul>\\n' +\n  '  <li>umožňujú presun medzi položkami ponuky,</li>\\n' +\n  '  <li>umožňujú presun medzi položkami v kontextovej ponuke panela nástrojov.</li>\\n' +\n  '</ul>\\n' +\n  '\\n' +\n  '<p>Klávesy so <strong>šípkami</strong> vykonávajú prepínanie v rámci vybranej časti používateľského rozhrania.</p>\\n' +\n  '\\n' +\n  '<p>Ak chcete zatvoriť otvorenú ponuku, otvorenú podponuku alebo otvorenú kontextovú ponuku, stlačte kláves <strong>Esc</strong>.</p>\\n' +\n  '\\n' +\n  '<p>Ak je aktuálne vybratá horná časť konkrétneho používateľského rozhrania, stlačením klávesu <strong>Esc</strong> úplne ukončíte tiež\\n' +\n  '  navigáciu pomocou klávesnice.</p>\\n' +\n  '\\n' +\n  '<h1>Vykonanie príkazu položky ponuky alebo tlačidla panela nástrojov</h1>\\n' +\n  '\\n' +\n  '<p>Keď je zvýraznená požadovaná položka ponuky alebo tlačidlo panela nástrojov, stlačením klávesov <strong>Return</strong>, <strong>Enter</strong>\\n' +\n  '  alebo <strong>medzerníka</strong> vykonáte príslušný príkaz položky.</p>\\n' +\n  '\\n' +\n  '<h1>Navigácia v dialógových oknách bez záložiek</h1>\\n' +\n  '\\n' +\n  '<p>Pri otvorení dialógových okien bez záložiek prejdete na prvý interaktívny komponent.</p>\\n' +\n  '\\n' +\n  '<p>Medzi interaktívnymi dialógovými komponentmi môžete prechádzať stlačením klávesov <strong>Tab</strong> alebo <strong>Shift+Tab</strong>.</p>\\n' +\n  '\\n' +\n  '<h1>Navigácia v dialógových oknách so záložkami</h1>\\n' +\n  '\\n' +\n  '<p>Pri otvorení dialógových okien so záložkami prejdete na prvé tlačidlo v ponuke záložiek.</p>\\n' +\n  '\\n' +\n  '<p>Medzi interaktívnymi komponentmi tejto dialógovej záložky môžete prechádzať stlačením klávesov <strong>Tab</strong> alebo\\n' +\n  '  <strong>Shift+Tab</strong>.</p>\\n' +\n  '\\n' +\n  '<p>Ak chcete prepnúť na ďalšiu záložku dialógového okna, prejdite do ponuky záložiek a potom môžete stlačením príslušného klávesu so <strong>šípkou</strong>\\n' +\n  '  prepínať medzi dostupnými záložkami.</p>\\n');"
  },
  {
    "path": "apps/web-antd/public/tinymce/plugins/help/js/i18n/keynav/sl_SI.js",
    "content": "tinymce.Resource.add('tinymce.html-i18n.help-keynav.sl_SI',\n'<h1>Začetek krmarjenja s tipkovnico</h1>\\n' +\n  '\\n' +\n  '<dl>\\n' +\n  '  <dt>Fokus na menijsko vrstico</dt>\\n' +\n  '  <dd>Windows ali Linux: Alt + F9</dd>\\n' +\n  '  <dd>macOS: &#x2325;F9</dd>\\n' +\n  '  <dt>Fokus na orodno vrstico</dt>\\n' +\n  '  <dd>Windows ali Linux: Alt + F10</dd>\\n' +\n  '  <dd>macOS: &#x2325;F10</dd>\\n' +\n  '  <dt>Fokus na nogo</dt>\\n' +\n  '  <dd>Windows ali Linux: Alt + F11</dd>\\n' +\n  '  <dd>macOS: &#x2325;F11</dd>\\n' +\n  '  <dt>Označitev obvestila</dt>\\n' +\n  '  <dd>Windows ali Linux: Alt + F12</dd>\\n' +\n  '  <dd>macOS: &#x2325;F12</dd>\\n' +\n  '  <dt>Fokus na kontekstualno orodno vrstico</dt>\\n' +\n  '  <dd>Windows, Linux ali macOS: Ctrl + F9</dd>\\n' +\n  '</dl>\\n' +\n  '\\n' +\n  '<p>Krmarjenje se bo začelo s prvim elementom uporabniškega vmesnika, ki bo izpostavljena ali podčrtan, če gre za prvi element na\\n' +\n  '  poti do elementa noge.</p>\\n' +\n  '\\n' +\n  '<h1>Krmarjenje med razdelki uporabniškega vmesnika</h1>\\n' +\n  '\\n' +\n  '<p>Če se želite pomakniti z enega dela uporabniškega vmesnika na naslednjega, pritisnite <strong>tabulatorko</strong>.</p>\\n' +\n  '\\n' +\n  '<p>Če se želite pomakniti z enega dela uporabniškega vmesnika na prejšnjega, pritisnite <strong>shift + tabulatorko</strong>.</p>\\n' +\n  '\\n' +\n  '<p>Zaporedje teh razdelkov uporabniškega vmesnika, ko pritiskate <strong>tabulatorko</strong>, je:</p>\\n' +\n  '\\n' +\n  '<ol>\\n' +\n  '  <li>Menijska vrstica</li>\\n' +\n  '  <li>Posamezne skupine orodne vrstice</li>\\n' +\n  '  <li>Stranska vrstica</li>\\n' +\n  '  <li>Pod do elementa v nogi</li>\\n' +\n  '  <li>Gumb za preklop štetja besed v nogi</li>\\n' +\n  '  <li>Povezava do blagovne znamke v nogi</li>\\n' +\n  '  <li>Ročaj za spreminjanje velikosti urejevalnika v nogi</li>\\n' +\n  '</ol>\\n' +\n  '\\n' +\n  '<p>Če razdelek uporabniškega vmesnika ni prisoten, je preskočen.</p>\\n' +\n  '\\n' +\n  '<p>Če ima noga fokus za krmarjenje s tipkovnico in ni vidne stranske vrstice, s pritiskom na <strong>shift + tabulatorko</strong>\\n' +\n  '  fokus premaknete na prvo skupino orodne vrstice, ne zadnjo.</p>\\n' +\n  '\\n' +\n  '<h1>Krmarjenje v razdelkih uporabniškega vmesnika</h1>\\n' +\n  '\\n' +\n  '<p>Če se želite premakniti z enega elementa uporabniškega vmesnika na naslednjega, pritisnite ustrezno <strong>puščično</strong> tipko.</p>\\n' +\n  '\\n' +\n  '<p><strong>Leva</strong> in <strong>desna</strong> puščična tipka</p>\\n' +\n  '\\n' +\n  '<ul>\\n' +\n  '  <li>omogočata premikanje med meniji v menijski vrstici.</li>\\n' +\n  '  <li>odpreta podmeni v meniju.</li>\\n' +\n  '  <li>omogočata premikanje med gumbi v skupini orodne vrstice.</li>\\n' +\n  '  <li>omogočata premikanje med elementi na poti do elementov noge.</li>\\n' +\n  '</ul>\\n' +\n  '\\n' +\n  '<p><strong>Spodnja</strong> in <strong>zgornja</strong> puščična tipka</p>\\n' +\n  '\\n' +\n  '<ul>\\n' +\n  '  <li>omogočata premikanje med elementi menija.</li>\\n' +\n  '  <li>omogočata premikanje med elementi v pojavnem meniju orodne vrstice.</li>\\n' +\n  '</ul>\\n' +\n  '\\n' +\n  '<p><strong>Puščične</strong> tipke omogočajo kroženje znotraj razdelka uporabniškega vmesnika, na katerem je fokus.</p>\\n' +\n  '\\n' +\n  '<p>Če želite zapreti odprt meni, podmeni ali pojavni meni, pritisnite tipko <strong>Esc</strong>.</p>\\n' +\n  '\\n' +\n  '<p>Če je trenutni fokus na »vrhu« določenega razdelka uporabniškega vmesnika, s pritiskom tipke <strong>Esc</strong> zaprete\\n' +\n  '  tudi celotno krmarjenje s tipkovnico.</p>\\n' +\n  '\\n' +\n  '<h1>Izvajanje menijskega elementa ali gumba orodne vrstice</h1>\\n' +\n  '\\n' +\n  '<p>Ko je označen želeni menijski element ali orodja vrstica, pritisnite <strong>vračalko</strong>, <strong>Enter</strong>\\n' +\n  '  ali <strong>preslednico</strong>, da izvedete element.</p>\\n' +\n  '\\n' +\n  '<h1>Krmarjenje po pogovornih oknih brez zavihkov</h1>\\n' +\n  '\\n' +\n  '<p>Ko odprete pogovorno okno brez zavihkov, ima fokus prva interaktivna komponenta.</p>\\n' +\n  '\\n' +\n  '<p>Med interaktivnimi komponentami pogovornega okna se premikate s pritiskom <strong>tabulatorke</strong> ali kombinacije tipke <strong>shift + tabulatorke</strong>.</p>\\n' +\n  '\\n' +\n  '<h1>Krmarjenje po pogovornih oknih z zavihki</h1>\\n' +\n  '\\n' +\n  '<p>Ko odprete pogovorno okno z zavihki, ima fokus prvi gumb v meniju zavihka.</p>\\n' +\n  '\\n' +\n  '<p>Med interaktivnimi komponentami tega zavihka pogovornega okna se premikate s pritiskom <strong>tabulatorke</strong> ali\\n' +\n  '  kombinacije tipke <strong>shift + tabulatorke</strong>.</p>\\n' +\n  '\\n' +\n  '<p>Na drug zavihek pogovornega okna preklopite tako, da fokus prestavite na meni zavihka in nato pritisnete ustrezno <strong>puščično</strong>\\n' +\n  '  tipko, da se pomaknete med razpoložljivimi zavihki.</p>\\n');"
  },
  {
    "path": "apps/web-antd/public/tinymce/plugins/help/js/i18n/keynav/sv_SE.js",
    "content": "tinymce.Resource.add('tinymce.html-i18n.help-keynav.sv_SE',\n'<h1>Påbörja tangentbordsnavigering</h1>\\n' +\n  '\\n' +\n  '<dl>\\n' +\n  '  <dt>Fokusera på menyraden</dt>\\n' +\n  '  <dd>Windows eller Linux: Alt+F9</dd>\\n' +\n  '  <dd>macOS: &#x2325;F9</dd>\\n' +\n  '  <dt>Fokusera på verktygsraden</dt>\\n' +\n  '  <dd>Windows eller Linux: Alt+F10</dd>\\n' +\n  '  <dd>macOS: &#x2325;F10</dd>\\n' +\n  '  <dt>Fokusera på verktygsraden</dt>\\n' +\n  '  <dd>Windows eller Linux: Alt+F11</dd>\\n' +\n  '  <dd>macOS: &#x2325;F11</dd>\\n' +\n  '  <dt>Fokusera aviseringen</dt>\\n' +\n  '  <dd>Windows eller Linux: Alt+F12</dd>\\n' +\n  '  <dd>macOS: &#x2325;F12</dd>\\n' +\n  '  <dt>Fokusera på en snabbverktygsrad</dt>\\n' +\n  '  <dd>Windows, Linux eller macOS: Ctrl+F9</dd>\\n' +\n  '</dl>\\n' +\n  '\\n' +\n  '<p>Navigeringen börjar vid det första gränssnittsobjektet, vilket är markerat eller understruket om det gäller det första objektet i\\n' +\n  '  sidfotens elementsökväg.</p>\\n' +\n  '\\n' +\n  '<h1>Navigera mellan UI-avsnitt</h1>\\n' +\n  '\\n' +\n  '<p>Flytta från ett UI-avsnitt till nästa genom att trycka på <strong>Tabb</strong>.</p>\\n' +\n  '\\n' +\n  '<p>Flytta från ett UI-avsnitt till det föregående genom att trycka på <strong>Skift+Tabb</strong>.</p>\\n' +\n  '\\n' +\n  '<p><strong>Tabb</strong>-ordningen för dessa UI-avsnitt är:</p>\\n' +\n  '\\n' +\n  '<ol>\\n' +\n  '  <li>Menyrad</li>\\n' +\n  '  <li>Varje verktygsradsgrupp</li>\\n' +\n  '  <li>Sidoruta</li>\\n' +\n  '  <li>Elementsökväg i sidfoten</li>\\n' +\n  '  <li>Växlingsknapp för ordantal i sidfoten</li>\\n' +\n  '  <li>Varumärkeslänk i sidfoten</li>\\n' +\n  '  <li>Storlekshandtag för redigeraren i sidfoten</li>\\n' +\n  '</ol>\\n' +\n  '\\n' +\n  '<p>Om ett UI-avsnitt inte finns hoppas det över.</p>\\n' +\n  '\\n' +\n  '<p>Om sidfoten har fokus på tangentbordsnavigering, och det inte finns någon synlig sidoruta, flyttas fokus till den första verktygsradsgruppen\\n' +\n  '  när du trycker på <strong>Skift+Tabb</strong>, inte till den sista.</p>\\n' +\n  '\\n' +\n  '<h1>Navigera i UI-avsnitt</h1>\\n' +\n  '\\n' +\n  '<p>Flytta från ett UI-element till nästa genom att trycka på motsvarande <strong>piltangent</strong>.</p>\\n' +\n  '\\n' +\n  '<p><strong>Vänsterpil</strong> och <strong>högerpil</strong></p>\\n' +\n  '\\n' +\n  '<ul>\\n' +\n  '  <li>flytta mellan menyer på menyraden.</li>\\n' +\n  '  <li>öppna en undermeny på en meny.</li>\\n' +\n  '  <li>flytta mellan knappar i en verktygsradgrupp.</li>\\n' +\n  '  <li>flytta mellan objekt i sidfotens elementsökväg.</li>\\n' +\n  '</ul>\\n' +\n  '\\n' +\n  '<p><strong>Nedpil</strong> och <strong>uppil</strong></p>\\n' +\n  '\\n' +\n  '<ul>\\n' +\n  '  <li>flytta mellan menyalternativ på en meny.</li>\\n' +\n  '  <li>flytta mellan alternativ på en popup-meny på verktygsraden.</li>\\n' +\n  '</ul>\\n' +\n  '\\n' +\n  '<p><strong>Piltangenterna</strong> cirkulerar inom det fokuserade UI-avsnittet.</p>\\n' +\n  '\\n' +\n  '<p>Tryck på <strong>Esc</strong>-tangenten om du vill stänga en öppen meny, undermeny eller popup-meny.</p>\\n' +\n  '\\n' +\n  '<p>Om det aktuella fokuset är högst upp i ett UI-avsnitt avlutas även tangentbordsnavigeringen helt när\\n' +\n  '  du trycker på <strong>Esc</strong>-tangenten.</p>\\n' +\n  '\\n' +\n  '<h1>Köra ett menyalternativ eller en verktygfältsknapp</h1>\\n' +\n  '\\n' +\n  '<p>När menyalternativet eller verktygsradsknappen är markerad trycker du på <strong>Retur</strong>, <strong>Enter</strong>\\n' +\n  '  eller <strong>blanksteg</strong> för att köra alternativet.</p>\\n' +\n  '\\n' +\n  '<h1>Navigera i dialogrutor utan flikar</h1>\\n' +\n  '\\n' +\n  '<p>I dialogrutor utan flikar är den första interaktiva komponenten i fokus när dialogrutan öppnas.</p>\\n' +\n  '\\n' +\n  '<p>Navigera mellan interaktiva dialogkomponenter genom att trycka på <strong>Tabb</strong> eller <strong>Skift+Tabb</strong>.</p>\\n' +\n  '\\n' +\n  '<h1>Navigera i dialogrutor med flikar</h1>\\n' +\n  '\\n' +\n  '<p>I dialogrutor utan flikar är den första knappen på flikmenyn i fokus när dialogrutan öppnas.</p>\\n' +\n  '\\n' +\n  '<p>Navigera mellan interaktiva komponenter på dialogrutefliken genom att trycka på <strong>Tabb</strong> eller\\n' +\n  '  <strong>Skift+Tabb</strong>.</p>\\n' +\n  '\\n' +\n  '<p>Växla till en annan dialogruta genom att fokusera på flikmenyn och sedan trycka på motsvarande <strong>piltangent</strong>\\n' +\n  '  för att cirkulera mellan de tillgängliga flikarna.</p>\\n');"
  },
  {
    "path": "apps/web-antd/public/tinymce/plugins/help/js/i18n/keynav/th_TH.js",
    "content": "tinymce.Resource.add('tinymce.html-i18n.help-keynav.th_TH',\n'<h1>เริ่มต้นการนำทางด้วยแป้นพิมพ์</h1>\\n' +\n  '\\n' +\n  '<dl>\\n' +\n  '  <dt>โฟกัสที่แถบเมนู</dt>\\n' +\n  '  <dd>Windows หรือ Linux: Alt+F9</dd>\\n' +\n  '  <dd>macOS: &#x2325;F9</dd>\\n' +\n  '  <dt>โฟกัสที่แถบเครื่องมือ</dt>\\n' +\n  '  <dd>Windows หรือ Linux: Alt+F10</dd>\\n' +\n  '  <dd>macOS: &#x2325;F10</dd>\\n' +\n  '  <dt>โฟกัสที่ส่วนท้าย</dt>\\n' +\n  '  <dd>Windows หรือ Linux: Alt+F11</dd>\\n' +\n  '  <dd>macOS: &#x2325;F11</dd>\\n' +\n  '  <dt>โฟกัสไปที่การแจ้งเตือน</dt>\\n' +\n  '  <dd>Windows หรือ Linux: Alt+F12</dd>\\n' +\n  '  <dd>macOS: &#x2325;F12</dd>\\n' +\n  '  <dt>โฟกัสที่แถบเครื่องมือตามบริบท</dt>\\n' +\n  '  <dd>Windows, Linux หรือ macOS: Ctrl+F9</dd>\\n' +\n  '</dl>\\n' +\n  '\\n' +\n  '<p>การนำทางจะเริ่มที่รายการ UI แรก ซึ่งจะมีการไฮไลต์หรือขีดเส้นใต้ไว้ในกรณีที่รายการแรกอยู่ใน\\n' +\n  '  พาธองค์ประกอบส่วนท้าย</p>\\n' +\n  '\\n' +\n  '<h1>การนำทางระหว่างส่วนต่างๆ ของ UI</h1>\\n' +\n  '\\n' +\n  '<p>ในการย้ายจากส่วน UI หนึ่งไปยังส่วนถัดไป ให้กด <strong>Tab</strong></p>\\n' +\n  '\\n' +\n  '<p>ในการย้ายจากส่วน UI หนึ่งไปยังส่วนก่อนหน้า ให้กด <strong>Shift+Tab</strong></p>\\n' +\n  '\\n' +\n  '<p>ลำดับ<strong>แท็บ</strong>ของส่วนต่างๆ ของ UI คือ:</p>\\n' +\n  '\\n' +\n  '<ol>\\n' +\n  '  <li>แถบเมนู</li>\\n' +\n  '  <li>แต่ละกลุ่มแถบเครื่องมือ</li>\\n' +\n  '  <li>แถบข้าง</li>\\n' +\n  '  <li>พาธองค์ประกอบในส่วนท้าย</li>\\n' +\n  '  <li>ปุ่มสลับเปิด/ปิดจำนวนคำในส่วนท้าย</li>\\n' +\n  '  <li>ลิงก์ชื่อแบรนด์ในส่วนท้าย</li>\\n' +\n  '  <li>จุดจับปรับขนาดของตัวแก้ไขในส่วนท้าย</li>\\n' +\n  '</ol>\\n' +\n  '\\n' +\n  '<p>หากส่วน UI ไม่ปรากฏ แสดงว่าถูกข้ามไป</p>\\n' +\n  '\\n' +\n  '<p>หากส่วนท้ายมีการโฟกัสการนำทางแป้นพิมพ์และไม่มีแถบข้างปรากฏ การกด <strong>Shift+Tab</strong>\\n' +\n  '  จะย้ายการโฟกัสไปที่กลุ่มแถบเครื่องมือแรก ไม่ใช่สุดท้าย</p>\\n' +\n  '\\n' +\n  '<h1>การนำทางภายในส่วนต่างๆ ของ UI</h1>\\n' +\n  '\\n' +\n  '<p>ในการย้ายจากองค์ประกอบ UI หนึ่งไปยังองค์ประกอบส่วนถัดไป ให้กดปุ่ม<strong>ลูกศร</strong>ที่เหมาะสม</p>\\n' +\n  '\\n' +\n  '<p>ปุ่มลูกศร<strong>ซ้าย</strong>และ<strong>ขวา</strong></p>\\n' +\n  '\\n' +\n  '<ul>\\n' +\n  '  <li>ย้ายไปมาระหว่างเมนูต่างๆ ในแถบเมนู</li>\\n' +\n  '  <li>เปิดเมนูย่อยในเมนู</li>\\n' +\n  '  <li>ย้ายไปมาระหว่างปุ่มต่างๆ ในกลุ่มแถบเครื่องมือ</li>\\n' +\n  '  <li>ย้ายไปมาระหว่างรายการต่างๆ ในพาธองค์ประกอบของส่วนท้าย</li>\\n' +\n  '</ul>\\n' +\n  '\\n' +\n  '<p>ปุ่มลูกศร<strong>ลง</strong>และ<strong>ขึ้น</strong></p>\\n' +\n  '\\n' +\n  '<ul>\\n' +\n  '  <li>ย้ายไปมาระหว่างรายการเมนูต่างๆ ในเมนู</li>\\n' +\n  '  <li>ย้ายไปมาระหว่างรายการต่างๆ ในเมนูป๊อบอัพแถบเครื่องมือ</li>\\n' +\n  '</ul>\\n' +\n  '\\n' +\n  '<p>ปุ่ม<strong>ลูกศร</strong>จะเลื่อนไปมาภายในส่วน UI ที่โฟกัส</p>\\n' +\n  '\\n' +\n  '<p>ในการปิดเมนูที่เปิดอยู่ เมนูย่อยที่เปิดอยู่ หรือเมนูป๊อบอัพที่เปิดอยู่ ให้กดปุ่ม <strong>Esc</strong></p>\\n' +\n  '\\n' +\n  '<p>หากโฟกัสปัจจุบันอยู่ที่ ‘ด้านบนสุด’ ของส่วน UI เฉพาะ การกดปุ่ม <strong>Esc</strong> จะทำให้ออกจาก\\n' +\n  '  การนำทางด้วยแป้นพิมพ์ทั้งหมดเช่นกัน</p>\\n' +\n  '\\n' +\n  '<h1>การดำเนินการรายการเมนูหรือปุ่มในแถบเครื่องมือ</h1>\\n' +\n  '\\n' +\n  '<p>เมื่อไฮไลต์รายการเมนูหรือปุ่มในแถบเครื่องมือที่ต้องการ ให้กด <strong>Return</strong>, <strong>Enter</strong>\\n' +\n  '  หรือ <strong>Space bar</strong> เพื่อดำเนินการรายการดังกล่าว</p>\\n' +\n  '\\n' +\n  '<h1>การนำทางสำหรับกล่องโต้ตอบที่ไม่อยู่ในแท็บ</h1>\\n' +\n  '\\n' +\n  '<p>ในกล่องโต้ตอบที่ไม่อยู่ในแท็บ จะโฟกัสที่ส่วนประกอบเชิงโต้ตอบแรกเมื่อกล่องโต้ตอบเปิด</p>\\n' +\n  '\\n' +\n  '<p>นำทางระหว่างส่วนประกอบเชิงโต้ตอบต่างๆ ของกล่องโต้ตอบ โดยการกด <strong>Tab</strong> หรือ <strong>Shift+Tab</strong></p>\\n' +\n  '\\n' +\n  '<h1>การนำทางสำหรับกล่องโต้ตอบที่อยู่ในแท็บ</h1>\\n' +\n  '\\n' +\n  '<p>ในกล่องโต้ตอบที่อยู่ในแท็บ จะโฟกัสที่ปุ่มแรกในเมนูแท็บเมื่อกล่องโต้ตอบเปิด</p>\\n' +\n  '\\n' +\n  '<p>นำทางระหว่างส่วนประกอบเชิงโต้ตอบต่างๆ ของแท็บกล่องโต้ตอบนี้โดยการกด <strong>Tab</strong> หรือ\\n' +\n  '  <strong>Shift+Tab</strong></p>\\n' +\n  '\\n' +\n  '<p>สลับไปยังแท็บกล่องโต้ตอบอื่นโดยการเลือกโฟกัสที่เมนูแท็บ แล้วกดปุ่ม<strong>ลูกศร</strong>ที่เหมาะสม\\n' +\n  '  เพื่อเลือกแท็บที่ใช้ได้</p>\\n');"
  },
  {
    "path": "apps/web-antd/public/tinymce/plugins/help/js/i18n/keynav/tr.js",
    "content": "tinymce.Resource.add('tinymce.html-i18n.help-keynav.tr',\n'<h1>Klavyeyle gezintiyi başlatma</h1>\\n' +\n  '\\n' +\n  '<dl>\\n' +\n  '  <dt>Menü çubuğuna odaklan</dt>\\n' +\n  '  <dd>Windows veya Linux: Alt+F9</dd>\\n' +\n  '  <dd>macOS: &#x2325;F9</dd>\\n' +\n  '  <dt>Araç çubuğuna odaklan</dt>\\n' +\n  '  <dd>Windows veya Linux: Alt+F10</dd>\\n' +\n  '  <dd>macOS: &#x2325;F10</dd>\\n' +\n  '  <dt>Alt bilgiye odaklan</dt>\\n' +\n  '  <dd>Windows veya Linux: Alt+F11</dd>\\n' +\n  '  <dd>macOS: &#x2325;F11</dd>\\n' +\n  '  <dt>Bildirime odakla</dt>\\n' +\n  '  <dd>Windows veya Linux: Alt+F12</dd>\\n' +\n  '  <dd>macOS: &#x2325;F12</dd>\\n' +\n  '  <dt>Bağlamsal araç çubuğuna odaklan</dt>\\n' +\n  '  <dd>Windows, Linux veya macOS: Ctrl+F9</dd>\\n' +\n  '</dl>\\n' +\n  '\\n' +\n  '<p>Gezinti ilk kullanıcı arabirimi öğesinden başlar, bu öğe vurgulanır ya da ilk öğe, Alt bilgi elemanı\\n' +\n  '  yolundaysa altı çizilir.</p>\\n' +\n  '\\n' +\n  '<h1>Kullanıcı arabirimi bölümleri arasında gezinme</h1>\\n' +\n  '\\n' +\n  '<p>Sonraki kullanıcı arabirimi bölümüne gitmek için <strong>Sekme</strong> tuşuna basın.</p>\\n' +\n  '\\n' +\n  '<p>Önceki kullanıcı arabirimi bölümüne gitmek için <strong>Shift+Sekme</strong> tuşlarına basın.</p>\\n' +\n  '\\n' +\n  '<p>Bu kullanıcı arabirimi bölümlerinin <strong>Sekme</strong> sırası:</p>\\n' +\n  '\\n' +\n  '<ol>\\n' +\n  '  <li>Menü çubuğu</li>\\n' +\n  '  <li>Her araç çubuğu grubu</li>\\n' +\n  '  <li>Kenar çubuğu</li>\\n' +\n  '  <li>Alt bilgide öğe yolu</li>\\n' +\n  '  <li>Alt bilgide sözcük sayısı geçiş düğmesi</li>\\n' +\n  '  <li>Alt bilgide marka bağlantısı</li>\\n' +\n  '  <li>Alt bilgide düzenleyiciyi yeniden boyutlandırma tutamacı</li>\\n' +\n  '</ol>\\n' +\n  '\\n' +\n  '<p>Kullanıcı arabirimi bölümü yoksa atlanır.</p>\\n' +\n  '\\n' +\n  '<p>Alt bilgide klavyeyle gezinti odağı yoksa ve görünür bir kenar çubuğu mevcut değilse <strong>Shift+Sekme</strong> tuşlarına basıldığında\\n' +\n  '  odak son araç çubuğu yerine ilk araç çubuğu grubuna taşınır.</p>\\n' +\n  '\\n' +\n  '<h1>Kullanıcı arabirimi bölümleri içinde gezinme</h1>\\n' +\n  '\\n' +\n  '<p>Sonraki kullanıcı arabirimi elemanına gitmek için uygun <strong>Ok</strong> tuşuna basın.</p>\\n' +\n  '\\n' +\n  '<p><strong>Sol</strong> ve <strong>Sağ</strong> ok tuşları</p>\\n' +\n  '\\n' +\n  '<ul>\\n' +\n  '  <li>menü çubuğundaki menüler arasında hareket eder.</li>\\n' +\n  '  <li>menüde bir alt menü açar.</li>\\n' +\n  '  <li>araç çubuğu grubundaki düğmeler arasında hareket eder.</li>\\n' +\n  '  <li>alt bilginin öğe yolundaki öğeler arasında hareket eder.</li>\\n' +\n  '</ul>\\n' +\n  '\\n' +\n  '<p><strong>Aşağı</strong> ve <strong>Yukarı</strong> ok tuşları</p>\\n' +\n  '\\n' +\n  '<ul>\\n' +\n  '  <li>menüdeki menü öğeleri arasında hareket eder.</li>\\n' +\n  '  <li>araç çubuğu açılır menüsündeki öğeler arasında hareket eder.</li>\\n' +\n  '</ul>\\n' +\n  '\\n' +\n  '<p><strong>Ok</strong> tuşları, odaklanılan kullanıcı arabirimi bölümü içinde döngüsel olarak hareket eder.</p>\\n' +\n  '\\n' +\n  '<p>Açık bir menüyü, açık bir alt menüyü veya açık bir açılır menüyü kapatmak için <strong>Esc</strong> tuşuna basın.</p>\\n' +\n  '\\n' +\n  '<p>Geçerli odak belirli bir kullanıcı arabirimi bölümünün \"üst\" kısmındaysa <strong>Esc</strong> tuşuna basıldığında\\n' +\n  '  klavyeyle gezintiden de tamamen çıkılır.</p>\\n' +\n  '\\n' +\n  '<h1>Menü öğesini veya araç çubuğu düğmesini yürütme</h1>\\n' +\n  '\\n' +\n  '<p>İstediğiniz menü öğesi veya araç çubuğu düğmesi vurgulandığında <strong>Return</strong>, <strong>Enter</strong>\\n' +\n  '  veya <strong>Ara çubuğu</strong> tuşuna basın.</p>\\n' +\n  '\\n' +\n  '<h1>Sekme bulunmayan iletişim kutularında gezinme</h1>\\n' +\n  '\\n' +\n  '<p>Sekme bulunmayan iletişim kutularında, iletişim kutusu açıldığında ilk etkileşimli bileşene odaklanılır.</p>\\n' +\n  '\\n' +\n  '<p>Etkileşimli iletişim kutusu bileşenleri arasında gezinmek için <strong>Sekme</strong> veya <strong>Shift+ Sekme</strong> tuşlarına basın.</p>\\n' +\n  '\\n' +\n  '<h1>Sekmeli iletişim kutularında gezinme</h1>\\n' +\n  '\\n' +\n  '<p>Sekmeli iletişim kutularında, iletişim kutusu açıldığında sekme menüsündeki ilk düğmeye odaklanılır.</p>\\n' +\n  '\\n' +\n  '<p>Bu iletişim kutusu sekmesinin etkileşimli bileşenleri arasında gezinmek için <strong>Sekme</strong> veya\\n' +\n  '  <strong>Shift+Sekme</strong> tuşlarına basın.</p>\\n' +\n  '\\n' +\n  '<p>Mevcut sekmeler arasında geçiş yapmak için sekme menüsüne odaklanıp uygun <strong>Ok</strong> tuşuna basarak\\n' +\n  '  başka bir iletişim kutusu sekmesine geçiş yapın.</p>\\n');"
  },
  {
    "path": "apps/web-antd/public/tinymce/plugins/help/js/i18n/keynav/uk.js",
    "content": "tinymce.Resource.add('tinymce.html-i18n.help-keynav.uk',\n'<h1>Початок роботи з навігацією за допомогою клавіатури</h1>\\n' +\n  '\\n' +\n  '<dl>\\n' +\n  '  <dt>Фокус на рядок меню</dt>\\n' +\n  '  <dd>Windows або Linux: Alt+F9</dd>\\n' +\n  '  <dd>macOS: &#x2325;F9</dd>\\n' +\n  '  <dt>Фокус на панелі інструментів</dt>\\n' +\n  '  <dd>Windows або Linux: Alt+F10</dd>\\n' +\n  '  <dd>macOS: &#x2325;F10</dd>\\n' +\n  '  <dt>Фокус на розділі \"Нижній колонтитул\"</dt>\\n' +\n  '  <dd>Windows або Linux: Alt+F11</dd>\\n' +\n  '  <dd>macOS: &#x2325;F11</dd>\\n' +\n  '  <dt>Фокус на сповіщення</dt>\\n' +\n  '  <dd>Windows або Linux: Alt+F12</dd>\\n' +\n  '  <dd>macOS: &#x2325;F12</dd>\\n' +\n  '  <dt>Фокус на контекстній панелі інструментів</dt>\\n' +\n  '  <dd>Windows, Linux або macOS: Ctrl+F9</dd>\\n' +\n  '</dl>\\n' +\n  '\\n' +\n  '<p>Навігація почнеться з першого елемента інтерфейсу користувача, який буде виділено або підкреслено в разі, якщо перший елемент знаходиться в\\n' +\n  '  шляху до елемента \"Нижній колонтитул\".</p>\\n' +\n  '\\n' +\n  '<h1>Навігація між розділами інтерфейсу користувача</h1>\\n' +\n  '\\n' +\n  '<p>Щоб перейти з одного розділу інтерфейсу користувача до наступного розділу, натисніть клавішу <strong>Tab</strong>.</p>\\n' +\n  '\\n' +\n  '<p>Щоб перейти з одного розділу інтерфейсу користувача до попереднього розділу, натисніть сполучення клавіш <strong>Shift+Tab</strong>.</p>\\n' +\n  '\\n' +\n  '<p>Порядок <strong>Вкладок</strong> цих розділів інтерфейсу користувача такий:</p>\\n' +\n  '\\n' +\n  '<ol>\\n' +\n  '  <li>Рядок меню</li>\\n' +\n  '  <li>Кожна група панелей інструментів</li>\\n' +\n  '  <li>Бічна панель</li>\\n' +\n  '  <li>Шлях до елементів у розділі \"Нижній колонтитул\"</li>\\n' +\n  '  <li>Кнопка перемикача \"Кількість слів\" у розділі \"Нижній колонтитул\"</li>\\n' +\n  '  <li>Посилання на брендинг у розділі \"Нижній колонтитул\"</li>\\n' +\n  '  <li>Маркер змінення розміру в розділі \"Нижній колонтитул\"</li>\\n' +\n  '</ol>\\n' +\n  '\\n' +\n  '<p>Якщо розділ інтерфейсу користувача відсутній, він пропускається.</p>\\n' +\n  '\\n' +\n  '<p>Якщо фокус навігації клавіатури знаходиться на розділі \"Нижній колонтитул\", але користувач не бачить видиму бічну панель, натисніть <strong>Shift+Tab</strong>,\\n' +\n  '  щоб перемістити фокус на першу групу панелі інструментів, а не на останню.</p>\\n' +\n  '\\n' +\n  '<h1>Навігація в межах розділів інтерфейсу користувача</h1>\\n' +\n  '\\n' +\n  '<p>Щоб перейти з одного елементу інтерфейсу користувача до наступного, натисніть відповідну клавішу <strong>зі стрілкою</strong>.</p>\\n' +\n  '\\n' +\n  '<p>Клавіші зі стрілками <strong>Ліворуч</strong> і <strong>Праворуч</strong></p>\\n' +\n  '\\n' +\n  '<ul>\\n' +\n  '  <li>переміщують між меню в рядку меню.</li>\\n' +\n  '  <li>відкривають вкладене меню в меню.</li>\\n' +\n  '  <li>переміщують користувача між кнопками в групі панелі інструментів.</li>\\n' +\n  '  <li>переміщують між елементами в шляху до елементів у розділі \"Нижній колонтитул\".</li>\\n' +\n  '</ul>\\n' +\n  '\\n' +\n  '<p>Клавіші зі стрілками <strong>Вниз</strong> і <strong>Вгору</strong></p>\\n' +\n  '\\n' +\n  '<ul>\\n' +\n  '  <li>переміщують між елементами меню в меню.</li>\\n' +\n  '  <li>переміщують між елементами в спливаючому меню панелі інструментів.</li>\\n' +\n  '</ul>\\n' +\n  '\\n' +\n  '<p>Клавіші <strong>зі стрілками</strong> переміщують фокус циклічно в межах розділу інтерфейсу користувача, на якому знаходиться фокус.</p>\\n' +\n  '\\n' +\n  '<p>Щоб закрити відкрите меню, відкрите вкладене меню або відкрите спливаюче меню, натисніть клавішу <strong>Esc</strong>.</p>\\n' +\n  '\\n' +\n  '<p>Якщо поточний фокус знаходиться на верхньому рівні певного розділу інтерфейсу користувача, натискання клавіші <strong>Esc</strong> також виконує вихід\\n' +\n  '  з навігації за допомогою клавіатури повністю.</p>\\n' +\n  '\\n' +\n  '<h1>Виконання елементу меню або кнопки панелі інструментів</h1>\\n' +\n  '\\n' +\n  '<p>Коли потрібний елемент меню або кнопку панелі інструментів виділено, натисніть клавіші <strong>Return</strong>, <strong>Enter</strong>,\\n' +\n  '  або <strong>Пробіл</strong>, щоб виконати цей елемент.</p>\\n' +\n  '\\n' +\n  '<h1>Навігація по діалоговим вікнам без вкладок</h1>\\n' +\n  '\\n' +\n  '<p>У діалогових вікнах без вкладок перший інтерактивний компонент приймає фокус, коли відкривається діалогове вікно.</p>\\n' +\n  '\\n' +\n  '<p>Переходьте між інтерактивними компонентами діалогового вікна, натискаючи клавіші <strong>Tab</strong> або <strong>Shift+Tab</strong>.</p>\\n' +\n  '\\n' +\n  '<h1>Навігація по діалоговим вікнам з вкладками</h1>\\n' +\n  '\\n' +\n  '<p>У діалогових вікнах із вкладками перша кнопка в меню вкладки приймає фокус, коли відкривається діалогове вікно.</p>\\n' +\n  '\\n' +\n  '<p>Переходьте між інтерактивними компонентами цієї вкладки діалогового вікна, натискаючи клавіші <strong>Tab</strong> або\\n' +\n  '  <strong>Shift+Tab</strong>.</p>\\n' +\n  '\\n' +\n  '<p>Щоб перейти на іншу вкладку діалогового вікна, перемістіть фокус на меню вкладки, а потім натисніть відповідну клавішу <strong>зі стрілкою</strong>,\\n' +\n  '  щоб циклічно переходити по доступним вкладкам.</p>\\n');"
  },
  {
    "path": "apps/web-antd/public/tinymce/plugins/help/js/i18n/keynav/vi.js",
    "content": "tinymce.Resource.add('tinymce.html-i18n.help-keynav.vi',\n'<h1>Bắt đầu điều hướng bàn phím</h1>\\n' +\n  '\\n' +\n  '<dl>\\n' +\n  '  <dt>Tập trung vào thanh menu</dt>\\n' +\n  '  <dd>Windows hoặc Linux: Alt+F9</dd>\\n' +\n  '  <dd>macOS: &#x2325;F9</dd>\\n' +\n  '  <dt>Tập trung vào thanh công cụ</dt>\\n' +\n  '  <dd>Windows hoặc Linux: Alt+F10</dd>\\n' +\n  '  <dd>macOS: &#x2325;F10</dd>\\n' +\n  '  <dt>Tập trung vào chân trang</dt>\\n' +\n  '  <dd>Windows hoặc Linux: Alt+F11</dd>\\n' +\n  '  <dd>macOS: &#x2325;F11</dd>\\n' +\n  '  <dt>Tập trung vào thông báo</dt>\\n' +\n  '  <dd>Windows hoặc Linux: Alt+F12</dd>\\n' +\n  '  <dd>macOS: &#x2325;F12</dd>\\n' +\n  '  <dt>Tập trung vào thanh công cụ ngữ cảnh</dt>\\n' +\n  '  <dd>Windows, Linux hoặc macOS: Ctrl+F9</dd>\\n' +\n  '</dl>\\n' +\n  '\\n' +\n  '<p>Điều hướng sẽ bắt đầu từ mục UI đầu tiên. Mục này sẽ được tô sáng hoặc có gạch dưới (nếu là mục đầu tiên trong\\n' +\n  '  đường dẫn phần tử Chân trang).</p>\\n' +\n  '\\n' +\n  '<h1>Di chuyển qua lại giữa các phần UI</h1>\\n' +\n  '\\n' +\n  '<p>Để di chuyển từ một phần UI sang phần tiếp theo, ấn <strong>Tab</strong>.</p>\\n' +\n  '\\n' +\n  '<p>Để di chuyển từ một phần UI về phần trước đó, ấn <strong>Shift+Tab</strong>.</p>\\n' +\n  '\\n' +\n  '<p>Thứ tự <strong>Tab</strong> của các phần UI này như sau:</p>\\n' +\n  '\\n' +\n  '<ol>\\n' +\n  '  <li>Thanh menu</li>\\n' +\n  '  <li>Từng nhóm thanh công cụ</li>\\n' +\n  '  <li>Thanh bên</li>\\n' +\n  '  <li>Đường dẫn phần tử trong chân trang</li>\\n' +\n  '  <li>Nút chuyển đổi đếm chữ ở chân trang</li>\\n' +\n  '  <li>Liên kết thương hiệu ở chân trang</li>\\n' +\n  '  <li>Núm điều tác chỉnh kích cỡ trình soạn thảo ở chân trang</li>\\n' +\n  '</ol>\\n' +\n  '\\n' +\n  '<p>Nếu người dùng không thấy một phần UI, thì có nghĩa phần đó bị bỏ qua.</p>\\n' +\n  '\\n' +\n  '<p>Nếu ở chân trang có tính năng tập trung điều hướng bàn phím, mà không có thanh bên nào hiện hữu, thao tác ấn <strong>Shift+Tab</strong>\\n' +\n  '  sẽ chuyển hướng tập trung vào nhóm thanh công cụ đầu tiên, không phải cuối cùng.</p>\\n' +\n  '\\n' +\n  '<h1>Di chuyển qua lại trong các phần UI</h1>\\n' +\n  '\\n' +\n  '<p>Để di chuyển từ một phần tử UI sang phần tiếp theo, ấn phím <strong>Mũi tên</strong> tương ứng cho phù hợp.</p>\\n' +\n  '\\n' +\n  '<p>Các phím mũi tên <strong>Trái</strong> và <strong>Phải</strong></p>\\n' +\n  '\\n' +\n  '<ul>\\n' +\n  '  <li>di chuyển giữa các menu trong thanh menu.</li>\\n' +\n  '  <li>mở menu phụ trong một menu.</li>\\n' +\n  '  <li>di chuyển giữa các nút trong nhóm thanh công cụ.</li>\\n' +\n  '  <li>di chuyển giữa các mục trong đường dẫn phần tử của chân trang.</li>\\n' +\n  '</ul>\\n' +\n  '\\n' +\n  '<p>Các phím mũi tên <strong>Hướng xuống</strong> và <strong>Hướng lên</strong></p>\\n' +\n  '\\n' +\n  '<ul>\\n' +\n  '  <li>di chuyển giữa các mục menu trong menu.</li>\\n' +\n  '  <li>di chuyển giữa các mục trong menu thanh công cụ dạng bật lên.</li>\\n' +\n  '</ul>\\n' +\n  '\\n' +\n  '<p>Các phím <strong>mũi tên</strong> xoay vòng trong một phần UI tập trung.</p>\\n' +\n  '\\n' +\n  '<p>Để đóng một menu mở, một menu phụ đang mở, hoặc một menu dạng bật lên đang mở, hãy ấn phím <strong>Esc</strong>.</p>\\n' +\n  '\\n' +\n  '<p>Nếu trọng tâm hiện tại là ở phần “đầu” của một phần UI cụ thể, thao tác ấn phím <strong>Esc</strong> cũng sẽ thoát\\n' +\n  '  toàn bộ phần điều hướng bàn phím.</p>\\n' +\n  '\\n' +\n  '<h1>Thực hiện chức năng của một mục menu hoặc nút thanh công cụ</h1>\\n' +\n  '\\n' +\n  '<p>Khi mục menu hoặc nút thanh công cụ muốn dùng được tô sáng, hãy ấn <strong>Return</strong>, <strong>Enter</strong>,\\n' +\n  '  hoặc <strong>Phím cách</strong> để thực hiện chức năng mục đó.</p>\\n' +\n  '\\n' +\n  '<h1>Điều hướng giữa các hộp thoại không có nhiều tab</h1>\\n' +\n  '\\n' +\n  '<p>Trong các hộp thoại không có nhiều tab, khi hộp thoại mở ra, trọng tâm sẽ hướng vào thành phần tương tác đầu tiên.</p>\\n' +\n  '\\n' +\n  '<p>Di chuyển giữa các thành phần hộp thoại tương tác bằng cách ấn <strong>Tab</strong> hoặc <strong>Shift+Tab</strong>.</p>\\n' +\n  '\\n' +\n  '<h1>Điều hướng giữa các hộp thoại có nhiều tab</h1>\\n' +\n  '\\n' +\n  '<p>Trong các hộp thoại có nhiều tab, khi hộp thoại mở ra, trọng tâm sẽ hướng vào nút đầu tiên trong menu tab.</p>\\n' +\n  '\\n' +\n  '<p>Di chuyển giữa các thành phần tương tác của tab hộp thoại này bằng cách ấn <strong>Tab</strong> hoặc\\n' +\n  '  <strong>Shift+Tab</strong>.</p>\\n' +\n  '\\n' +\n  '<p>Chuyển sang một tab hộp thoại khác bằng cách chuyển trọng tâm vào menu tab, rồi ấn phím <strong>Mũi tên</strong> phù hợp\\n' +\n  '  để xoay vòng các tab hiện có.</p>\\n');"
  },
  {
    "path": "apps/web-antd/public/tinymce/plugins/help/js/i18n/keynav/zh_CN.js",
    "content": "tinymce.Resource.add('tinymce.html-i18n.help-keynav.zh_CN',\n'<h1>开始键盘导航</h1>\\n' +\n  '\\n' +\n  '<dl>\\n' +\n  '  <dt>使菜单栏处于焦点</dt>\\n' +\n  '  <dd>Windows 或 Linux：Alt+F9</dd>\\n' +\n  '  <dd>macOS：&#x2325;F9</dd>\\n' +\n  '  <dt>使工具栏处于焦点</dt>\\n' +\n  '  <dd>Windows 或 Linux：Alt+F10</dd>\\n' +\n  '  <dd>macOS：&#x2325;F10</dd>\\n' +\n  '  <dt>使页脚处于焦点</dt>\\n' +\n  '  <dd>Windows 或 Linux：Alt+F11</dd>\\n' +\n  '  <dd>macOS：&#x2325;F11</dd>\\n' +\n  '  <dt>使通知处于焦点</dt>\\n' +\n  '  <dd>Windows 或 Linux：Alt+F12</dd>\\n' +\n  '  <dd>macOS：&#x2325;F12</dd>\\n' +\n  '  <dt>使上下文工具栏处于焦点</dt>\\n' +\n  '  <dd>Windows、Linux 或 macOS：Ctrl+F9</dd>\\n' +\n  '</dl>\\n' +\n  '\\n' +\n  '<p>导航将在第一个 UI 项上开始，其中突出显示该项，或者对于页脚元素路径中的第一项，将为其添加下划线。</p>\\n' +\n  '\\n' +\n  '<h1>在 UI 部分之间导航</h1>\\n' +\n  '\\n' +\n  '<p>要从一个 UI 部分移至下一个，请按 <strong>Tab</strong>。</p>\\n' +\n  '\\n' +\n  '<p>要从一个 UI 部分移至上一个，请按 <strong>Shift+Tab</strong>。</p>\\n' +\n  '\\n' +\n  '<p>这些 UI 部分的 <strong>Tab</strong> 顺序为：</p>\\n' +\n  '\\n' +\n  '<ol>\\n' +\n  '  <li>菜单栏</li>\\n' +\n  '  <li>每个工具栏组</li>\\n' +\n  '  <li>边栏</li>\\n' +\n  '  <li>页脚中的元素路径</li>\\n' +\n  '  <li>页脚中的字数切换按钮</li>\\n' +\n  '  <li>页脚中的品牌链接</li>\\n' +\n  '  <li>页脚中的编辑器调整大小图柄</li>\\n' +\n  '</ol>\\n' +\n  '\\n' +\n  '<p>如果不存在某个 UI 部分，则跳过它。</p>\\n' +\n  '\\n' +\n  '<p>如果键盘导航焦点在页脚，并且没有可见的边栏，则按 <strong>Shift+Tab</strong> 将焦点移至第一个工具栏组而非最后一个。</p>\\n' +\n  '\\n' +\n  '<h1>在 UI 部分内导航</h1>\\n' +\n  '\\n' +\n  '<p>要从一个 UI 元素移至下一个，请按相应的<strong>箭头</strong>键。</p>\\n' +\n  '\\n' +\n  '<p><strong>左</strong>和<strong>右</strong>箭头键</p>\\n' +\n  '\\n' +\n  '<ul>\\n' +\n  '  <li>在菜单栏中的菜单之间移动。</li>\\n' +\n  '  <li>打开菜单中的子菜单。</li>\\n' +\n  '  <li>在工具栏组中的按钮之间移动。</li>\\n' +\n  '  <li>在页脚的元素路径中的各项之间移动。</li>\\n' +\n  '</ul>\\n' +\n  '\\n' +\n  '<p><strong>下</strong>和<strong>上</strong>箭头键</p>\\n' +\n  '\\n' +\n  '<ul>\\n' +\n  '  <li>在菜单中的菜单项之间移动。</li>\\n' +\n  '  <li>在工具栏弹出菜单中的各项之间移动。</li>\\n' +\n  '</ul>\\n' +\n  '\\n' +\n  '<p><strong>箭头</strong>键在具有焦点的 UI 部分内循环。</p>\\n' +\n  '\\n' +\n  '<p>要关闭打开的菜单、打开的子菜单或打开的弹出菜单，请按 <strong>Esc</strong> 键。</p>\\n' +\n  '\\n' +\n  '<p>如果当前的焦点在特定 UI 部分的“顶部”，则按 <strong>Esc</strong> 键还将完全退出键盘导航。</p>\\n' +\n  '\\n' +\n  '<h1>执行菜单项或工具栏按钮</h1>\\n' +\n  '\\n' +\n  '<p>当突出显示所需的菜单项或工具栏按钮时，按 <strong>Return</strong>、<strong>Enter</strong> 或<strong>空格</strong>以执行该项。</p>\\n' +\n  '\\n' +\n  '<h1>在非标签页式对话框中导航</h1>\\n' +\n  '\\n' +\n  '<p>在非标签页式对话框中，当对话框打开时，第一个交互组件获得焦点。</p>\\n' +\n  '\\n' +\n  '<p>通过按 <strong>Tab</strong> 或 <strong>Shift+Tab</strong>，在交互对话框组件之间导航。</p>\\n' +\n  '\\n' +\n  '<h1>在标签页式对话框中导航</h1>\\n' +\n  '\\n' +\n  '<p>在标签页式对话框中，当对话框打开时，标签页菜单中的第一个按钮获得焦点。</p>\\n' +\n  '\\n' +\n  '<p>通过按 <strong>Tab</strong> 或 <strong>Shift+Tab</strong>，在此对话框的交互组件之间导航。</p>\\n' +\n  '\\n' +\n  '<p>通过将焦点移至另一对话框标签页的菜单，然后按相应的<strong>箭头</strong>键以在可用的标签页间循环，从而切换到该对话框标签页。</p>\\n');"
  },
  {
    "path": "apps/web-antd/public/tinymce/plugins/help/js/i18n/keynav/zh_TW.js",
    "content": "tinymce.Resource.add('tinymce.html-i18n.help-keynav.zh_TW',\n'<h1>開始鍵盤瀏覽</h1>\\n' +\n  '\\n' +\n  '<dl>\\n' +\n  '  <dt>跳至功能表列</dt>\\n' +\n  '  <dd>Windows 或 Linux：Alt+F9</dd>\\n' +\n  '  <dd>macOS：&#x2325;F9</dd>\\n' +\n  '  <dt>跳至工具列</dt>\\n' +\n  '  <dd>Windows 或 Linux：Alt+F10</dd>\\n' +\n  '  <dd>macOS：&#x2325;F10</dd>\\n' +\n  '  <dt>跳至頁尾</dt>\\n' +\n  '  <dd>Windows 或 Linux：Alt+F11</dd>\\n' +\n  '  <dd>macOS：&#x2325;F11</dd>\\n' +\n  '  <dt>跳至通知</dt>\\n' +\n  '  <dd>Windows 或 Linux：Alt+F12</dd>\\n' +\n  '  <dd>macOS：&#x2325;F12</dd>\\n' +\n  '  <dt>跳至關聯式工具列</dt>\\n' +\n  '  <dd>Windows、Linux 或 macOS：Ctrl+F9</dd>\\n' +\n  '</dl>\\n' +\n  '\\n' +\n  '<p>瀏覽會從第一個 UI 項目開始，該項目會反白顯示，但如果是「頁尾」元素路徑的第一項，\\n' +\n  '  則加底線。</p>\\n' +\n  '\\n' +\n  '<h1>在 UI 區段之間瀏覽</h1>\\n' +\n  '\\n' +\n  '<p>從 UI 區段移至下一個，請按 <strong>Tab</strong>。</p>\\n' +\n  '\\n' +\n  '<p>從 UI 區段移回上一個，請按 <strong>Shift+Tab</strong>。</p>\\n' +\n  '\\n' +\n  '<p>這些 UI 區段的 <strong>Tab</strong> 順序如下：</p>\\n' +\n  '\\n' +\n  '<ol>\\n' +\n  '  <li>功能表列</li>\\n' +\n  '  <li>各個工具列群組</li>\\n' +\n  '  <li>側邊欄</li>\\n' +\n  '  <li>頁尾中的元素路徑</li>\\n' +\n  '  <li>頁尾中字數切換按鈕</li>\\n' +\n  '  <li>頁尾中的品牌連結</li>\\n' +\n  '  <li>頁尾中編輯器調整大小控點</li>\\n' +\n  '</ol>\\n' +\n  '\\n' +\n  '<p>如果 UI 區段未顯示，表示已略過該區段。</p>\\n' +\n  '\\n' +\n  '<p>如果鍵盤瀏覽跳至頁尾，但沒有顯示側邊欄，則按下 <strong>Shift+Tab</strong>\\n' +\n  '  會跳至第一個工具列群組，而不是最後一個。</p>\\n' +\n  '\\n' +\n  '<h1>在 UI 區段之內瀏覽</h1>\\n' +\n  '\\n' +\n  '<p>在兩個 UI 元素之間移動，請按適當的<strong>方向</strong>鍵。</p>\\n' +\n  '\\n' +\n  '<p><strong>向左</strong>和<strong>向右</strong>方向鍵</p>\\n' +\n  '\\n' +\n  '<ul>\\n' +\n  '  <li>在功能表列中的功能表之間移動。</li>\\n' +\n  '  <li>開啟功能表中的子功能表。</li>\\n' +\n  '  <li>在工具列群組中的按鈕之間移動。</li>\\n' +\n  '  <li>在頁尾的元素路徑中項目之間移動。</li>\\n' +\n  '</ul>\\n' +\n  '\\n' +\n  '<p><strong>向下</strong>和<strong>向上</strong>方向鍵</p>\\n' +\n  '\\n' +\n  '<ul>\\n' +\n  '  <li>在功能表中的功能表項目之間移動。</li>\\n' +\n  '  <li>在工具列快顯功能表中的項目之間移動。</li>\\n' +\n  '</ul>\\n' +\n  '\\n' +\n  '<p><strong>方向</strong>鍵會在所跳至 UI 區段之內循環。</p>\\n' +\n  '\\n' +\n  '<p>若要關閉已開啟的功能表、已開啟的子功能表，或已開啟的快顯功能表，請按 <strong>Esc</strong> 鍵。</p>\\n' +\n  '\\n' +\n  '<p>如果目前已跳至特定 UI 區段的「頂端」，則按 <strong>Esc</strong> 鍵也會結束\\n' +\n  '  整個鍵盤瀏覽。</p>\\n' +\n  '\\n' +\n  '<h1>執行功能表列項目或工具列按鈕</h1>\\n' +\n  '\\n' +\n  '<p>當想要的功能表項目或工具列按鈕已反白顯示時，按 <strong>Return</strong>、<strong>Enter</strong>、\\n' +\n  '  或<strong>空白鍵</strong>即可執行該項目。</p>\\n' +\n  '\\n' +\n  '<h1>瀏覽非索引標籤式對話方塊</h1>\\n' +\n  '\\n' +\n  '<p>在非索引標籤式對話方塊中，開啟對話方塊時會跳至第一個互動元件。</p>\\n' +\n  '\\n' +\n  '<p>按 <strong>Tab</strong> 或 <strong>Shift+Tab</strong> 即可在互動式對話方塊元件之間瀏覽。</p>\\n' +\n  '\\n' +\n  '<h1>瀏覽索引標籤式對話方塊</h1>\\n' +\n  '\\n' +\n  '<p>在索引標籤式對話方塊中，開啟對話方塊時會跳至索引標籤式功能表中的第一個按鈕。</p>\\n' +\n  '\\n' +\n  '<p>若要在此對話方塊的互動式元件之間瀏覽，請按 <strong>Tab</strong> 或\\n' +\n  '  <strong>Shift+Tab</strong>。</p>\\n' +\n  '\\n' +\n  '<p>先跳至索引標籤式功能表，然後按適當的<strong>方向</strong>鍵，即可切換至另一個對話方塊索引標籤，\\n' +\n  '  以循環瀏覽可用的索引標籤。</p>\\n');"
  },
  {
    "path": "apps/web-antd/public/tinymce/skins/content/dark/content.js",
    "content": "tinymce.Resource.add('content/dark/content.css', \"body{background-color:#222f3e;color:#fff;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen,Ubuntu,Cantarell,'Open Sans','Helvetica Neue',sans-serif;line-height:1.4;margin:1rem}a{color:#4099ff}table{border-collapse:collapse}table:not([cellpadding]) td,table:not([cellpadding]) th{padding:.4rem}table[border]:not([border=\\\"0\\\"]):not([style*=border-width]) td,table[border]:not([border=\\\"0\\\"]):not([style*=border-width]) th{border-width:1px}table[border]:not([border=\\\"0\\\"]):not([style*=border-style]) td,table[border]:not([border=\\\"0\\\"]):not([style*=border-style]) th{border-style:solid}table[border]:not([border=\\\"0\\\"]):not([style*=border-color]) td,table[border]:not([border=\\\"0\\\"]):not([style*=border-color]) th{border-color:#6d737b}figure{display:table;margin:1rem auto}figure figcaption{color:#8a8f97;display:block;margin-top:.25rem;text-align:center}hr{border-color:#6d737b;border-style:solid;border-width:1px 0 0 0}code{background-color:#6d737b;border-radius:3px;padding:.1rem .2rem}.mce-content-body:not([dir=rtl]) blockquote{border-left:2px solid #6d737b;margin-left:1.5rem;padding-left:1rem}.mce-content-body[dir=rtl] blockquote{border-right:2px solid #6d737b;margin-right:1.5rem;padding-right:1rem}\")\n//# sourceMappingURL=content.js.map\n"
  },
  {
    "path": "apps/web-antd/public/tinymce/skins/content/default/content.js",
    "content": "tinymce.Resource.add('content/default/content.css', \"body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen,Ubuntu,Cantarell,'Open Sans','Helvetica Neue',sans-serif;line-height:1.4;margin:1rem}table{border-collapse:collapse}table:not([cellpadding]) td,table:not([cellpadding]) th{padding:.4rem}table[border]:not([border=\\\"0\\\"]):not([style*=border-width]) td,table[border]:not([border=\\\"0\\\"]):not([style*=border-width]) th{border-width:1px}table[border]:not([border=\\\"0\\\"]):not([style*=border-style]) td,table[border]:not([border=\\\"0\\\"]):not([style*=border-style]) th{border-style:solid}table[border]:not([border=\\\"0\\\"]):not([style*=border-color]) td,table[border]:not([border=\\\"0\\\"]):not([style*=border-color]) th{border-color:#ccc}figure{display:table;margin:1rem auto}figure figcaption{color:#999;display:block;margin-top:.25rem;text-align:center}hr{border-color:#ccc;border-style:solid;border-width:1px 0 0 0}code{background-color:#e8e8e8;border-radius:3px;padding:.1rem .2rem}.mce-content-body:not([dir=rtl]) blockquote{border-left:2px solid #ccc;margin-left:1.5rem;padding-left:1rem}.mce-content-body[dir=rtl] blockquote{border-right:2px solid #ccc;margin-right:1.5rem;padding-right:1rem}\")\n//# sourceMappingURL=content.js.map\n"
  },
  {
    "path": "apps/web-antd/public/tinymce/skins/content/document/content.js",
    "content": "tinymce.Resource.add('content/document/content.css', \"@media screen{html{background:#f4f4f4;min-height:100%}}body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen,Ubuntu,Cantarell,'Open Sans','Helvetica Neue',sans-serif}@media screen{body{background-color:#fff;box-shadow:0 0 4px rgba(0,0,0,.15);box-sizing:border-box;margin:1rem auto 0;max-width:820px;min-height:calc(100vh - 1rem);padding:4rem 6rem 6rem 6rem}}table{border-collapse:collapse}table:not([cellpadding]) td,table:not([cellpadding]) th{padding:.4rem}table[border]:not([border=\\\"0\\\"]):not([style*=border-width]) td,table[border]:not([border=\\\"0\\\"]):not([style*=border-width]) th{border-width:1px}table[border]:not([border=\\\"0\\\"]):not([style*=border-style]) td,table[border]:not([border=\\\"0\\\"]):not([style*=border-style]) th{border-style:solid}table[border]:not([border=\\\"0\\\"]):not([style*=border-color]) td,table[border]:not([border=\\\"0\\\"]):not([style*=border-color]) th{border-color:#ccc}figure figcaption{color:#999;margin-top:.25rem;text-align:center}hr{border-color:#ccc;border-style:solid;border-width:1px 0 0 0}.mce-content-body:not([dir=rtl]) blockquote{border-left:2px solid #ccc;margin-left:1.5rem;padding-left:1rem}.mce-content-body[dir=rtl] blockquote{border-right:2px solid #ccc;margin-right:1.5rem;padding-right:1rem}\")\n//# sourceMappingURL=content.js.map\n"
  },
  {
    "path": "apps/web-antd/public/tinymce/skins/content/tinymce-5/content.js",
    "content": "tinymce.Resource.add('content/tinymce-5/content.css', \"body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen,Ubuntu,Cantarell,'Open Sans','Helvetica Neue',sans-serif;line-height:1.4;margin:1rem}table{border-collapse:collapse}table:not([cellpadding]) td,table:not([cellpadding]) th{padding:.4rem}table[border]:not([border=\\\"0\\\"]):not([style*=border-width]) td,table[border]:not([border=\\\"0\\\"]):not([style*=border-width]) th{border-width:1px}table[border]:not([border=\\\"0\\\"]):not([style*=border-style]) td,table[border]:not([border=\\\"0\\\"]):not([style*=border-style]) th{border-style:solid}table[border]:not([border=\\\"0\\\"]):not([style*=border-color]) td,table[border]:not([border=\\\"0\\\"]):not([style*=border-color]) th{border-color:#ccc}figure{display:table;margin:1rem auto}figure figcaption{color:#999;display:block;margin-top:.25rem;text-align:center}hr{border-color:#ccc;border-style:solid;border-width:1px 0 0 0}code{background-color:#e8e8e8;border-radius:3px;padding:.1rem .2rem}.mce-content-body:not([dir=rtl]) blockquote{border-left:2px solid #ccc;margin-left:1.5rem;padding-left:1rem}.mce-content-body[dir=rtl] blockquote{border-right:2px solid #ccc;margin-right:1.5rem;padding-right:1rem}\")\n//# sourceMappingURL=content.js.map\n"
  },
  {
    "path": "apps/web-antd/public/tinymce/skins/content/tinymce-5-dark/content.js",
    "content": "tinymce.Resource.add('content/tinymce-5-dark/content.css', \"body{background-color:#2f3742;color:#dfe0e4;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen,Ubuntu,Cantarell,'Open Sans','Helvetica Neue',sans-serif;line-height:1.4;margin:1rem}a{color:#4099ff}table{border-collapse:collapse}table:not([cellpadding]) td,table:not([cellpadding]) th{padding:.4rem}table[border]:not([border=\\\"0\\\"]):not([style*=border-width]) td,table[border]:not([border=\\\"0\\\"]):not([style*=border-width]) th{border-width:1px}table[border]:not([border=\\\"0\\\"]):not([style*=border-style]) td,table[border]:not([border=\\\"0\\\"]):not([style*=border-style]) th{border-style:solid}table[border]:not([border=\\\"0\\\"]):not([style*=border-color]) td,table[border]:not([border=\\\"0\\\"]):not([style*=border-color]) th{border-color:#6d737b}figure{display:table;margin:1rem auto}figure figcaption{color:#8a8f97;display:block;margin-top:.25rem;text-align:center}hr{border-color:#6d737b;border-style:solid;border-width:1px 0 0 0}code{background-color:#6d737b;border-radius:3px;padding:.1rem .2rem}.mce-content-body:not([dir=rtl]) blockquote{border-left:2px solid #6d737b;margin-left:1.5rem;padding-left:1rem}.mce-content-body[dir=rtl] blockquote{border-right:2px solid #6d737b;margin-right:1.5rem;padding-right:1rem}\")\n//# sourceMappingURL=content.js.map\n"
  },
  {
    "path": "apps/web-antd/public/tinymce/skins/content/writer/content.js",
    "content": "tinymce.Resource.add('content/writer/content.css', \"body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen,Ubuntu,Cantarell,'Open Sans','Helvetica Neue',sans-serif;line-height:1.4;margin:1rem auto;max-width:900px}table{border-collapse:collapse}table:not([cellpadding]) td,table:not([cellpadding]) th{padding:.4rem}table[border]:not([border=\\\"0\\\"]):not([style*=border-width]) td,table[border]:not([border=\\\"0\\\"]):not([style*=border-width]) th{border-width:1px}table[border]:not([border=\\\"0\\\"]):not([style*=border-style]) td,table[border]:not([border=\\\"0\\\"]):not([style*=border-style]) th{border-style:solid}table[border]:not([border=\\\"0\\\"]):not([style*=border-color]) td,table[border]:not([border=\\\"0\\\"]):not([style*=border-color]) th{border-color:#ccc}figure{display:table;margin:1rem auto}figure figcaption{color:#999;display:block;margin-top:.25rem;text-align:center}hr{border-color:#ccc;border-style:solid;border-width:1px 0 0 0}code{background-color:#e8e8e8;border-radius:3px;padding:.1rem .2rem}.mce-content-body:not([dir=rtl]) blockquote{border-left:2px solid #ccc;margin-left:1.5rem;padding-left:1rem}.mce-content-body[dir=rtl] blockquote{border-right:2px solid #ccc;margin-right:1.5rem;padding-right:1rem}\")\n//# sourceMappingURL=content.js.map\n"
  },
  {
    "path": "apps/web-antd/public/tinymce/skins/ui/oxide/content.inline.js",
    "content": "tinymce.Resource.add('ui/default/content.inline.css', \".mce-content-body .mce-item-anchor{background:transparent url(\\\"data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D'8'%20height%3D'12'%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%3E%3Cpath%20d%3D'M0%200L8%200%208%2012%204.09117821%209%200%2012z'%2F%3E%3C%2Fsvg%3E%0A\\\") no-repeat center}.mce-content-body .mce-item-anchor:empty{cursor:default;display:inline-block;height:12px!important;padding:0 2px;-webkit-user-modify:read-only;-moz-user-modify:read-only;-webkit-user-select:all;-moz-user-select:all;user-select:all;width:8px!important}.mce-content-body .mce-item-anchor:not(:empty){background-position-x:2px;display:inline-block;padding-left:12px}.mce-content-body .mce-item-anchor[data-mce-selected]{outline-offset:1px}.tox-comments-visible .tox-comment[contenteditable=false]:not([data-mce-selected]),.tox-comments-visible span.tox-comment img:not([data-mce-selected]),.tox-comments-visible span.tox-comment span.mce-preview-object:not([data-mce-selected]),.tox-comments-visible span.tox-comment>audio:not([data-mce-selected]),.tox-comments-visible span.tox-comment>video:not([data-mce-selected]){outline:3px solid #ffe89d}.tox-comments-visible .tox-comment[contenteditable=false][data-mce-annotation-active=true]:not([data-mce-selected]){outline:3px solid #fed635}.tox-comments-visible span.tox-comment[data-mce-annotation-active=true] img:not([data-mce-selected]),.tox-comments-visible span.tox-comment[data-mce-annotation-active=true] span.mce-preview-object:not([data-mce-selected]),.tox-comments-visible span.tox-comment[data-mce-annotation-active=true]>audio:not([data-mce-selected]),.tox-comments-visible span.tox-comment[data-mce-annotation-active=true]>video:not([data-mce-selected]){outline:3px solid #fed635}.tox-comments-visible span.tox-comment:not([data-mce-selected]){background-color:#ffe89d;outline:0}.tox-comments-visible span.tox-comment[data-mce-annotation-active=true]:not([data-mce-selected=inline-boundary]){background-color:#fed635}.tox-checklist>li:not(.tox-checklist--hidden){list-style:none;margin:.25em 0}.tox-checklist>li:not(.tox-checklist--hidden)::before{content:url(\\\"data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2216%22%20height%3D%2216%22%20viewBox%3D%220%200%2016%2016%22%3E%3Cg%20id%3D%22checklist-unchecked%22%20fill%3D%22none%22%20fill-rule%3D%22evenodd%22%3E%3Crect%20id%3D%22Rectangle%22%20width%3D%2215%22%20height%3D%2215%22%20x%3D%22.5%22%20y%3D%22.5%22%20fill-rule%3D%22nonzero%22%20stroke%3D%22%234C4C4C%22%20rx%3D%222%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E%0A\\\");cursor:pointer;height:1em;margin-left:-1.5em;margin-top:.125em;position:absolute;width:1em}.tox-checklist li:not(.tox-checklist--hidden).tox-checklist--checked::before{content:url(\\\"data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2216%22%20height%3D%2216%22%20viewBox%3D%220%200%2016%2016%22%3E%3Cg%20id%3D%22checklist-checked%22%20fill%3D%22none%22%20fill-rule%3D%22evenodd%22%3E%3Crect%20id%3D%22Rectangle%22%20width%3D%2216%22%20height%3D%2216%22%20fill%3D%22%234099FF%22%20fill-rule%3D%22nonzero%22%20rx%3D%222%22%2F%3E%3Cpath%20id%3D%22Path%22%20fill%3D%22%23FFF%22%20fill-rule%3D%22nonzero%22%20d%3D%22M11.5703186%2C3.14417309%20C11.8516238%2C2.73724603%2012.4164781%2C2.62829933%2012.83558%2C2.89774797%20C13.260121%2C3.17069355%2013.3759736%2C3.72932262%2013.0909105%2C4.14168582%20L7.7580587%2C11.8560195%20C7.43776896%2C12.3193404%206.76483983%2C12.3852142%206.35607322%2C11.9948725%20L3.02491697%2C8.8138662%20C2.66090143%2C8.46625845%202.65798871%2C7.89594698%203.01850234%2C7.54483354%20C3.373942%2C7.19866177%203.94940006%2C7.19592841%204.30829608%2C7.5386474%20L6.85276923%2C9.9684299%20L11.5703186%2C3.14417309%20Z%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E%0A\\\")}[dir=rtl] .tox-checklist>li:not(.tox-checklist--hidden)::before{margin-left:0;margin-right:-1.5em}code[class*=language-],pre[class*=language-]{color:#000;background:0 0;text-shadow:0 1px #fff;font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;tab-size:4;-webkit-hyphens:none;hyphens:none}code[class*=language-] ::-moz-selection,code[class*=language-]::-moz-selection,pre[class*=language-] ::-moz-selection,pre[class*=language-]::-moz-selection{text-shadow:none;background:#b3d4fc}code[class*=language-] ::selection,code[class*=language-]::selection,pre[class*=language-] ::selection,pre[class*=language-]::selection{text-shadow:none;background:#b3d4fc}@media print{code[class*=language-],pre[class*=language-]{text-shadow:none}}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto}:not(pre)>code[class*=language-],pre[class*=language-]{background:#f5f2f0}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#708090}.token.punctuation{color:#999}.token.namespace{opacity:.7}.token.boolean,.token.constant,.token.deleted,.token.number,.token.property,.token.symbol,.token.tag{color:#905}.token.attr-name,.token.builtin,.token.char,.token.inserted,.token.selector,.token.string{color:#690}.language-css .token.string,.style .token.string,.token.entity,.token.operator,.token.url{color:#9a6e3a;background:hsla(0,0%,100%,.5)}.token.atrule,.token.attr-value,.token.keyword{color:#07a}.token.class-name,.token.function{color:#dd4a68}.token.important,.token.regex,.token.variable{color:#e90}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}.mce-content-body{overflow-wrap:break-word;word-wrap:break-word}.mce-content-body .mce-visual-caret{background-color:#000;background-color:currentColor;position:absolute}.mce-content-body .mce-visual-caret-hidden{display:none}.mce-content-body [data-mce-caret]{left:-1000px;margin:0;padding:0;position:absolute;right:auto;top:0}.mce-content-body .mce-offscreen-selection{left:-2000000px;max-width:1000000px;position:absolute}.mce-content-body [contentEditable=false]{cursor:default}.mce-content-body [contentEditable=true]{cursor:text}.tox-cursor-format-painter{cursor:url(\\\"data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%20viewBox%3D%220%200%2024%2024%22%3E%0A%20%20%3Cg%20fill%3D%22none%22%20fill-rule%3D%22evenodd%22%3E%0A%20%20%20%20%3Cpath%20fill%3D%22%23000%22%20fill-rule%3D%22nonzero%22%20d%3D%22M15%2C6%20C15%2C5.45%2014.55%2C5%2014%2C5%20L6%2C5%20C5.45%2C5%205%2C5.45%205%2C6%20L5%2C10%20C5%2C10.55%205.45%2C11%206%2C11%20L14%2C11%20C14.55%2C11%2015%2C10.55%2015%2C10%20L15%2C9%20L16%2C9%20L16%2C12%20L9%2C12%20L9%2C19%20C9%2C19.55%209.45%2C20%2010%2C20%20L11%2C20%20C11.55%2C20%2012%2C19.55%2012%2C19%20L12%2C14%20L18%2C14%20L18%2C7%20L15%2C7%20L15%2C6%20Z%22%2F%3E%0A%20%20%20%20%3Cpath%20fill%3D%22%23000%22%20fill-rule%3D%22nonzero%22%20d%3D%22M1%2C1%20L8.25%2C1%20C8.66421356%2C1%209%2C1.33578644%209%2C1.75%20L9%2C1.75%20C9%2C2.16421356%208.66421356%2C2.5%208.25%2C2.5%20L2.5%2C2.5%20L2.5%2C8.25%20C2.5%2C8.66421356%202.16421356%2C9%201.75%2C9%20L1.75%2C9%20C1.33578644%2C9%201%2C8.66421356%201%2C8.25%20L1%2C1%20Z%22%2F%3E%0A%20%20%3C%2Fg%3E%0A%3C%2Fsvg%3E%0A\\\"),default}div.mce-footnotes hr{margin-inline-end:auto;margin-inline-start:0;width:25%}div.mce-footnotes li>a.mce-footnotes-backlink{text-decoration:none}@media print{sup.mce-footnote a{color:#000;text-decoration:none}div.mce-footnotes{break-inside:avoid;width:100%}div.mce-footnotes li>a.mce-footnotes-backlink{display:none}}tiny-math-block{display:flex;justify-content:center;margin:16px 0 16px 0}tiny-math-inline{display:inline-block}.mce-content-body figure.align-left{float:left}.mce-content-body figure.align-right{float:right}.mce-content-body figure.image.align-center{display:table;margin-left:auto;margin-right:auto}.mce-preview-object{border:1px solid gray;display:inline-block;line-height:0;margin:0 2px 0 2px;position:relative}.mce-preview-object .mce-shim{background:url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7);height:100%;left:0;position:absolute;top:0;width:100%}.mce-preview-object[data-mce-selected=\\\"2\\\"] .mce-shim{display:none}.mce-content-body .mce-mergetag{cursor:default!important;-webkit-user-select:none;-moz-user-select:none;user-select:none}.mce-content-body .mce-mergetag:hover{background-color:rgba(0,108,231,.1)}.mce-content-body .mce-mergetag-affix{background-color:rgba(0,108,231,.1);color:#006ce7}.mce-object{background:transparent url(\\\"data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%3E%3Cpath%20d%3D%22M4%203h16a1%201%200%200%201%201%201v16a1%201%200%200%201-1%201H4a1%201%200%200%201-1-1V4a1%201%200%200%201%201-1zm1%202v14h14V5H5zm4.79%202.565l5.64%204.028a.5.5%200%200%201%200%20.814l-5.64%204.028a.5.5%200%200%201-.79-.407V7.972a.5.5%200%200%201%20.79-.407z%22%2F%3E%3C%2Fsvg%3E%0A\\\") no-repeat center;border:1px dashed #aaa}.mce-pagebreak{border:1px dashed #aaa;cursor:default;display:block;height:5px;margin-top:15px;page-break-before:always;width:100%}@media print{.mce-pagebreak{border:0}}.tiny-pageembed .mce-shim{background:url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7);height:100%;left:0;position:absolute;top:0;width:100%}.tiny-pageembed[data-mce-selected=\\\"2\\\"] .mce-shim{display:none}.tiny-pageembed{display:inline-block;position:relative}.tiny-pageembed--16by9,.tiny-pageembed--1by1,.tiny-pageembed--21by9,.tiny-pageembed--4by3{display:block;overflow:hidden;padding:0;position:relative;width:100%}.tiny-pageembed--21by9{padding-top:42.857143%}.tiny-pageembed--16by9{padding-top:56.25%}.tiny-pageembed--4by3{padding-top:75%}.tiny-pageembed--1by1{padding-top:100%}.tiny-pageembed--16by9 iframe,.tiny-pageembed--1by1 iframe,.tiny-pageembed--21by9 iframe,.tiny-pageembed--4by3 iframe{border:0;height:100%;left:0;position:absolute;top:0;width:100%}.mce-content-body[data-mce-placeholder]{position:relative}.mce-content-body[data-mce-placeholder]:not(.mce-visualblocks)::before{color:rgba(34,47,62,.7);content:attr(data-mce-placeholder);position:absolute}@media (forced-colors:active){.mce-content-body[data-mce-placeholder]:not(.mce-visualblocks)::before{color:highlight;filter:brightness(30%);z-index:-1}}.mce-content-body:not([dir=rtl])[data-mce-placeholder]:not(.mce-visualblocks)::before{left:1px}.mce-content-body[dir=rtl][data-mce-placeholder]:not(.mce-visualblocks)::before{right:1px}.mce-content-body div.mce-resizehandle{background-color:#4099ff;border-color:#4099ff;border-style:solid;border-width:1px;box-sizing:border-box;height:10px;position:absolute;width:10px;z-index:1298}.mce-content-body div.mce-resizehandle:hover{background-color:#4099ff}.mce-content-body div.mce-resizehandle:nth-of-type(1){cursor:nwse-resize}.mce-content-body div.mce-resizehandle:nth-of-type(2){cursor:nesw-resize}.mce-content-body div.mce-resizehandle:nth-of-type(3){cursor:nwse-resize}.mce-content-body div.mce-resizehandle:nth-of-type(4){cursor:nesw-resize}.mce-content-body .mce-resize-backdrop{z-index:10000}.mce-content-body .mce-clonedresizable{cursor:default;opacity:.5;outline:1px dashed #000;position:absolute;z-index:10001}.mce-content-body .mce-clonedresizable.mce-resizetable-columns td,.mce-content-body .mce-clonedresizable.mce-resizetable-columns th{border:0}.mce-content-body .mce-resize-helper{background:#555;background:rgba(0,0,0,.75);border:1px;border-radius:3px;color:#fff;display:none;font-family:sans-serif;font-size:12px;line-height:14px;margin:5px 10px;padding:5px;position:absolute;white-space:nowrap;z-index:10002}.tox-rtc-user-selection{position:relative}.tox-rtc-user-cursor{bottom:0;cursor:default;position:absolute;top:0;width:2px}.tox-rtc-user-cursor::before{background-color:inherit;border-radius:50%;content:'';display:block;height:8px;position:absolute;right:-3px;top:-3px;width:8px}.tox-rtc-user-cursor:hover::after{background-color:inherit;border-radius:100px;box-sizing:border-box;color:#fff;content:attr(data-user);display:block;font-size:12px;font-weight:700;left:-5px;min-height:8px;min-width:8px;padding:0 12px;position:absolute;top:-11px;white-space:nowrap;z-index:1000}.tox-rtc-user-selection--1 .tox-rtc-user-cursor{background-color:#2dc26b}.tox-rtc-user-selection--2 .tox-rtc-user-cursor{background-color:#e03e2d}.tox-rtc-user-selection--3 .tox-rtc-user-cursor{background-color:#f1c40f}.tox-rtc-user-selection--4 .tox-rtc-user-cursor{background-color:#3598db}.tox-rtc-user-selection--5 .tox-rtc-user-cursor{background-color:#b96ad9}.tox-rtc-user-selection--6 .tox-rtc-user-cursor{background-color:#e67e23}.tox-rtc-user-selection--7 .tox-rtc-user-cursor{background-color:#aaa69d}.tox-rtc-user-selection--8 .tox-rtc-user-cursor{background-color:#f368e0}.tox-rtc-remote-image{background:#eaeaea url(\\\"data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%2236%22%20height%3D%2212%22%20viewBox%3D%220%200%2036%2012%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%3Ccircle%20cx%3D%226%22%20cy%3D%226%22%20r%3D%223%22%20fill%3D%22rgba(0%2C%200%2C%200%2C%20.2)%22%3E%0A%20%20%20%20%3Canimate%20attributeName%3D%22r%22%20values%3D%223%3B5%3B3%22%20calcMode%3D%22linear%22%20dur%3D%221s%22%20repeatCount%3D%22indefinite%22%20%2F%3E%0A%20%20%3C%2Fcircle%3E%0A%20%20%3Ccircle%20cx%3D%2218%22%20cy%3D%226%22%20r%3D%223%22%20fill%3D%22rgba(0%2C%200%2C%200%2C%20.2)%22%3E%0A%20%20%20%20%3Canimate%20attributeName%3D%22r%22%20values%3D%223%3B5%3B3%22%20calcMode%3D%22linear%22%20begin%3D%22.33s%22%20dur%3D%221s%22%20repeatCount%3D%22indefinite%22%20%2F%3E%0A%20%20%3C%2Fcircle%3E%0A%20%20%3Ccircle%20cx%3D%2230%22%20cy%3D%226%22%20r%3D%223%22%20fill%3D%22rgba(0%2C%200%2C%200%2C%20.2)%22%3E%0A%20%20%20%20%3Canimate%20attributeName%3D%22r%22%20values%3D%223%3B5%3B3%22%20calcMode%3D%22linear%22%20begin%3D%22.66s%22%20dur%3D%221s%22%20repeatCount%3D%22indefinite%22%20%2F%3E%0A%20%20%3C%2Fcircle%3E%0A%3C%2Fsvg%3E%0A\\\") no-repeat center center;border:1px solid #ccc;min-height:240px;min-width:320px}.mce-match-marker{background:#aaa;color:#fff}.mce-match-marker-selected{background:#39f;color:#fff}.mce-match-marker-selected::-moz-selection{background:#39f;color:#fff}.mce-match-marker-selected::selection{background:#39f;color:#fff}.mce-content-body audio[data-mce-selected],.mce-content-body details[data-mce-selected],.mce-content-body embed[data-mce-selected],.mce-content-body img[data-mce-selected],.mce-content-body object[data-mce-selected],.mce-content-body table[data-mce-selected],.mce-content-body video[data-mce-selected]{outline:3px solid #b4d7ff}.mce-content-body hr[data-mce-selected]{outline:3px solid #b4d7ff;outline-offset:1px}.mce-content-body [contentEditable=false] [contentEditable=true]:focus{outline:3px solid #b4d7ff}.mce-content-body [contentEditable=false] [contentEditable=true]:hover{outline:3px solid #b4d7ff}.mce-content-body [contentEditable=false][data-mce-selected]{cursor:not-allowed;outline:3px solid #b4d7ff}.mce-content-body.mce-content-readonly [contentEditable=true]:focus,.mce-content-body.mce-content-readonly [contentEditable=true]:hover{outline:0}.mce-content-body [data-mce-selected=inline-boundary]{background-color:#b4d7ff}.mce-content-body .mce-edit-focus{outline:3px solid #b4d7ff}.mce-content-body td[data-mce-selected],.mce-content-body th[data-mce-selected]{position:relative}.mce-content-body td[data-mce-selected]::-moz-selection,.mce-content-body th[data-mce-selected]::-moz-selection{background:0 0}.mce-content-body td[data-mce-selected]::selection,.mce-content-body th[data-mce-selected]::selection{background:0 0}.mce-content-body td[data-mce-selected] *,.mce-content-body th[data-mce-selected] *{outline:0;-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}.mce-content-body td[data-mce-selected]::after,.mce-content-body th[data-mce-selected]::after{background-color:rgba(180,215,255,.7);border:1px solid rgba(180,215,255,.7);bottom:-1px;content:'';left:-1px;mix-blend-mode:multiply;position:absolute;right:-1px;top:-1px}@media screen and (-ms-high-contrast:active),(-ms-high-contrast:none){.mce-content-body td[data-mce-selected]::after,.mce-content-body th[data-mce-selected]::after{border-color:rgba(0,84,180,.7)}}.mce-content-body img[data-mce-selected]::-moz-selection{background:0 0}.mce-content-body img[data-mce-selected]::selection{background:0 0}.ephox-snooker-resizer-bar{background-color:#b4d7ff;opacity:0;-webkit-user-select:none;-moz-user-select:none;user-select:none}.ephox-snooker-resizer-cols{cursor:col-resize}.ephox-snooker-resizer-rows{cursor:row-resize}.ephox-snooker-resizer-bar.ephox-snooker-resizer-bar-dragging{opacity:1}.mce-spellchecker-word{background-image:url(\\\"data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D'4'%20height%3D'4'%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%3E%3Cpath%20stroke%3D'%23ff0000'%20fill%3D'none'%20stroke-linecap%3D'round'%20stroke-opacity%3D'.75'%20d%3D'M0%203L2%201%204%203'%2F%3E%3C%2Fsvg%3E%0A\\\");background-position:0 calc(100% + 1px);background-repeat:repeat-x;background-size:auto 6px;cursor:default;height:2rem}.mce-spellchecker-grammar{background-image:url(\\\"data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D'4'%20height%3D'4'%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%3E%3Cpath%20stroke%3D'%2300A835'%20fill%3D'none'%20stroke-linecap%3D'round'%20d%3D'M0%203L2%201%204%203'%2F%3E%3C%2Fsvg%3E%0A\\\");background-position:0 calc(100% + 1px);background-repeat:repeat-x;background-size:auto 6px;cursor:default}.mce-toc{border:1px solid gray}.mce-toc h2{margin:4px}.mce-toc ul>li{list-style-type:none}[data-mce-block]{display:block}.mce-item-table:not([border]),.mce-item-table:not([border]) caption,.mce-item-table:not([border]) td,.mce-item-table:not([border]) th,.mce-item-table[border=\\\"0\\\"],.mce-item-table[border=\\\"0\\\"] caption,.mce-item-table[border=\\\"0\\\"] td,.mce-item-table[border=\\\"0\\\"] th,table[style*=\\\"border-width: 0px\\\"],table[style*=\\\"border-width: 0px\\\"] caption,table[style*=\\\"border-width: 0px\\\"] td,table[style*=\\\"border-width: 0px\\\"] th{border:1px dashed #bbb}.mce-visualblocks address,.mce-visualblocks article,.mce-visualblocks aside,.mce-visualblocks blockquote,.mce-visualblocks div:not([data-mce-bogus]),.mce-visualblocks dl,.mce-visualblocks figcaption,.mce-visualblocks figure,.mce-visualblocks h1,.mce-visualblocks h2,.mce-visualblocks h3,.mce-visualblocks h4,.mce-visualblocks h5,.mce-visualblocks h6,.mce-visualblocks hgroup,.mce-visualblocks ol,.mce-visualblocks p,.mce-visualblocks pre,.mce-visualblocks section,.mce-visualblocks ul{background-repeat:no-repeat;border:1px dashed #bbb;margin-left:3px;padding-top:10px}.mce-visualblocks p{background-image:url(data:image/gif;base64,R0lGODlhCQAJAJEAAAAAAP///7u7u////yH5BAEAAAMALAAAAAAJAAkAAAIQnG+CqCN/mlyvsRUpThG6AgA7)}.mce-visualblocks h1{background-image:url(data:image/gif;base64,R0lGODlhDQAKAIABALu7u////yH5BAEAAAEALAAAAAANAAoAAAIXjI8GybGu1JuxHoAfRNRW3TWXyF2YiRUAOw==)}.mce-visualblocks h2{background-image:url(data:image/gif;base64,R0lGODlhDgAKAIABALu7u////yH5BAEAAAEALAAAAAAOAAoAAAIajI8Hybbx4oOuqgTynJd6bGlWg3DkJzoaUAAAOw==)}.mce-visualblocks h3{background-image:url(data:image/gif;base64,R0lGODlhDgAKAIABALu7u////yH5BAEAAAEALAAAAAAOAAoAAAIZjI8Hybbx4oOuqgTynJf2Ln2NOHpQpmhAAQA7)}.mce-visualblocks h4{background-image:url(data:image/gif;base64,R0lGODlhDgAKAIABALu7u////yH5BAEAAAEALAAAAAAOAAoAAAIajI8HybbxInR0zqeAdhtJlXwV1oCll2HaWgAAOw==)}.mce-visualblocks h5{background-image:url(data:image/gif;base64,R0lGODlhDgAKAIABALu7u////yH5BAEAAAEALAAAAAAOAAoAAAIajI8HybbxIoiuwjane4iq5GlW05GgIkIZUAAAOw==)}.mce-visualblocks h6{background-image:url(data:image/gif;base64,R0lGODlhDgAKAIABALu7u////yH5BAEAAAEALAAAAAAOAAoAAAIajI8HybbxIoiuwjan04jep1iZ1XRlAo5bVgAAOw==)}.mce-visualblocks div:not([data-mce-bogus]){background-image:url(data:image/gif;base64,R0lGODlhEgAKAIABALu7u////yH5BAEAAAEALAAAAAASAAoAAAIfjI9poI0cgDywrhuxfbrzDEbQM2Ei5aRjmoySW4pAAQA7)}.mce-visualblocks section{background-image:url(data:image/gif;base64,R0lGODlhKAAKAIABALu7u////yH5BAEAAAEALAAAAAAoAAoAAAI5jI+pywcNY3sBWHdNrplytD2ellDeSVbp+GmWqaDqDMepc8t17Y4vBsK5hDyJMcI6KkuYU+jpjLoKADs=)}.mce-visualblocks article{background-image:url(data:image/gif;base64,R0lGODlhKgAKAIABALu7u////yH5BAEAAAEALAAAAAAqAAoAAAI6jI+pywkNY3wG0GBvrsd2tXGYSGnfiF7ikpXemTpOiJScasYoDJJrjsG9gkCJ0ag6KhmaIe3pjDYBBQA7)}.mce-visualblocks blockquote{background-image:url(data:image/gif;base64,R0lGODlhPgAKAIABALu7u////yH5BAEAAAEALAAAAAA+AAoAAAJPjI+py+0Knpz0xQDyuUhvfoGgIX5iSKZYgq5uNL5q69asZ8s5rrf0yZmpNkJZzFesBTu8TOlDVAabUyatguVhWduud3EyiUk45xhTTgMBBQA7)}.mce-visualblocks address{background-image:url(data:image/gif;base64,R0lGODlhLQAKAIABALu7u////yH5BAEAAAEALAAAAAAtAAoAAAI/jI+pywwNozSP1gDyyZcjb3UaRpXkWaXmZW4OqKLhBmLs+K263DkJK7OJeifh7FicKD9A1/IpGdKkyFpNmCkAADs=)}.mce-visualblocks pre{background-image:url(data:image/gif;base64,R0lGODlhFQAKAIABALu7uwAAACH5BAEAAAEALAAAAAAVAAoAAAIjjI+ZoN0cgDwSmnpz1NCueYERhnibZVKLNnbOq8IvKpJtVQAAOw==)}.mce-visualblocks figure{background-image:url(data:image/gif;base64,R0lGODlhJAAKAIAAALu7u////yH5BAEAAAEALAAAAAAkAAoAAAI0jI+py+2fwAHUSFvD3RlvG4HIp4nX5JFSpnZUJ6LlrM52OE7uSWosBHScgkSZj7dDKnWAAgA7)}.mce-visualblocks figcaption{border:1px dashed #bbb}.mce-visualblocks hgroup{background-image:url(data:image/gif;base64,R0lGODlhJwAKAIABALu7uwAAACH5BAEAAAEALAAAAAAnAAoAAAI3jI+pywYNI3uB0gpsRtt5fFnfNZaVSYJil4Wo03Hv6Z62uOCgiXH1kZIIJ8NiIxRrAZNMZAtQAAA7)}.mce-visualblocks aside{background-image:url(data:image/gif;base64,R0lGODlhHgAKAIABAKqqqv///yH5BAEAAAEALAAAAAAeAAoAAAItjI+pG8APjZOTzgtqy7I3f1yehmQcFY4WKZbqByutmW4aHUd6vfcVbgudgpYCADs=)}.mce-visualblocks ul{background-image:url(data:image/gif;base64,R0lGODlhDQAKAIAAALu7u////yH5BAEAAAEALAAAAAANAAoAAAIXjI8GybGuYnqUVSjvw26DzzXiqIDlVwAAOw==)}.mce-visualblocks ol{background-image:url(data:image/gif;base64,R0lGODlhDQAKAIABALu7u////yH5BAEAAAEALAAAAAANAAoAAAIXjI8GybH6HHt0qourxC6CvzXieHyeWQAAOw==)}.mce-visualblocks dl{background-image:url(data:image/gif;base64,R0lGODlhDQAKAIABALu7u////yH5BAEAAAEALAAAAAANAAoAAAIXjI8GybEOnmOvUoWznTqeuEjNSCqeGRUAOw==)}.mce-visualblocks:not([dir=rtl]) address,.mce-visualblocks:not([dir=rtl]) article,.mce-visualblocks:not([dir=rtl]) aside,.mce-visualblocks:not([dir=rtl]) blockquote,.mce-visualblocks:not([dir=rtl]) div:not([data-mce-bogus]),.mce-visualblocks:not([dir=rtl]) dl,.mce-visualblocks:not([dir=rtl]) figcaption,.mce-visualblocks:not([dir=rtl]) figure,.mce-visualblocks:not([dir=rtl]) h1,.mce-visualblocks:not([dir=rtl]) h2,.mce-visualblocks:not([dir=rtl]) h3,.mce-visualblocks:not([dir=rtl]) h4,.mce-visualblocks:not([dir=rtl]) h5,.mce-visualblocks:not([dir=rtl]) h6,.mce-visualblocks:not([dir=rtl]) hgroup,.mce-visualblocks:not([dir=rtl]) ol,.mce-visualblocks:not([dir=rtl]) p,.mce-visualblocks:not([dir=rtl]) pre,.mce-visualblocks:not([dir=rtl]) section,.mce-visualblocks:not([dir=rtl]) ul{margin-left:3px}.mce-visualblocks[dir=rtl] address,.mce-visualblocks[dir=rtl] article,.mce-visualblocks[dir=rtl] aside,.mce-visualblocks[dir=rtl] blockquote,.mce-visualblocks[dir=rtl] div:not([data-mce-bogus]),.mce-visualblocks[dir=rtl] dl,.mce-visualblocks[dir=rtl] figcaption,.mce-visualblocks[dir=rtl] figure,.mce-visualblocks[dir=rtl] h1,.mce-visualblocks[dir=rtl] h2,.mce-visualblocks[dir=rtl] h3,.mce-visualblocks[dir=rtl] h4,.mce-visualblocks[dir=rtl] h5,.mce-visualblocks[dir=rtl] h6,.mce-visualblocks[dir=rtl] hgroup,.mce-visualblocks[dir=rtl] ol,.mce-visualblocks[dir=rtl] p,.mce-visualblocks[dir=rtl] pre,.mce-visualblocks[dir=rtl] section,.mce-visualblocks[dir=rtl] ul{background-position-x:right;margin-right:3px}.mce-nbsp,.mce-shy{background:#aaa}.mce-shy::after{content:'-'}\")\n//# sourceMappingURL=content.inline.js.map\n"
  },
  {
    "path": "apps/web-antd/public/tinymce/skins/ui/oxide/content.js",
    "content": "tinymce.Resource.add('ui/default/content.css', \".mce-content-body .mce-item-anchor{background:transparent url(\\\"data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D'8'%20height%3D'12'%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%3E%3Cpath%20d%3D'M0%200L8%200%208%2012%204.09117821%209%200%2012z'%2F%3E%3C%2Fsvg%3E%0A\\\") no-repeat center}.mce-content-body .mce-item-anchor:empty{cursor:default;display:inline-block;height:12px!important;padding:0 2px;-webkit-user-modify:read-only;-moz-user-modify:read-only;-webkit-user-select:all;-moz-user-select:all;user-select:all;width:8px!important}.mce-content-body .mce-item-anchor:not(:empty){background-position-x:2px;display:inline-block;padding-left:12px}.mce-content-body .mce-item-anchor[data-mce-selected]{outline-offset:1px}.tox-comments-visible .tox-comment[contenteditable=false]:not([data-mce-selected]),.tox-comments-visible span.tox-comment img:not([data-mce-selected]),.tox-comments-visible span.tox-comment span.mce-preview-object:not([data-mce-selected]),.tox-comments-visible span.tox-comment>audio:not([data-mce-selected]),.tox-comments-visible span.tox-comment>video:not([data-mce-selected]){outline:3px solid #ffe89d}.tox-comments-visible .tox-comment[contenteditable=false][data-mce-annotation-active=true]:not([data-mce-selected]){outline:3px solid #fed635}.tox-comments-visible span.tox-comment[data-mce-annotation-active=true] img:not([data-mce-selected]),.tox-comments-visible span.tox-comment[data-mce-annotation-active=true] span.mce-preview-object:not([data-mce-selected]),.tox-comments-visible span.tox-comment[data-mce-annotation-active=true]>audio:not([data-mce-selected]),.tox-comments-visible span.tox-comment[data-mce-annotation-active=true]>video:not([data-mce-selected]){outline:3px solid #fed635}.tox-comments-visible span.tox-comment:not([data-mce-selected]){background-color:#ffe89d;outline:0}.tox-comments-visible span.tox-comment[data-mce-annotation-active=true]:not([data-mce-selected=inline-boundary]){background-color:#fed635}.tox-checklist>li:not(.tox-checklist--hidden){list-style:none;margin:.25em 0}.tox-checklist>li:not(.tox-checklist--hidden)::before{content:url(\\\"data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2216%22%20height%3D%2216%22%20viewBox%3D%220%200%2016%2016%22%3E%3Cg%20id%3D%22checklist-unchecked%22%20fill%3D%22none%22%20fill-rule%3D%22evenodd%22%3E%3Crect%20id%3D%22Rectangle%22%20width%3D%2215%22%20height%3D%2215%22%20x%3D%22.5%22%20y%3D%22.5%22%20fill-rule%3D%22nonzero%22%20stroke%3D%22%234C4C4C%22%20rx%3D%222%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E%0A\\\");cursor:pointer;height:1em;margin-left:-1.5em;margin-top:.125em;position:absolute;width:1em}.tox-checklist li:not(.tox-checklist--hidden).tox-checklist--checked::before{content:url(\\\"data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2216%22%20height%3D%2216%22%20viewBox%3D%220%200%2016%2016%22%3E%3Cg%20id%3D%22checklist-checked%22%20fill%3D%22none%22%20fill-rule%3D%22evenodd%22%3E%3Crect%20id%3D%22Rectangle%22%20width%3D%2216%22%20height%3D%2216%22%20fill%3D%22%234099FF%22%20fill-rule%3D%22nonzero%22%20rx%3D%222%22%2F%3E%3Cpath%20id%3D%22Path%22%20fill%3D%22%23FFF%22%20fill-rule%3D%22nonzero%22%20d%3D%22M11.5703186%2C3.14417309%20C11.8516238%2C2.73724603%2012.4164781%2C2.62829933%2012.83558%2C2.89774797%20C13.260121%2C3.17069355%2013.3759736%2C3.72932262%2013.0909105%2C4.14168582%20L7.7580587%2C11.8560195%20C7.43776896%2C12.3193404%206.76483983%2C12.3852142%206.35607322%2C11.9948725%20L3.02491697%2C8.8138662%20C2.66090143%2C8.46625845%202.65798871%2C7.89594698%203.01850234%2C7.54483354%20C3.373942%2C7.19866177%203.94940006%2C7.19592841%204.30829608%2C7.5386474%20L6.85276923%2C9.9684299%20L11.5703186%2C3.14417309%20Z%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E%0A\\\")}[dir=rtl] .tox-checklist>li:not(.tox-checklist--hidden)::before{margin-left:0;margin-right:-1.5em}code[class*=language-],pre[class*=language-]{color:#000;background:0 0;text-shadow:0 1px #fff;font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;tab-size:4;-webkit-hyphens:none;hyphens:none}code[class*=language-] ::-moz-selection,code[class*=language-]::-moz-selection,pre[class*=language-] ::-moz-selection,pre[class*=language-]::-moz-selection{text-shadow:none;background:#b3d4fc}code[class*=language-] ::selection,code[class*=language-]::selection,pre[class*=language-] ::selection,pre[class*=language-]::selection{text-shadow:none;background:#b3d4fc}@media print{code[class*=language-],pre[class*=language-]{text-shadow:none}}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto}:not(pre)>code[class*=language-],pre[class*=language-]{background:#f5f2f0}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#708090}.token.punctuation{color:#999}.token.namespace{opacity:.7}.token.boolean,.token.constant,.token.deleted,.token.number,.token.property,.token.symbol,.token.tag{color:#905}.token.attr-name,.token.builtin,.token.char,.token.inserted,.token.selector,.token.string{color:#690}.language-css .token.string,.style .token.string,.token.entity,.token.operator,.token.url{color:#9a6e3a;background:hsla(0,0%,100%,.5)}.token.atrule,.token.attr-value,.token.keyword{color:#07a}.token.class-name,.token.function{color:#dd4a68}.token.important,.token.regex,.token.variable{color:#e90}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}.mce-content-body{overflow-wrap:break-word;word-wrap:break-word}.mce-content-body .mce-visual-caret{background-color:#000;background-color:currentColor;position:absolute}.mce-content-body .mce-visual-caret-hidden{display:none}.mce-content-body [data-mce-caret]{left:-1000px;margin:0;padding:0;position:absolute;right:auto;top:0}.mce-content-body .mce-offscreen-selection{left:-2000000px;max-width:1000000px;position:absolute}.mce-content-body [contentEditable=false]{cursor:default}.mce-content-body [contentEditable=true]{cursor:text}.tox-cursor-format-painter{cursor:url(\\\"data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%20viewBox%3D%220%200%2024%2024%22%3E%0A%20%20%3Cg%20fill%3D%22none%22%20fill-rule%3D%22evenodd%22%3E%0A%20%20%20%20%3Cpath%20fill%3D%22%23000%22%20fill-rule%3D%22nonzero%22%20d%3D%22M15%2C6%20C15%2C5.45%2014.55%2C5%2014%2C5%20L6%2C5%20C5.45%2C5%205%2C5.45%205%2C6%20L5%2C10%20C5%2C10.55%205.45%2C11%206%2C11%20L14%2C11%20C14.55%2C11%2015%2C10.55%2015%2C10%20L15%2C9%20L16%2C9%20L16%2C12%20L9%2C12%20L9%2C19%20C9%2C19.55%209.45%2C20%2010%2C20%20L11%2C20%20C11.55%2C20%2012%2C19.55%2012%2C19%20L12%2C14%20L18%2C14%20L18%2C7%20L15%2C7%20L15%2C6%20Z%22%2F%3E%0A%20%20%20%20%3Cpath%20fill%3D%22%23000%22%20fill-rule%3D%22nonzero%22%20d%3D%22M1%2C1%20L8.25%2C1%20C8.66421356%2C1%209%2C1.33578644%209%2C1.75%20L9%2C1.75%20C9%2C2.16421356%208.66421356%2C2.5%208.25%2C2.5%20L2.5%2C2.5%20L2.5%2C8.25%20C2.5%2C8.66421356%202.16421356%2C9%201.75%2C9%20L1.75%2C9%20C1.33578644%2C9%201%2C8.66421356%201%2C8.25%20L1%2C1%20Z%22%2F%3E%0A%20%20%3C%2Fg%3E%0A%3C%2Fsvg%3E%0A\\\"),default}div.mce-footnotes hr{margin-inline-end:auto;margin-inline-start:0;width:25%}div.mce-footnotes li>a.mce-footnotes-backlink{text-decoration:none}@media print{sup.mce-footnote a{color:#000;text-decoration:none}div.mce-footnotes{break-inside:avoid;width:100%}div.mce-footnotes li>a.mce-footnotes-backlink{display:none}}tiny-math-block{display:flex;justify-content:center;margin:16px 0 16px 0}tiny-math-inline{display:inline-block}.mce-content-body figure.align-left{float:left}.mce-content-body figure.align-right{float:right}.mce-content-body figure.image.align-center{display:table;margin-left:auto;margin-right:auto}.mce-preview-object{border:1px solid gray;display:inline-block;line-height:0;margin:0 2px 0 2px;position:relative}.mce-preview-object .mce-shim{background:url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7);height:100%;left:0;position:absolute;top:0;width:100%}.mce-preview-object[data-mce-selected=\\\"2\\\"] .mce-shim{display:none}.mce-content-body .mce-mergetag{cursor:default!important;-webkit-user-select:none;-moz-user-select:none;user-select:none}.mce-content-body .mce-mergetag:hover{background-color:rgba(0,108,231,.1)}.mce-content-body .mce-mergetag-affix{background-color:rgba(0,108,231,.1);color:#006ce7}.mce-object{background:transparent url(\\\"data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%3E%3Cpath%20d%3D%22M4%203h16a1%201%200%200%201%201%201v16a1%201%200%200%201-1%201H4a1%201%200%200%201-1-1V4a1%201%200%200%201%201-1zm1%202v14h14V5H5zm4.79%202.565l5.64%204.028a.5.5%200%200%201%200%20.814l-5.64%204.028a.5.5%200%200%201-.79-.407V7.972a.5.5%200%200%201%20.79-.407z%22%2F%3E%3C%2Fsvg%3E%0A\\\") no-repeat center;border:1px dashed #aaa}.mce-pagebreak{border:1px dashed #aaa;cursor:default;display:block;height:5px;margin-top:15px;page-break-before:always;width:100%}@media print{.mce-pagebreak{border:0}}.tiny-pageembed .mce-shim{background:url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7);height:100%;left:0;position:absolute;top:0;width:100%}.tiny-pageembed[data-mce-selected=\\\"2\\\"] .mce-shim{display:none}.tiny-pageembed{display:inline-block;position:relative}.tiny-pageembed--16by9,.tiny-pageembed--1by1,.tiny-pageembed--21by9,.tiny-pageembed--4by3{display:block;overflow:hidden;padding:0;position:relative;width:100%}.tiny-pageembed--21by9{padding-top:42.857143%}.tiny-pageembed--16by9{padding-top:56.25%}.tiny-pageembed--4by3{padding-top:75%}.tiny-pageembed--1by1{padding-top:100%}.tiny-pageembed--16by9 iframe,.tiny-pageembed--1by1 iframe,.tiny-pageembed--21by9 iframe,.tiny-pageembed--4by3 iframe{border:0;height:100%;left:0;position:absolute;top:0;width:100%}.mce-content-body[data-mce-placeholder]{position:relative}.mce-content-body[data-mce-placeholder]:not(.mce-visualblocks)::before{color:rgba(34,47,62,.7);content:attr(data-mce-placeholder);position:absolute}@media (forced-colors:active){.mce-content-body[data-mce-placeholder]:not(.mce-visualblocks)::before{color:highlight;filter:brightness(30%);z-index:-1}}.mce-content-body:not([dir=rtl])[data-mce-placeholder]:not(.mce-visualblocks)::before{left:1px}.mce-content-body[dir=rtl][data-mce-placeholder]:not(.mce-visualblocks)::before{right:1px}.mce-content-body div.mce-resizehandle{background-color:#4099ff;border-color:#4099ff;border-style:solid;border-width:1px;box-sizing:border-box;height:10px;position:absolute;width:10px;z-index:1298}.mce-content-body div.mce-resizehandle:hover{background-color:#4099ff}.mce-content-body div.mce-resizehandle:nth-of-type(1){cursor:nwse-resize}.mce-content-body div.mce-resizehandle:nth-of-type(2){cursor:nesw-resize}.mce-content-body div.mce-resizehandle:nth-of-type(3){cursor:nwse-resize}.mce-content-body div.mce-resizehandle:nth-of-type(4){cursor:nesw-resize}.mce-content-body .mce-resize-backdrop{z-index:10000}.mce-content-body .mce-clonedresizable{cursor:default;opacity:.5;outline:1px dashed #000;position:absolute;z-index:10001}.mce-content-body .mce-clonedresizable.mce-resizetable-columns td,.mce-content-body .mce-clonedresizable.mce-resizetable-columns th{border:0}.mce-content-body .mce-resize-helper{background:#555;background:rgba(0,0,0,.75);border:1px;border-radius:3px;color:#fff;display:none;font-family:sans-serif;font-size:12px;line-height:14px;margin:5px 10px;padding:5px;position:absolute;white-space:nowrap;z-index:10002}.tox-rtc-user-selection{position:relative}.tox-rtc-user-cursor{bottom:0;cursor:default;position:absolute;top:0;width:2px}.tox-rtc-user-cursor::before{background-color:inherit;border-radius:50%;content:'';display:block;height:8px;position:absolute;right:-3px;top:-3px;width:8px}.tox-rtc-user-cursor:hover::after{background-color:inherit;border-radius:100px;box-sizing:border-box;color:#fff;content:attr(data-user);display:block;font-size:12px;font-weight:700;left:-5px;min-height:8px;min-width:8px;padding:0 12px;position:absolute;top:-11px;white-space:nowrap;z-index:1000}.tox-rtc-user-selection--1 .tox-rtc-user-cursor{background-color:#2dc26b}.tox-rtc-user-selection--2 .tox-rtc-user-cursor{background-color:#e03e2d}.tox-rtc-user-selection--3 .tox-rtc-user-cursor{background-color:#f1c40f}.tox-rtc-user-selection--4 .tox-rtc-user-cursor{background-color:#3598db}.tox-rtc-user-selection--5 .tox-rtc-user-cursor{background-color:#b96ad9}.tox-rtc-user-selection--6 .tox-rtc-user-cursor{background-color:#e67e23}.tox-rtc-user-selection--7 .tox-rtc-user-cursor{background-color:#aaa69d}.tox-rtc-user-selection--8 .tox-rtc-user-cursor{background-color:#f368e0}.tox-rtc-remote-image{background:#eaeaea url(\\\"data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%2236%22%20height%3D%2212%22%20viewBox%3D%220%200%2036%2012%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%3Ccircle%20cx%3D%226%22%20cy%3D%226%22%20r%3D%223%22%20fill%3D%22rgba(0%2C%200%2C%200%2C%20.2)%22%3E%0A%20%20%20%20%3Canimate%20attributeName%3D%22r%22%20values%3D%223%3B5%3B3%22%20calcMode%3D%22linear%22%20dur%3D%221s%22%20repeatCount%3D%22indefinite%22%20%2F%3E%0A%20%20%3C%2Fcircle%3E%0A%20%20%3Ccircle%20cx%3D%2218%22%20cy%3D%226%22%20r%3D%223%22%20fill%3D%22rgba(0%2C%200%2C%200%2C%20.2)%22%3E%0A%20%20%20%20%3Canimate%20attributeName%3D%22r%22%20values%3D%223%3B5%3B3%22%20calcMode%3D%22linear%22%20begin%3D%22.33s%22%20dur%3D%221s%22%20repeatCount%3D%22indefinite%22%20%2F%3E%0A%20%20%3C%2Fcircle%3E%0A%20%20%3Ccircle%20cx%3D%2230%22%20cy%3D%226%22%20r%3D%223%22%20fill%3D%22rgba(0%2C%200%2C%200%2C%20.2)%22%3E%0A%20%20%20%20%3Canimate%20attributeName%3D%22r%22%20values%3D%223%3B5%3B3%22%20calcMode%3D%22linear%22%20begin%3D%22.66s%22%20dur%3D%221s%22%20repeatCount%3D%22indefinite%22%20%2F%3E%0A%20%20%3C%2Fcircle%3E%0A%3C%2Fsvg%3E%0A\\\") no-repeat center center;border:1px solid #ccc;min-height:240px;min-width:320px}.mce-match-marker{background:#aaa;color:#fff}.mce-match-marker-selected{background:#39f;color:#fff}.mce-match-marker-selected::-moz-selection{background:#39f;color:#fff}.mce-match-marker-selected::selection{background:#39f;color:#fff}.mce-content-body audio[data-mce-selected],.mce-content-body details[data-mce-selected],.mce-content-body embed[data-mce-selected],.mce-content-body img[data-mce-selected],.mce-content-body object[data-mce-selected],.mce-content-body table[data-mce-selected],.mce-content-body video[data-mce-selected]{outline:3px solid #b4d7ff}.mce-content-body hr[data-mce-selected]{outline:3px solid #b4d7ff;outline-offset:1px}.mce-content-body [contentEditable=false] [contentEditable=true]:focus{outline:3px solid #b4d7ff}.mce-content-body [contentEditable=false] [contentEditable=true]:hover{outline:3px solid #b4d7ff}.mce-content-body [contentEditable=false][data-mce-selected]{cursor:not-allowed;outline:3px solid #b4d7ff}.mce-content-body.mce-content-readonly [contentEditable=true]:focus,.mce-content-body.mce-content-readonly [contentEditable=true]:hover{outline:0}.mce-content-body [data-mce-selected=inline-boundary]{background-color:#b4d7ff}.mce-content-body .mce-edit-focus{outline:3px solid #b4d7ff}.mce-content-body td[data-mce-selected],.mce-content-body th[data-mce-selected]{position:relative}.mce-content-body td[data-mce-selected]::-moz-selection,.mce-content-body th[data-mce-selected]::-moz-selection{background:0 0}.mce-content-body td[data-mce-selected]::selection,.mce-content-body th[data-mce-selected]::selection{background:0 0}.mce-content-body td[data-mce-selected] *,.mce-content-body th[data-mce-selected] *{outline:0;-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}.mce-content-body td[data-mce-selected]::after,.mce-content-body th[data-mce-selected]::after{background-color:rgba(180,215,255,.7);border:1px solid rgba(180,215,255,.7);bottom:-1px;content:'';left:-1px;mix-blend-mode:multiply;position:absolute;right:-1px;top:-1px}@media screen and (-ms-high-contrast:active),(-ms-high-contrast:none){.mce-content-body td[data-mce-selected]::after,.mce-content-body th[data-mce-selected]::after{border-color:rgba(0,84,180,.7)}}.mce-content-body img[data-mce-selected]::-moz-selection{background:0 0}.mce-content-body img[data-mce-selected]::selection{background:0 0}.ephox-snooker-resizer-bar{background-color:#b4d7ff;opacity:0;-webkit-user-select:none;-moz-user-select:none;user-select:none}.ephox-snooker-resizer-cols{cursor:col-resize}.ephox-snooker-resizer-rows{cursor:row-resize}.ephox-snooker-resizer-bar.ephox-snooker-resizer-bar-dragging{opacity:1}.mce-spellchecker-word{background-image:url(\\\"data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D'4'%20height%3D'4'%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%3E%3Cpath%20stroke%3D'%23ff0000'%20fill%3D'none'%20stroke-linecap%3D'round'%20stroke-opacity%3D'.75'%20d%3D'M0%203L2%201%204%203'%2F%3E%3C%2Fsvg%3E%0A\\\");background-position:0 calc(100% + 1px);background-repeat:repeat-x;background-size:auto 6px;cursor:default;height:2rem}.mce-spellchecker-grammar{background-image:url(\\\"data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D'4'%20height%3D'4'%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%3E%3Cpath%20stroke%3D'%2300A835'%20fill%3D'none'%20stroke-linecap%3D'round'%20d%3D'M0%203L2%201%204%203'%2F%3E%3C%2Fsvg%3E%0A\\\");background-position:0 calc(100% + 1px);background-repeat:repeat-x;background-size:auto 6px;cursor:default}.mce-toc{border:1px solid gray}.mce-toc h2{margin:4px}.mce-toc ul>li{list-style-type:none}[data-mce-block]{display:block}.mce-item-table:not([border]),.mce-item-table:not([border]) caption,.mce-item-table:not([border]) td,.mce-item-table:not([border]) th,.mce-item-table[border=\\\"0\\\"],.mce-item-table[border=\\\"0\\\"] caption,.mce-item-table[border=\\\"0\\\"] td,.mce-item-table[border=\\\"0\\\"] th,table[style*=\\\"border-width: 0px\\\"],table[style*=\\\"border-width: 0px\\\"] caption,table[style*=\\\"border-width: 0px\\\"] td,table[style*=\\\"border-width: 0px\\\"] th{border:1px dashed #bbb}.mce-visualblocks address,.mce-visualblocks article,.mce-visualblocks aside,.mce-visualblocks blockquote,.mce-visualblocks div:not([data-mce-bogus]),.mce-visualblocks dl,.mce-visualblocks figcaption,.mce-visualblocks figure,.mce-visualblocks h1,.mce-visualblocks h2,.mce-visualblocks h3,.mce-visualblocks h4,.mce-visualblocks h5,.mce-visualblocks h6,.mce-visualblocks hgroup,.mce-visualblocks ol,.mce-visualblocks p,.mce-visualblocks pre,.mce-visualblocks section,.mce-visualblocks ul{background-repeat:no-repeat;border:1px dashed #bbb;margin-left:3px;padding-top:10px}.mce-visualblocks p{background-image:url(data:image/gif;base64,R0lGODlhCQAJAJEAAAAAAP///7u7u////yH5BAEAAAMALAAAAAAJAAkAAAIQnG+CqCN/mlyvsRUpThG6AgA7)}.mce-visualblocks h1{background-image:url(data:image/gif;base64,R0lGODlhDQAKAIABALu7u////yH5BAEAAAEALAAAAAANAAoAAAIXjI8GybGu1JuxHoAfRNRW3TWXyF2YiRUAOw==)}.mce-visualblocks h2{background-image:url(data:image/gif;base64,R0lGODlhDgAKAIABALu7u////yH5BAEAAAEALAAAAAAOAAoAAAIajI8Hybbx4oOuqgTynJd6bGlWg3DkJzoaUAAAOw==)}.mce-visualblocks h3{background-image:url(data:image/gif;base64,R0lGODlhDgAKAIABALu7u////yH5BAEAAAEALAAAAAAOAAoAAAIZjI8Hybbx4oOuqgTynJf2Ln2NOHpQpmhAAQA7)}.mce-visualblocks h4{background-image:url(data:image/gif;base64,R0lGODlhDgAKAIABALu7u////yH5BAEAAAEALAAAAAAOAAoAAAIajI8HybbxInR0zqeAdhtJlXwV1oCll2HaWgAAOw==)}.mce-visualblocks h5{background-image:url(data:image/gif;base64,R0lGODlhDgAKAIABALu7u////yH5BAEAAAEALAAAAAAOAAoAAAIajI8HybbxIoiuwjane4iq5GlW05GgIkIZUAAAOw==)}.mce-visualblocks h6{background-image:url(data:image/gif;base64,R0lGODlhDgAKAIABALu7u////yH5BAEAAAEALAAAAAAOAAoAAAIajI8HybbxIoiuwjan04jep1iZ1XRlAo5bVgAAOw==)}.mce-visualblocks div:not([data-mce-bogus]){background-image:url(data:image/gif;base64,R0lGODlhEgAKAIABALu7u////yH5BAEAAAEALAAAAAASAAoAAAIfjI9poI0cgDywrhuxfbrzDEbQM2Ei5aRjmoySW4pAAQA7)}.mce-visualblocks section{background-image:url(data:image/gif;base64,R0lGODlhKAAKAIABALu7u////yH5BAEAAAEALAAAAAAoAAoAAAI5jI+pywcNY3sBWHdNrplytD2ellDeSVbp+GmWqaDqDMepc8t17Y4vBsK5hDyJMcI6KkuYU+jpjLoKADs=)}.mce-visualblocks article{background-image:url(data:image/gif;base64,R0lGODlhKgAKAIABALu7u////yH5BAEAAAEALAAAAAAqAAoAAAI6jI+pywkNY3wG0GBvrsd2tXGYSGnfiF7ikpXemTpOiJScasYoDJJrjsG9gkCJ0ag6KhmaIe3pjDYBBQA7)}.mce-visualblocks blockquote{background-image:url(data:image/gif;base64,R0lGODlhPgAKAIABALu7u////yH5BAEAAAEALAAAAAA+AAoAAAJPjI+py+0Knpz0xQDyuUhvfoGgIX5iSKZYgq5uNL5q69asZ8s5rrf0yZmpNkJZzFesBTu8TOlDVAabUyatguVhWduud3EyiUk45xhTTgMBBQA7)}.mce-visualblocks address{background-image:url(data:image/gif;base64,R0lGODlhLQAKAIABALu7u////yH5BAEAAAEALAAAAAAtAAoAAAI/jI+pywwNozSP1gDyyZcjb3UaRpXkWaXmZW4OqKLhBmLs+K263DkJK7OJeifh7FicKD9A1/IpGdKkyFpNmCkAADs=)}.mce-visualblocks pre{background-image:url(data:image/gif;base64,R0lGODlhFQAKAIABALu7uwAAACH5BAEAAAEALAAAAAAVAAoAAAIjjI+ZoN0cgDwSmnpz1NCueYERhnibZVKLNnbOq8IvKpJtVQAAOw==)}.mce-visualblocks figure{background-image:url(data:image/gif;base64,R0lGODlhJAAKAIAAALu7u////yH5BAEAAAEALAAAAAAkAAoAAAI0jI+py+2fwAHUSFvD3RlvG4HIp4nX5JFSpnZUJ6LlrM52OE7uSWosBHScgkSZj7dDKnWAAgA7)}.mce-visualblocks figcaption{border:1px dashed #bbb}.mce-visualblocks hgroup{background-image:url(data:image/gif;base64,R0lGODlhJwAKAIABALu7uwAAACH5BAEAAAEALAAAAAAnAAoAAAI3jI+pywYNI3uB0gpsRtt5fFnfNZaVSYJil4Wo03Hv6Z62uOCgiXH1kZIIJ8NiIxRrAZNMZAtQAAA7)}.mce-visualblocks aside{background-image:url(data:image/gif;base64,R0lGODlhHgAKAIABAKqqqv///yH5BAEAAAEALAAAAAAeAAoAAAItjI+pG8APjZOTzgtqy7I3f1yehmQcFY4WKZbqByutmW4aHUd6vfcVbgudgpYCADs=)}.mce-visualblocks ul{background-image:url(data:image/gif;base64,R0lGODlhDQAKAIAAALu7u////yH5BAEAAAEALAAAAAANAAoAAAIXjI8GybGuYnqUVSjvw26DzzXiqIDlVwAAOw==)}.mce-visualblocks ol{background-image:url(data:image/gif;base64,R0lGODlhDQAKAIABALu7u////yH5BAEAAAEALAAAAAANAAoAAAIXjI8GybH6HHt0qourxC6CvzXieHyeWQAAOw==)}.mce-visualblocks dl{background-image:url(data:image/gif;base64,R0lGODlhDQAKAIABALu7u////yH5BAEAAAEALAAAAAANAAoAAAIXjI8GybEOnmOvUoWznTqeuEjNSCqeGRUAOw==)}.mce-visualblocks:not([dir=rtl]) address,.mce-visualblocks:not([dir=rtl]) article,.mce-visualblocks:not([dir=rtl]) aside,.mce-visualblocks:not([dir=rtl]) blockquote,.mce-visualblocks:not([dir=rtl]) div:not([data-mce-bogus]),.mce-visualblocks:not([dir=rtl]) dl,.mce-visualblocks:not([dir=rtl]) figcaption,.mce-visualblocks:not([dir=rtl]) figure,.mce-visualblocks:not([dir=rtl]) h1,.mce-visualblocks:not([dir=rtl]) h2,.mce-visualblocks:not([dir=rtl]) h3,.mce-visualblocks:not([dir=rtl]) h4,.mce-visualblocks:not([dir=rtl]) h5,.mce-visualblocks:not([dir=rtl]) h6,.mce-visualblocks:not([dir=rtl]) hgroup,.mce-visualblocks:not([dir=rtl]) ol,.mce-visualblocks:not([dir=rtl]) p,.mce-visualblocks:not([dir=rtl]) pre,.mce-visualblocks:not([dir=rtl]) section,.mce-visualblocks:not([dir=rtl]) ul{margin-left:3px}.mce-visualblocks[dir=rtl] address,.mce-visualblocks[dir=rtl] article,.mce-visualblocks[dir=rtl] aside,.mce-visualblocks[dir=rtl] blockquote,.mce-visualblocks[dir=rtl] div:not([data-mce-bogus]),.mce-visualblocks[dir=rtl] dl,.mce-visualblocks[dir=rtl] figcaption,.mce-visualblocks[dir=rtl] figure,.mce-visualblocks[dir=rtl] h1,.mce-visualblocks[dir=rtl] h2,.mce-visualblocks[dir=rtl] h3,.mce-visualblocks[dir=rtl] h4,.mce-visualblocks[dir=rtl] h5,.mce-visualblocks[dir=rtl] h6,.mce-visualblocks[dir=rtl] hgroup,.mce-visualblocks[dir=rtl] ol,.mce-visualblocks[dir=rtl] p,.mce-visualblocks[dir=rtl] pre,.mce-visualblocks[dir=rtl] section,.mce-visualblocks[dir=rtl] ul{background-position-x:right;margin-right:3px}.mce-nbsp,.mce-shy{background:#aaa}.mce-shy::after{content:'-'}body{font-family:sans-serif}table{border-collapse:collapse}\")\n//# sourceMappingURL=content.js.map\n"
  },
  {
    "path": "apps/web-antd/public/tinymce/skins/ui/oxide/skin.js",
    "content": "tinymce.Resource.add('ui/default/skin.css', \".tox{box-shadow:none;box-sizing:content-box;color:#222f3e;cursor:auto;font-family:-apple-system,BlinkMacSystemFont,\\\"Segoe UI\\\",Roboto,Oxygen-Sans,Ubuntu,Cantarell,\\\"Helvetica Neue\\\",sans-serif;font-size:16px;font-style:normal;font-weight:400;line-height:normal;-webkit-tap-highlight-color:transparent;text-decoration:none;text-shadow:none;text-transform:none;vertical-align:initial;white-space:normal}.tox :not(svg):not(rect){box-sizing:inherit;color:inherit;cursor:inherit;direction:inherit;font-family:inherit;font-size:inherit;font-style:inherit;font-weight:inherit;line-height:inherit;-webkit-tap-highlight-color:inherit;text-align:inherit;text-decoration:inherit;text-shadow:inherit;text-transform:inherit;vertical-align:inherit;white-space:inherit}.tox :not(svg):not(rect){background:0 0;border:0;box-shadow:none;float:none;height:auto;margin:0;max-width:none;outline:0;padding:0;position:static;width:auto}.tox:not([dir=rtl]){direction:ltr;text-align:left}.tox[dir=rtl]{direction:rtl;text-align:right}.tox-tinymce{border:2px solid #eee;border-radius:10px;box-shadow:none;box-sizing:border-box;display:flex;flex-direction:column;font-family:-apple-system,BlinkMacSystemFont,\\\"Segoe UI\\\",Roboto,Oxygen-Sans,Ubuntu,Cantarell,\\\"Helvetica Neue\\\",sans-serif;overflow:hidden;position:relative;visibility:inherit!important}.tox.tox-tinymce-inline{border:none;box-shadow:none;overflow:initial}.tox.tox-tinymce-inline .tox-editor-container{overflow:initial}.tox.tox-tinymce-inline .tox-editor-header{background-color:#fff;border:2px solid #eee;border-radius:10px;box-shadow:none;overflow:hidden}.tox-tinymce-aux{font-family:-apple-system,BlinkMacSystemFont,\\\"Segoe UI\\\",Roboto,Oxygen-Sans,Ubuntu,Cantarell,\\\"Helvetica Neue\\\",sans-serif;z-index:1300}.tox-tinymce :focus,.tox-tinymce-aux :focus{outline:0}button::-moz-focus-inner{border:0}.tox[dir=rtl] .tox-icon--flip svg{transform:rotateY(180deg)}.tox .accessibility-issue__header{align-items:center;display:flex;margin-bottom:4px}.tox .accessibility-issue__description{align-items:stretch;border-radius:6px;display:flex;justify-content:space-between}.tox .accessibility-issue__description>div{padding-bottom:4px}.tox .accessibility-issue__description>div>div{align-items:center;display:flex;margin-bottom:4px}.tox .accessibility-issue__description>div>div .tox-icon svg{display:block}.tox .accessibility-issue__repair{margin-top:16px}.tox .tox-dialog__body-content .accessibility-issue--info .accessibility-issue__description{background-color:rgba(0,101,216,.1);color:#222f3e}.tox .tox-dialog__body-content .accessibility-issue--info .tox-form__group h2{color:#006ce7}.tox .tox-dialog__body-content .accessibility-issue--info .tox-icon svg{fill:#006ce7}.tox .tox-dialog__body-content .accessibility-issue--info a.tox-button--naked.tox-button--icon{background-color:#006ce7;color:#fff}.tox .tox-dialog__body-content .accessibility-issue--info a.tox-button--naked.tox-button--icon:focus,.tox .tox-dialog__body-content .accessibility-issue--info a.tox-button--naked.tox-button--icon:hover{background-color:#0060ce}.tox .tox-dialog__body-content .accessibility-issue--info a.tox-button--naked.tox-button--icon:active{background-color:#0054b4}.tox .tox-dialog__body-content .accessibility-issue--warn .accessibility-issue__description{background-color:rgba(255,165,0,.08);color:#222f3e}.tox .tox-dialog__body-content .accessibility-issue--warn .tox-form__group h2{color:#8f5d00}.tox .tox-dialog__body-content .accessibility-issue--warn .tox-icon svg{fill:#8f5d00}.tox .tox-dialog__body-content .accessibility-issue--warn a.tox-button--naked.tox-button--icon{background-color:#ffe89d;color:#222f3e}.tox .tox-dialog__body-content .accessibility-issue--warn a.tox-button--naked.tox-button--icon:focus,.tox .tox-dialog__body-content .accessibility-issue--warn a.tox-button--naked.tox-button--icon:hover{background-color:#f2d574;color:#222f3e}.tox .tox-dialog__body-content .accessibility-issue--warn a.tox-button--naked.tox-button--icon:active{background-color:#e8c657;color:#222f3e}.tox .tox-dialog__body-content .accessibility-issue--error .accessibility-issue__description{background-color:rgba(204,0,0,.1);color:#222f3e}.tox .tox-dialog__body-content .accessibility-issue--error .tox-form__group h2{color:#c00}.tox .tox-dialog__body-content .accessibility-issue--error .tox-icon svg{fill:#c00}.tox .tox-dialog__body-content .accessibility-issue--error a.tox-button--naked.tox-button--icon{background-color:#f2bfbf;color:#222f3e}.tox .tox-dialog__body-content .accessibility-issue--error a.tox-button--naked.tox-button--icon:focus,.tox .tox-dialog__body-content .accessibility-issue--error a.tox-button--naked.tox-button--icon:hover{background-color:#e9a4a4;color:#222f3e}.tox .tox-dialog__body-content .accessibility-issue--error a.tox-button--naked.tox-button--icon:active{background-color:#ee9494;color:#222f3e}.tox .tox-dialog__body-content .accessibility-issue--success .accessibility-issue__description{background-color:rgba(120,171,70,.1);color:#222f3e}.tox .tox-dialog__body-content .accessibility-issue--success .accessibility-issue__description>:last-child{display:none}.tox .tox-dialog__body-content .accessibility-issue--success .tox-form__group h2{color:#527530}.tox .tox-dialog__body-content .accessibility-issue--success .tox-icon svg{fill:#527530}.tox .tox-dialog__body-content .accessibility-issue__header .tox-form__group h1,.tox .tox-dialog__body-content .tox-form__group .accessibility-issue__description h2{font-size:14px;margin-top:0}.tox:not([dir=rtl]) .tox-dialog__body-content .accessibility-issue__header .tox-button{margin-left:4px}.tox:not([dir=rtl]) .tox-dialog__body-content .accessibility-issue__header>:nth-last-child(2){margin-left:auto}.tox:not([dir=rtl]) .tox-dialog__body-content .accessibility-issue__description{padding:4px 4px 4px 8px}.tox[dir=rtl] .tox-dialog__body-content .accessibility-issue__header .tox-button{margin-right:4px}.tox[dir=rtl] .tox-dialog__body-content .accessibility-issue__header>:nth-last-child(2){margin-right:auto}.tox[dir=rtl] .tox-dialog__body-content .accessibility-issue__description{padding:4px 8px 4px 4px}.tox .mce-codemirror{background:#fff;bottom:0;font-size:13px;left:0;position:absolute;right:0;top:0;z-index:1}.tox .mce-codemirror.tox-inline-codemirror{margin:8px;position:absolute}.tox .tox-advtemplate .tox-form__grid{flex:1}.tox .tox-advtemplate .tox-form__grid>div:first-child{display:flex;flex-direction:column;width:30%}.tox .tox-advtemplate .tox-form__grid>div:first-child>div:nth-child(2){flex-basis:0;flex-grow:1;overflow:auto}@media only screen and (max-width:767px){body:not(.tox-force-desktop) .tox .tox-advtemplate .tox-form__grid>div:first-child{width:100%}}.tox .tox-advtemplate iframe{border-color:#eee;border-radius:10px;border-style:solid;border-width:1px;margin:0 10px}.tox .tox-anchorbar{display:flex;flex:0 0 auto}.tox .tox-bottom-anchorbar{display:flex;flex:0 0 auto}.tox .tox-bar{display:flex;flex:0 0 auto}.tox .tox-button{background-color:#006ce7;background-image:none;background-position:0 0;background-repeat:repeat;border-color:#006ce7;border-radius:6px;border-style:solid;border-width:1px;box-shadow:none;box-sizing:border-box;color:#fff;cursor:pointer;display:inline-block;font-family:-apple-system,BlinkMacSystemFont,\\\"Segoe UI\\\",Roboto,Oxygen-Sans,Ubuntu,Cantarell,\\\"Helvetica Neue\\\",sans-serif;font-size:14px;font-style:normal;font-weight:700;letter-spacing:normal;line-height:24px;margin:0;outline:0;padding:4px 16px;position:relative;text-align:center;text-decoration:none;text-transform:none;white-space:nowrap}.tox .tox-button::before{border-radius:6px;bottom:-1px;box-shadow:inset 0 0 0 1px #fff,0 0 0 2px #006ce7;content:'';left:-1px;opacity:0;pointer-events:none;position:absolute;right:-1px;top:-1px}.tox .tox-button[disabled]{background-color:#006ce7;background-image:none;border-color:#006ce7;box-shadow:none;color:rgba(255,255,255,.5);cursor:not-allowed}.tox .tox-button:focus:not(:disabled){background-color:#0060ce;background-image:none;border-color:#0060ce;box-shadow:none;color:#fff}.tox .tox-button:focus:not(:disabled)::before{opacity:1}.tox .tox-button:hover:not(:disabled){background-color:#0060ce;background-image:none;border-color:#0060ce;box-shadow:none;color:#fff}.tox .tox-button:active:not(:disabled){background-color:#0054b4;background-image:none;border-color:#0054b4;box-shadow:none;color:#fff}.tox .tox-button.tox-button--enabled{background-color:#0054b4;background-image:none;border-color:#0054b4;box-shadow:none;color:#fff}.tox .tox-button.tox-button--enabled[disabled]{background-color:#0054b4;background-image:none;border-color:#0054b4;box-shadow:none;color:rgba(255,255,255,.5);cursor:not-allowed}.tox .tox-button.tox-button--enabled:focus:not(:disabled){background-color:#00489b;background-image:none;border-color:#00489b;box-shadow:none;color:#fff}.tox .tox-button.tox-button--enabled:hover:not(:disabled){background-color:#00489b;background-image:none;border-color:#00489b;box-shadow:none;color:#fff}.tox .tox-button.tox-button--enabled:active:not(:disabled){background-color:#003c81;background-image:none;border-color:#003c81;box-shadow:none;color:#fff}.tox .tox-button--icon-and-text,.tox .tox-button.tox-button--icon-and-text,.tox .tox-button.tox-button--secondary.tox-button--icon-and-text{display:flex;padding:5px 4px}.tox .tox-button--icon-and-text .tox-icon svg,.tox .tox-button.tox-button--icon-and-text .tox-icon svg,.tox .tox-button.tox-button--secondary.tox-button--icon-and-text .tox-icon svg{display:block;fill:currentColor}.tox .tox-button--secondary{background-color:#f0f0f0;background-image:none;background-position:0 0;background-repeat:repeat;border-color:#f0f0f0;border-radius:6px;border-style:solid;border-width:1px;box-shadow:none;color:#222f3e;font-size:14px;font-style:normal;font-weight:700;letter-spacing:normal;outline:0;padding:4px 16px;text-decoration:none;text-transform:none}.tox .tox-button--secondary[disabled]{background-color:#f0f0f0;background-image:none;border-color:#f0f0f0;box-shadow:none;color:rgba(34,47,62,.5)}.tox .tox-button--secondary:focus:not(:disabled){background-color:#e3e3e3;background-image:none;border-color:#e3e3e3;box-shadow:none;color:#222f3e}.tox .tox-button--secondary:hover:not(:disabled){background-color:#e3e3e3;background-image:none;border-color:#e3e3e3;box-shadow:none;color:#222f3e}.tox .tox-button--secondary:active:not(:disabled){background-color:#d6d6d6;background-image:none;border-color:#d6d6d6;box-shadow:none;color:#222f3e}.tox .tox-button--secondary.tox-button--enabled{background-color:#a8c8ed;background-image:none;border-color:#a8c8ed;box-shadow:none;color:#222f3e}.tox .tox-button--secondary.tox-button--enabled[disabled]{background-color:#a8c8ed;background-image:none;border-color:#a8c8ed;box-shadow:none;color:rgba(34,47,62,.5)}.tox .tox-button--secondary.tox-button--enabled:focus:not(:disabled){background-color:#93bbe9;background-image:none;border-color:#93bbe9;box-shadow:none;color:#222f3e}.tox .tox-button--secondary.tox-button--enabled:hover:not(:disabled){background-color:#93bbe9;background-image:none;border-color:#93bbe9;box-shadow:none;color:#222f3e}.tox .tox-button--secondary.tox-button--enabled:active:not(:disabled){background-color:#7daee4;background-image:none;border-color:#7daee4;box-shadow:none;color:#222f3e}.tox .tox-button--icon,.tox .tox-button.tox-button--icon,.tox .tox-button.tox-button--secondary.tox-button--icon{padding:4px}.tox .tox-button--icon .tox-icon svg,.tox .tox-button.tox-button--icon .tox-icon svg,.tox .tox-button.tox-button--secondary.tox-button--icon .tox-icon svg{display:block;fill:currentColor}.tox .tox-button-link{background:0;border:none;box-sizing:border-box;cursor:pointer;display:inline-block;font-family:-apple-system,BlinkMacSystemFont,\\\"Segoe UI\\\",Roboto,Oxygen-Sans,Ubuntu,Cantarell,\\\"Helvetica Neue\\\",sans-serif;font-size:16px;font-weight:400;line-height:1.3;margin:0;padding:0;white-space:nowrap}.tox .tox-button-link--sm{font-size:14px}.tox .tox-button--naked{background-color:transparent;border-color:transparent;box-shadow:unset;color:#222f3e}.tox .tox-button--naked[disabled]{background-color:rgba(34,47,62,.12);border-color:transparent;box-shadow:unset;color:rgba(34,47,62,.5)}.tox .tox-button--naked:hover:not(:disabled){background-color:rgba(34,47,62,.12);border-color:transparent;box-shadow:unset;color:#222f3e}.tox .tox-button--naked:focus:not(:disabled){background-color:rgba(34,47,62,.12);border-color:transparent;box-shadow:unset;color:#222f3e}.tox .tox-button--naked:active:not(:disabled){background-color:rgba(34,47,62,.18);border-color:transparent;box-shadow:unset;color:#222f3e}.tox .tox-button--naked .tox-icon svg{fill:currentColor}.tox .tox-button--naked.tox-button--icon:hover:not(:disabled){color:#222f3e}.tox .tox-checkbox{align-items:center;border-radius:6px;cursor:pointer;display:flex;height:36px;min-width:36px}.tox .tox-checkbox__input{height:1px;overflow:hidden;position:absolute;top:auto;width:1px}.tox .tox-checkbox__icons{align-items:center;border-radius:6px;box-shadow:0 0 0 2px transparent;box-sizing:content-box;display:flex;height:24px;justify-content:center;padding:calc(4px - 1px);width:24px}.tox .tox-checkbox__icons .tox-checkbox-icon__unchecked svg{display:block;fill:rgba(34,47,62,.3)}@media (forced-colors:active){.tox .tox-checkbox__icons .tox-checkbox-icon__unchecked svg{fill:currentColor!important}}.tox .tox-checkbox__icons .tox-checkbox-icon__indeterminate svg{display:none;fill:#006ce7}.tox .tox-checkbox__icons .tox-checkbox-icon__checked svg{display:none;fill:#006ce7}.tox .tox-checkbox--disabled{color:rgba(34,47,62,.5);cursor:not-allowed}.tox .tox-checkbox--disabled .tox-checkbox__icons .tox-checkbox-icon__checked svg{fill:rgba(34,47,62,.5)}.tox .tox-checkbox--disabled .tox-checkbox__icons .tox-checkbox-icon__unchecked svg{fill:rgba(34,47,62,.5)}.tox .tox-checkbox--disabled .tox-checkbox__icons .tox-checkbox-icon__indeterminate svg{fill:rgba(34,47,62,.5)}.tox input.tox-checkbox__input:checked+.tox-checkbox__icons .tox-checkbox-icon__unchecked svg{display:none}.tox input.tox-checkbox__input:checked+.tox-checkbox__icons .tox-checkbox-icon__checked svg{display:block}.tox input.tox-checkbox__input:indeterminate+.tox-checkbox__icons .tox-checkbox-icon__unchecked svg{display:none}.tox input.tox-checkbox__input:indeterminate+.tox-checkbox__icons .tox-checkbox-icon__indeterminate svg{display:block}.tox input.tox-checkbox__input:focus+.tox-checkbox__icons{border-radius:6px;box-shadow:inset 0 0 0 1px #006ce7;padding:calc(4px - 1px)}.tox:not([dir=rtl]) .tox-checkbox__label{margin-left:4px}.tox:not([dir=rtl]) .tox-checkbox__input{left:-10000px}.tox:not([dir=rtl]) .tox-bar .tox-checkbox{margin-left:4px}.tox[dir=rtl] .tox-checkbox__label{margin-right:4px}.tox[dir=rtl] .tox-checkbox__input{right:-10000px}.tox[dir=rtl] .tox-bar .tox-checkbox{margin-right:4px}.tox .tox-collection--toolbar .tox-collection__group{display:flex;padding:0}.tox .tox-collection--grid .tox-collection__group{display:flex;flex-wrap:wrap;max-height:208px;overflow-x:hidden;overflow-y:auto;padding:0}.tox .tox-collection--list .tox-collection__group{border-bottom-width:0;border-color:#e3e3e3;border-left-width:0;border-right-width:0;border-style:solid;border-top-width:1px;padding:4px 0}.tox .tox-collection--list .tox-collection__group:first-child{border-top-width:0}.tox .tox-collection__group-heading{background-color:#fcfcfc;color:rgba(34,47,62,.7);cursor:default;font-size:12px;font-style:normal;font-weight:400;margin-bottom:4px;margin-top:-4px;padding:4px 8px;text-transform:none;-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}.tox .tox-collection__item{align-items:center;border-radius:3px;color:#222f3e;display:flex;-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}.tox .tox-collection--list .tox-collection__item{padding:4px 8px}.tox .tox-collection--toolbar .tox-collection__item{border-radius:3px;padding:4px}.tox .tox-collection--grid .tox-collection__item{border-radius:3px;padding:4px}.tox .tox-collection--list .tox-collection__item--enabled{background-color:#fff;color:#222f3e}.tox .tox-collection--list .tox-collection__item--active{background-color:#006ce7}.tox .tox-collection--toolbar .tox-collection__item--enabled,.tox .tox-collection--toolbar .tox-collection__item--enabled.tox-collection__item--active,.tox .tox-collection--toolbar .tox-collection__item--enabled.tox-collection__item--active:hover{background-color:#a6ccf7;color:#222f3e}@media (forced-colors:active){.tox .tox-collection--toolbar .tox-collection__item--enabled,.tox .tox-collection--toolbar .tox-collection__item--enabled.tox-collection__item--active,.tox .tox-collection--toolbar .tox-collection__item--enabled.tox-collection__item--active:hover{border-radius:3px;outline:solid 1px}}.tox .tox-collection--toolbar .tox-collection__item--active{background-color:#fff;position:relative}.tox .tox-collection--toolbar .tox-collection__item--active:hover{background-color:#f0f0f0;color:#222f3e}.tox .tox-collection--toolbar .tox-collection__item--active:focus{background-color:#f0f0f0;color:#222f3e}.tox .tox-collection--toolbar .tox-collection__item--active:focus::after{border-radius:3px;bottom:0;box-shadow:0 0 0 2px #006ce7;content:'';left:0;position:absolute;right:0;top:0}@media (forced-colors:active){.tox .tox-collection--toolbar .tox-collection__item--active:focus::after{border:2px solid highlight}}.tox .tox-collection--grid .tox-collection__item--enabled{background-color:#a6ccf7;color:#222f3e}.tox .tox-collection--grid .tox-collection__item--active:not(.tox-collection__item--state-disabled){background-color:#f0f0f0;color:#222f3e;position:relative;z-index:1}.tox .tox-collection--grid .tox-collection__item--active:not(.tox-collection__item--state-disabled):focus::after{border-radius:3px;bottom:0;box-shadow:0 0 0 2px #006ce7 inset;content:'';left:0;position:absolute;right:0;top:0}@media (forced-colors:active){.tox .tox-collection--grid .tox-collection__item--active:not(.tox-collection__item--state-disabled):focus::after{border:2px solid highlight}}.tox .tox-collection--list .tox-collection__item--active:not(.tox-collection__item--state-disabled){color:#fff}@media (forced-colors:active){.tox .tox-collection--list .tox-collection__item--active:not(.tox-collection__item--state-disabled){border:solid 1px}}.tox .tox-collection--toolbar .tox-collection__item--active:not(.tox-collection__item--state-disabled){color:#222f3e}@media (forced-colors:active){.tox .tox-collection--toolbar .tox-collection__item--active:not(.tox-collection__item--state-disabled):hover{border-radius:3px;outline:solid 1px}}.tox .tox-collection__item-checkmark,.tox .tox-collection__item-icon{align-items:center;display:flex;height:24px;justify-content:center;width:24px}.tox .tox-collection__item-checkmark svg,.tox .tox-collection__item-icon svg{fill:currentColor}.tox .tox-collection--toolbar-lg .tox-collection__item-icon{height:48px;width:48px}.tox .tox-collection__item-label{color:currentColor;display:inline-block;flex:1;font-size:14px;font-style:normal;font-weight:400;line-height:24px;max-width:100%;text-transform:none;word-break:break-all}.tox .tox-collection__item-accessory{color:currentColor;display:inline-block;font-size:14px;height:24px;line-height:24px;text-transform:none}.tox .tox-collection__item-caret{align-items:center;display:flex;min-height:24px}.tox .tox-collection__item-caret::after{content:'';font-size:0;min-height:inherit}.tox .tox-collection__item-caret svg{fill:currentColor}.tox .tox-collection__item--state-disabled{background-color:transparent;color:rgba(34,47,62,.5);cursor:not-allowed}.tox .tox-collection__item--state-disabled .tox-collection__item-caret svg{fill:rgba(34,47,62,.5)}.tox .tox-collection--list .tox-collection__item:not(.tox-collection__item--enabled) .tox-collection__item-checkmark svg{display:none}.tox .tox-collection--list .tox-collection__item:not(.tox-collection__item--enabled) .tox-collection__item-accessory+.tox-collection__item-checkmark{display:none}.tox .tox-collection--horizontal{background-color:#fff;border:1px solid #e3e3e3;border-radius:6px;box-shadow:0 0 2px 0 rgba(34,47,62,.2),0 4px 8px 0 rgba(34,47,62,.15);display:flex;flex:0 0 auto;flex-shrink:0;flex-wrap:nowrap;margin-bottom:0;overflow-x:auto;padding:0}.tox .tox-collection--horizontal .tox-collection__group{align-items:center;display:flex;flex-wrap:nowrap;margin:0;padding:0 4px}.tox .tox-collection--horizontal .tox-collection__item{height:28px;margin:6px 1px 5px 0;padding:0 4px}.tox .tox-collection--horizontal .tox-collection__item-label{white-space:nowrap}.tox .tox-collection--horizontal .tox-collection__item-caret{margin-left:4px}.tox .tox-collection__item-container{display:flex}.tox .tox-collection__item-container--row{align-items:center;flex:1 1 auto;flex-direction:row}.tox .tox-collection__item-container--row.tox-collection__item-container--align-left{margin-right:auto}.tox .tox-collection__item-container--row.tox-collection__item-container--align-right{justify-content:flex-end;margin-left:auto}.tox .tox-collection__item-container--row.tox-collection__item-container--valign-top{align-items:flex-start;margin-bottom:auto}.tox .tox-collection__item-container--row.tox-collection__item-container--valign-middle{align-items:center}.tox .tox-collection__item-container--row.tox-collection__item-container--valign-bottom{align-items:flex-end;margin-top:auto}.tox .tox-collection__item-container--column{align-self:center;flex:1 1 auto;flex-direction:column}.tox .tox-collection__item-container--column.tox-collection__item-container--align-left{align-items:flex-start}.tox .tox-collection__item-container--column.tox-collection__item-container--align-right{align-items:flex-end}.tox .tox-collection__item-container--column.tox-collection__item-container--valign-top{align-self:flex-start}.tox .tox-collection__item-container--column.tox-collection__item-container--valign-middle{align-self:center}.tox .tox-collection__item-container--column.tox-collection__item-container--valign-bottom{align-self:flex-end}.tox:not([dir=rtl]) .tox-collection--horizontal .tox-collection__group:not(:last-of-type){border-right:1px solid transparent}.tox:not([dir=rtl]) .tox-collection--list .tox-collection__item>:not(:first-child){margin-left:8px}.tox:not([dir=rtl]) .tox-collection--list .tox-collection__item>.tox-collection__item-label:first-child{margin-left:4px}.tox:not([dir=rtl]) .tox-collection__item-accessory{margin-left:16px;text-align:right}.tox:not([dir=rtl]) .tox-collection .tox-collection__item-caret{margin-left:16px}.tox[dir=rtl] .tox-collection--horizontal .tox-collection__group:not(:last-of-type){border-left:1px solid transparent}.tox[dir=rtl] .tox-collection--list .tox-collection__item>:not(:first-child){margin-right:8px}.tox[dir=rtl] .tox-collection--list .tox-collection__item>.tox-collection__item-label:first-child{margin-right:4px}.tox[dir=rtl] .tox-collection__item-accessory{margin-right:16px;text-align:left}.tox[dir=rtl] .tox-collection .tox-collection__item-caret{margin-right:16px;transform:rotateY(180deg)}.tox[dir=rtl] .tox-collection--horizontal .tox-collection__item-caret{margin-right:4px}@media (forced-colors:active){.tox .tox-hue-slider,.tox .tox-rgb-form .tox-rgba-preview{background-color:currentColor!important;border:1px solid highlight!important;forced-color-adjust:none}}.tox .tox-color-picker-container{display:flex;flex-direction:row;height:225px;margin:0}.tox .tox-sv-palette{box-sizing:border-box;display:flex;height:100%}.tox .tox-sv-palette-spectrum{height:100%}.tox .tox-sv-palette,.tox .tox-sv-palette-spectrum{width:225px}.tox .tox-sv-palette-thumb{background:0 0;border:1px solid #000;border-radius:50%;box-sizing:content-box;height:12px;position:absolute;width:12px}.tox .tox-sv-palette-inner-thumb{border:1px solid #fff;border-radius:50%;height:10px;position:absolute;width:10px}.tox .tox-hue-slider{box-sizing:border-box;height:100%;width:25px}.tox .tox-hue-slider-spectrum{background:linear-gradient(to bottom,red,#ff0080,#f0f,#8000ff,#00f,#0080ff,#0ff,#00ff80,#0f0,#80ff00,#ff0,#ff8000,red);height:100%;width:100%}.tox .tox-hue-slider,.tox .tox-hue-slider-spectrum{width:20px}.tox .tox-hue-slider-spectrum:focus,.tox .tox-sv-palette-spectrum:focus{outline:#08f solid}.tox .tox-hue-slider-thumb{background:#fff;border:1px solid #000;box-sizing:content-box;height:4px;width:100%}.tox .tox-rgb-form{display:flex;flex-direction:column;justify-content:space-between}.tox .tox-rgb-form div{align-items:center;display:flex;justify-content:space-between;margin-bottom:5px;width:inherit}.tox .tox-rgb-form input{width:6em}.tox .tox-rgb-form input.tox-invalid{border:1px solid red!important}.tox .tox-rgb-form .tox-rgba-preview{border:1px solid #000;flex-grow:2;margin-bottom:0}.tox:not([dir=rtl]) .tox-sv-palette{margin-right:15px}.tox:not([dir=rtl]) .tox-hue-slider{margin-right:15px}.tox:not([dir=rtl]) .tox-hue-slider-thumb{margin-left:-1px}.tox:not([dir=rtl]) .tox-rgb-form label{margin-right:.5em}.tox[dir=rtl] .tox-sv-palette{margin-left:15px}.tox[dir=rtl] .tox-hue-slider{margin-left:15px}.tox[dir=rtl] .tox-hue-slider-thumb{margin-right:-1px}.tox[dir=rtl] .tox-rgb-form label{margin-left:.5em}.tox .tox-toolbar .tox-swatches,.tox .tox-toolbar__overflow .tox-swatches,.tox .tox-toolbar__primary .tox-swatches{margin:5px 0 6px 11px}.tox .tox-collection--list .tox-collection__group .tox-swatches-menu{border:0;margin:-4px -4px}.tox .tox-swatches__row{display:flex}@media (forced-colors:active){.tox .tox-swatches__row{forced-color-adjust:none}}.tox .tox-swatch{height:30px;transition:transform .15s,box-shadow .15s;width:30px}.tox .tox-swatch:focus,.tox .tox-swatch:hover{box-shadow:0 0 0 1px rgba(127,127,127,.3) inset;transform:scale(.8)}.tox .tox-swatch--remove{align-items:center;display:flex;justify-content:center}.tox .tox-swatch--remove svg path{stroke:#e74c3c}.tox .tox-swatches__picker-btn{align-items:center;background-color:transparent;border:0;cursor:pointer;display:flex;height:30px;justify-content:center;outline:0;padding:0;width:30px}.tox .tox-swatches__picker-btn svg{fill:#222f3e;height:24px;width:24px}.tox .tox-swatches__picker-btn:hover{background:#f0f0f0}.tox div.tox-swatch:not(.tox-swatch--remove) svg{display:none;fill:#222f3e;height:24px;margin:calc((30px - 24px)/ 2) calc((30px - 24px)/ 2);width:24px}.tox div.tox-swatch:not(.tox-swatch--remove) svg path{fill:#fff;paint-order:stroke;stroke:#222f3e;stroke-width:2px}.tox div.tox-swatch:not(.tox-swatch--remove).tox-collection__item--enabled svg{display:block}.tox:not([dir=rtl]) .tox-swatches__picker-btn{margin-left:auto}.tox[dir=rtl] .tox-swatches__picker-btn{margin-right:auto}.tox .tox-comment-thread{background:#fff;position:relative}.tox .tox-comment-thread>:not(:first-child){margin-top:8px}.tox .tox-comment{background:#fff;border:1px solid #eee;border-radius:6px;box-shadow:0 4px 8px 0 rgba(34,47,62,.1);padding:8px 8px 16px 8px;position:relative}.tox .tox-comment__header{align-items:center;color:#222f3e;display:flex;justify-content:space-between}.tox .tox-comment__date{color:#222f3e;font-size:12px;line-height:18px}.tox .tox-comment__body{color:#222f3e;font-size:14px;font-style:normal;font-weight:400;line-height:1.3;margin-top:8px;position:relative;text-transform:initial}.tox .tox-comment__body textarea{resize:none;white-space:normal;width:100%}.tox .tox-comment__expander{padding-top:8px}.tox .tox-comment__expander p{color:rgba(34,47,62,.7);font-size:14px;font-style:normal}.tox .tox-comment__body p{margin:0}.tox .tox-comment__buttonspacing{padding-top:16px;text-align:center}.tox .tox-comment-thread__overlay::after{background:#fff;bottom:0;content:\\\"\\\";display:flex;left:0;opacity:.9;position:absolute;right:0;top:0;z-index:5}.tox .tox-comment__reply{display:flex;flex-shrink:0;flex-wrap:wrap;justify-content:flex-end;margin-top:8px}.tox .tox-comment__reply>:first-child{margin-bottom:8px;width:100%}.tox .tox-comment__edit{display:flex;flex-wrap:wrap;justify-content:flex-end;margin-top:16px}.tox .tox-comment__gradient::after{background:linear-gradient(rgba(255,255,255,0),#fff);bottom:0;content:\\\"\\\";display:block;height:5em;margin-top:-40px;position:absolute;width:100%}.tox .tox-comment__overlay{background:#fff;bottom:0;display:flex;flex-direction:column;flex-grow:1;left:0;opacity:.9;position:absolute;right:0;text-align:center;top:0;z-index:5}.tox .tox-comment__loading-text{align-items:center;color:#222f3e;display:flex;flex-direction:column;position:relative}.tox .tox-comment__loading-text>div{padding-bottom:16px}.tox .tox-comment__overlaytext{bottom:0;flex-direction:column;font-size:14px;left:0;padding:1em;position:absolute;right:0;top:0;z-index:10}.tox .tox-comment__overlaytext p{background-color:#fff;box-shadow:0 0 8px 8px #fff;color:#222f3e;text-align:center}.tox .tox-comment__overlaytext div:nth-of-type(2){font-size:.8em}.tox .tox-comment__busy-spinner{align-items:center;background-color:#fff;bottom:0;display:flex;justify-content:center;left:0;position:absolute;right:0;top:0;z-index:20}.tox .tox-comment__scroll{display:flex;flex-direction:column;flex-shrink:1;overflow:auto}.tox .tox-conversations{margin:8px}.tox:not([dir=rtl]) .tox-comment__edit{margin-left:8px}.tox:not([dir=rtl]) .tox-comment__buttonspacing>:last-child,.tox:not([dir=rtl]) .tox-comment__edit>:last-child,.tox:not([dir=rtl]) .tox-comment__reply>:last-child{margin-left:8px}.tox[dir=rtl] .tox-comment__edit{margin-right:8px}.tox[dir=rtl] .tox-comment__buttonspacing>:last-child,.tox[dir=rtl] .tox-comment__edit>:last-child,.tox[dir=rtl] .tox-comment__reply>:last-child{margin-right:8px}.tox .tox-user{align-items:center;display:flex}.tox .tox-user__avatar svg{fill:rgba(34,47,62,.7)}.tox .tox-user__avatar img{border-radius:50%;height:36px;object-fit:cover;vertical-align:middle;width:36px}.tox .tox-user__name{color:#222f3e;font-size:14px;font-style:normal;font-weight:700;line-height:18px;text-transform:none}.tox:not([dir=rtl]) .tox-user__avatar img,.tox:not([dir=rtl]) .tox-user__avatar svg{margin-right:8px}.tox:not([dir=rtl]) .tox-user__avatar+.tox-user__name{margin-left:8px}.tox[dir=rtl] .tox-user__avatar img,.tox[dir=rtl] .tox-user__avatar svg{margin-left:8px}.tox[dir=rtl] .tox-user__avatar+.tox-user__name{margin-right:8px}.tox .tox-dialog-wrap{align-items:center;bottom:0;display:flex;justify-content:center;left:0;position:fixed;right:0;top:0;z-index:1100}.tox .tox-dialog-wrap__backdrop{background-color:rgba(255,255,255,.75);bottom:0;left:0;position:absolute;right:0;top:0;z-index:1}.tox .tox-dialog-wrap__backdrop--opaque{background-color:#fff}.tox .tox-dialog{background-color:#fff;border-color:#eee;border-radius:10px;border-style:solid;border-width:0;box-shadow:0 16px 16px -10px rgba(34,47,62,.15),0 0 40px 1px rgba(34,47,62,.15);display:flex;flex-direction:column;max-height:100%;max-width:480px;overflow:hidden;position:relative;width:95vw;z-index:2}@media only screen and (max-width:767px){body:not(.tox-force-desktop) .tox .tox-dialog{align-self:flex-start;margin:8px auto;max-height:calc(100vh - 8px * 2);width:calc(100vw - 16px)}}.tox .tox-dialog-inline{z-index:1100}.tox .tox-dialog__header{align-items:center;background-color:#fff;border-bottom:none;color:#222f3e;display:flex;font-size:16px;justify-content:space-between;padding:8px 16px 0 16px;position:relative}.tox .tox-dialog__header .tox-button{z-index:1}.tox .tox-dialog__draghandle{cursor:grab;height:100%;left:0;position:absolute;top:0;width:100%}.tox .tox-dialog__draghandle:active{cursor:grabbing}.tox .tox-dialog__dismiss{margin-left:auto}.tox .tox-dialog__title{font-family:-apple-system,BlinkMacSystemFont,\\\"Segoe UI\\\",Roboto,Oxygen-Sans,Ubuntu,Cantarell,\\\"Helvetica Neue\\\",sans-serif;font-size:20px;font-style:normal;font-weight:400;line-height:1.3;margin:0;text-transform:none}.tox .tox-dialog__body{color:#222f3e;display:flex;flex:1;font-size:16px;font-style:normal;font-weight:400;line-height:1.3;min-width:0;text-align:left;text-transform:none}@media only screen and (max-width:767px){body:not(.tox-force-desktop) .tox .tox-dialog__body{flex-direction:column}}.tox .tox-dialog__body-nav{align-items:flex-start;display:flex;flex-direction:column;flex-shrink:0;padding:16px 16px}@media only screen and (min-width:768px){.tox .tox-dialog__body-nav{max-width:11em}}@media only screen and (max-width:767px){body:not(.tox-force-desktop) .tox .tox-dialog__body-nav{flex-direction:row;-webkit-overflow-scrolling:touch;overflow-x:auto;padding-bottom:0}}.tox .tox-dialog__body-nav-item{border-bottom:2px solid transparent;color:rgba(34,47,62,.7);display:inline-block;flex-shrink:0;font-size:14px;line-height:1.3;margin-bottom:8px;max-width:13em;text-decoration:none}.tox .tox-dialog__body-nav-item:focus{background-color:rgba(0,108,231,.1)}.tox .tox-dialog__body-nav-item--active{border-bottom:2px solid #006ce7;color:#006ce7}@media (forced-colors:active){.tox .tox-dialog__body-nav-item--active{border-bottom:2px solid highlight;color:highlight}}.tox .tox-dialog__body-content{box-sizing:border-box;display:flex;flex:1;flex-direction:column;max-height:min(650px,calc(100vh - 110px));overflow:auto;-webkit-overflow-scrolling:touch;padding:16px 16px}.tox .tox-dialog__body-content>*{margin-bottom:0;margin-top:16px}.tox .tox-dialog__body-content>:first-child{margin-top:0}.tox .tox-dialog__body-content>:last-child{margin-bottom:0}.tox .tox-dialog__body-content>:only-child{margin-bottom:0;margin-top:0}.tox .tox-dialog__body-content a{color:#006ce7;cursor:pointer;text-decoration:underline}.tox .tox-dialog__body-content a:focus,.tox .tox-dialog__body-content a:hover{color:#003c81;text-decoration:underline}.tox .tox-dialog__body-content a:focus-visible{border-radius:1px;outline:2px solid #006ce7;outline-offset:2px}.tox .tox-dialog__body-content a:active{color:#00244e;text-decoration:underline}.tox .tox-dialog__body-content svg{fill:#222f3e}.tox .tox-dialog__body-content strong{font-weight:700}.tox .tox-dialog__body-content ul{list-style-type:disc}.tox .tox-dialog__body-content dd,.tox .tox-dialog__body-content ol,.tox .tox-dialog__body-content ul{padding-inline-start:2.5rem}.tox .tox-dialog__body-content dl,.tox .tox-dialog__body-content ol,.tox .tox-dialog__body-content ul{margin-bottom:16px}.tox .tox-dialog__body-content dd,.tox .tox-dialog__body-content dl,.tox .tox-dialog__body-content dt,.tox .tox-dialog__body-content ol,.tox .tox-dialog__body-content ul{display:block;margin-inline-end:0;margin-inline-start:0}.tox .tox-dialog__body-content .tox-form__group h1{color:#222f3e;font-size:20px;font-style:normal;font-weight:700;letter-spacing:normal;margin-bottom:16px;margin-top:2rem;text-transform:none}.tox .tox-dialog__body-content .tox-form__group h2{color:#222f3e;font-size:16px;font-style:normal;font-weight:700;letter-spacing:normal;margin-bottom:16px;margin-top:2rem;text-transform:none}.tox .tox-dialog__body-content .tox-form__group p{margin-bottom:16px}.tox .tox-dialog__body-content .tox-form__group h1:first-child,.tox .tox-dialog__body-content .tox-form__group h2:first-child,.tox .tox-dialog__body-content .tox-form__group p:first-child{margin-top:0}.tox .tox-dialog__body-content .tox-form__group h1:last-child,.tox .tox-dialog__body-content .tox-form__group h2:last-child,.tox .tox-dialog__body-content .tox-form__group p:last-child{margin-bottom:0}.tox .tox-dialog__body-content .tox-form__group h1:only-child,.tox .tox-dialog__body-content .tox-form__group h2:only-child,.tox .tox-dialog__body-content .tox-form__group p:only-child{margin-bottom:0;margin-top:0}.tox .tox-dialog__body-content .tox-form__group .tox-label.tox-label--center{text-align:center}.tox .tox-dialog__body-content .tox-form__group .tox-label.tox-label--end{text-align:end}.tox .tox-dialog--width-lg{height:650px;max-width:1200px}.tox .tox-dialog--fullscreen{height:100%;max-width:100%}.tox .tox-dialog--fullscreen .tox-dialog__body-content{max-height:100%}.tox .tox-dialog--width-md{max-width:800px}.tox .tox-dialog--width-md .tox-dialog__body-content{overflow:auto}.tox .tox-dialog__body-content--centered{text-align:center}.tox .tox-dialog__footer{align-items:center;background-color:#fff;border-top:none;display:flex;justify-content:space-between;padding:8px 16px}.tox .tox-dialog__footer-end,.tox .tox-dialog__footer-start{display:flex}.tox .tox-dialog__busy-spinner{align-items:center;background-color:rgba(255,255,255,.75);bottom:0;display:flex;justify-content:center;left:0;position:absolute;right:0;top:0;z-index:3}.tox .tox-dialog__table{border-collapse:collapse;width:100%}.tox .tox-dialog__table thead th{font-weight:700;padding-bottom:8px}.tox .tox-dialog__table thead th:first-child{padding-right:8px}.tox .tox-dialog__table tbody tr{border-bottom:1px solid #626262}.tox .tox-dialog__table tbody tr:last-child{border-bottom:none}.tox .tox-dialog__table td{padding-bottom:8px;padding-top:8px}.tox .tox-dialog__table td:first-child{padding-right:8px}.tox .tox-dialog__iframe{min-height:200px}.tox .tox-dialog__iframe.tox-dialog__iframe--opaque{background:#fff}.tox .tox-navobj-bordered{position:relative}.tox .tox-navobj-bordered::before{border:1px solid #eee;border-radius:6px;content:'';inset:0;opacity:1;pointer-events:none;position:absolute;z-index:1}.tox .tox-navobj-bordered iframe{border-radius:6px}.tox .tox-navobj-bordered-focus.tox-navobj-bordered::before{border-color:#006ce7;box-shadow:0 0 0 1px #006ce7;outline:0}.tox .tox-dialog__popups{position:absolute;width:100%;z-index:1100}.tox .tox-dialog__body-iframe{display:flex;flex:1;flex-direction:column}.tox .tox-dialog__body-iframe .tox-navobj{display:flex;flex:1}.tox .tox-dialog__body-iframe .tox-navobj :nth-child(2){flex:1;height:100%}.tox .tox-dialog-dock-fadeout{opacity:0;visibility:hidden}.tox .tox-dialog-dock-fadein{opacity:1;visibility:visible}.tox .tox-dialog-dock-transition{transition:visibility 0s linear .3s,opacity .3s ease}.tox .tox-dialog-dock-transition.tox-dialog-dock-fadein{transition-delay:0s}@media only screen and (max-width:767px){body:not(.tox-force-desktop) .tox:not([dir=rtl]) .tox-dialog__body-nav{margin-right:0}}@media only screen and (max-width:767px){body:not(.tox-force-desktop) .tox:not([dir=rtl]) .tox-dialog__body-nav-item:not(:first-child){margin-left:8px}}.tox:not([dir=rtl]) .tox-dialog__footer .tox-dialog__footer-end>*,.tox:not([dir=rtl]) .tox-dialog__footer .tox-dialog__footer-start>*{margin-left:8px}.tox[dir=rtl] .tox-dialog__body{text-align:right}@media only screen and (max-width:767px){body:not(.tox-force-desktop) .tox[dir=rtl] .tox-dialog__body-nav{margin-left:0}}@media only screen and (max-width:767px){body:not(.tox-force-desktop) .tox[dir=rtl] .tox-dialog__body-nav-item:not(:first-child){margin-right:8px}}.tox[dir=rtl] .tox-dialog__footer .tox-dialog__footer-end>*,.tox[dir=rtl] .tox-dialog__footer .tox-dialog__footer-start>*{margin-right:8px}body.tox-dialog__disable-scroll{overflow:hidden}.tox .tox-dropzone-container{display:flex;flex:1}.tox .tox-dropzone{align-items:center;background:#fff;border:2px dashed #eee;box-sizing:border-box;display:flex;flex-direction:column;flex-grow:1;justify-content:center;min-height:100px;padding:10px}.tox .tox-dropzone p{color:rgba(34,47,62,.7);margin:0 0 16px 0}.tox .tox-edit-area{display:flex;flex:1;overflow:hidden;position:relative}.tox .tox-edit-area::before{border:2px solid #006ce7;border-radius:4px;content:'';inset:0;opacity:0;pointer-events:none;position:absolute;transition:opacity .15s;z-index:1}@media (forced-colors:active){.tox .tox-edit-area::before{border:2px solid highlight}}.tox .tox-edit-area__iframe{background-color:#fff;border:0;box-sizing:border-box;flex:1;height:100%;position:absolute;width:100%}.tox.tox-edit-focus .tox-edit-area::before{opacity:1}.tox.tox-inline-edit-area{border:1px dotted #eee}.tox .tox-editor-container{display:flex;flex:1 1 auto;flex-direction:column;overflow:hidden}.tox .tox-editor-header{display:grid;grid-template-columns:1fr min-content;z-index:2}.tox:not(.tox-tinymce-inline) .tox-editor-header{background-color:#fff;border-bottom:none;box-shadow:0 2px 2px -2px rgba(34,47,62,.1),0 8px 8px -4px rgba(34,47,62,.07);padding:4px 0}.tox:not(.tox-tinymce-inline) .tox-editor-header:not(.tox-editor-dock-transition){transition:box-shadow .5s}.tox:not(.tox-tinymce-inline).tox-tinymce--toolbar-bottom .tox-editor-header{border-top:1px solid #e3e3e3;box-shadow:none}.tox:not(.tox-tinymce-inline).tox-tinymce--toolbar-sticky-on .tox-editor-header{background-color:#fff;box-shadow:0 2px 2px -2px rgba(34,47,62,.2),0 8px 8px -4px rgba(34,47,62,.15);padding:4px 0}.tox:not(.tox-tinymce-inline).tox-tinymce--toolbar-sticky-on.tox-tinymce--toolbar-bottom .tox-editor-header{box-shadow:0 2px 2px -2px rgba(34,47,62,.2),0 8px 8px -4px rgba(34,47,62,.15)}.tox.tox:not(.tox-tinymce-inline) .tox-editor-header.tox-editor-header--empty{background:0 0;border:none;box-shadow:none;padding:0}.tox-editor-dock-fadeout{opacity:0;visibility:hidden}.tox-editor-dock-fadein{opacity:1;visibility:visible}.tox-editor-dock-transition{transition:visibility 0s linear .25s,opacity .25s ease}.tox-editor-dock-transition.tox-editor-dock-fadein{transition-delay:0s}.tox .tox-control-wrap{flex:1;position:relative}.tox .tox-control-wrap:not(.tox-control-wrap--status-invalid) .tox-control-wrap__status-icon-invalid,.tox .tox-control-wrap:not(.tox-control-wrap--status-unknown) .tox-control-wrap__status-icon-unknown,.tox .tox-control-wrap:not(.tox-control-wrap--status-valid) .tox-control-wrap__status-icon-valid{display:none}.tox .tox-control-wrap svg{display:block}.tox .tox-control-wrap__status-icon-wrap{position:absolute;top:50%;transform:translateY(-50%)}.tox .tox-control-wrap__status-icon-invalid svg{fill:#c00}.tox .tox-control-wrap__status-icon-unknown svg{fill:orange}.tox .tox-control-wrap__status-icon-valid svg{fill:green}.tox:not([dir=rtl]) .tox-control-wrap--status-invalid .tox-textfield,.tox:not([dir=rtl]) .tox-control-wrap--status-unknown .tox-textfield,.tox:not([dir=rtl]) .tox-control-wrap--status-valid .tox-textfield{padding-right:32px}.tox:not([dir=rtl]) .tox-control-wrap__status-icon-wrap{right:4px}.tox[dir=rtl] .tox-control-wrap--status-invalid .tox-textfield,.tox[dir=rtl] .tox-control-wrap--status-unknown .tox-textfield,.tox[dir=rtl] .tox-control-wrap--status-valid .tox-textfield{padding-left:32px}.tox[dir=rtl] .tox-control-wrap__status-icon-wrap{left:4px}.tox .tox-custom-preview{border-color:#eee;border-radius:6px;border-style:solid;border-width:1px;flex:1;padding:8px}.tox .tox-autocompleter{max-width:25em}.tox .tox-autocompleter .tox-menu{box-sizing:border-box;max-width:25em}.tox .tox-autocompleter .tox-autocompleter-highlight{font-weight:700}.tox .tox-color-input{display:flex;position:relative;z-index:1}.tox .tox-color-input .tox-textfield{z-index:-1}.tox .tox-color-input span{border-color:rgba(34,47,62,.2);border-radius:6px;border-style:solid;border-width:1px;box-shadow:none;box-sizing:border-box;height:24px;position:absolute;top:6px;width:24px}@media (forced-colors:active){.tox .tox-color-input span{border-color:currentColor;border-width:2px!important;forced-color-adjust:none}}.tox .tox-color-input span:focus:not([aria-disabled=true]),.tox .tox-color-input span:hover:not([aria-disabled=true]){border-color:#006ce7;cursor:pointer}.tox .tox-color-input span::before{background-image:linear-gradient(45deg,rgba(0,0,0,.25) 25%,transparent 25%),linear-gradient(-45deg,rgba(0,0,0,.25) 25%,transparent 25%),linear-gradient(45deg,transparent 75%,rgba(0,0,0,.25) 75%),linear-gradient(-45deg,transparent 75%,rgba(0,0,0,.25) 75%);background-position:0 0,0 6px,6px -6px,-6px 0;background-size:12px 12px;border:1px solid #fff;border-radius:6px;box-sizing:border-box;content:'';height:24px;left:-1px;position:absolute;top:-1px;width:24px;z-index:-1}@media (forced-colors:active){.tox .tox-color-input span::before{border:none}}.tox .tox-color-input span[aria-disabled=true]{cursor:not-allowed}.tox:not([dir=rtl]) .tox-color-input .tox-textfield{padding-left:36px}.tox:not([dir=rtl]) .tox-color-input span{left:6px}.tox[dir=rtl] .tox-color-input .tox-textfield{padding-right:36px}.tox[dir=rtl] .tox-color-input span{right:6px}.tox .tox-label,.tox .tox-toolbar-label{color:rgba(34,47,62,.7);display:block;font-size:14px;font-style:normal;font-weight:400;line-height:1.3;padding:0 8px 0 0;text-transform:none;white-space:nowrap}.tox .tox-toolbar-label{padding:0 8px}.tox[dir=rtl] .tox-label{padding:0 0 0 8px}.tox .tox-form{display:flex;flex:1;flex-direction:column}.tox .tox-form__group{box-sizing:border-box;margin-bottom:4px}.tox .tox-form-group--maximize{flex:1}.tox .tox-form__group--error{color:#c00}.tox .tox-form__group--collection{display:flex}.tox .tox-form__grid{display:flex;flex-direction:row;flex-wrap:wrap;justify-content:space-between}.tox .tox-form__grid--2col>.tox-form__group{width:calc(50% - (8px / 2))}.tox .tox-form__grid--3col>.tox-form__group{width:calc(100% / 3 - (8px / 2))}.tox .tox-form__grid--4col>.tox-form__group{width:calc(25% - (8px / 2))}.tox .tox-form__controls-h-stack{align-items:center;display:flex}.tox .tox-form__group--inline{align-items:center;display:flex}.tox .tox-form__group--stretched{display:flex;flex:1;flex-direction:column}.tox .tox-form__group--stretched .tox-textarea{flex:1}.tox .tox-form__group--stretched .tox-navobj{display:flex;flex:1}.tox .tox-form__group--stretched .tox-navobj :nth-child(2){flex:1;height:100%}.tox:not([dir=rtl]) .tox-form__controls-h-stack>:not(:first-child){margin-left:4px}.tox[dir=rtl] .tox-form__controls-h-stack>:not(:first-child){margin-right:4px}.tox .tox-lock.tox-locked .tox-lock-icon__unlock,.tox .tox-lock:not(.tox-locked) .tox-lock-icon__lock{display:none}.tox .tox-listboxfield .tox-listbox--select,.tox .tox-textarea,.tox .tox-textarea-wrap .tox-textarea:focus,.tox .tox-textfield,.tox .tox-toolbar-textfield{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#fff;border-color:#eee;border-radius:6px;border-style:solid;border-width:1px;box-shadow:none;box-sizing:border-box;color:#222f3e;font-family:-apple-system,BlinkMacSystemFont,\\\"Segoe UI\\\",Roboto,Oxygen-Sans,Ubuntu,Cantarell,\\\"Helvetica Neue\\\",sans-serif;font-size:16px;line-height:24px;margin:0;min-height:34px;outline:0;padding:5px 5.5px;resize:none;width:100%}.tox .tox-textarea[disabled],.tox .tox-textfield[disabled]{background-color:#f2f2f2;color:rgba(34,47,62,.85);cursor:not-allowed}.tox .tox-custom-editor:focus-within,.tox .tox-listboxfield .tox-listbox--select:focus,.tox .tox-textarea-wrap:focus-within,.tox .tox-textarea:focus,.tox .tox-textfield:focus{background-color:#fff;border-color:#006ce7;box-shadow:0 0 0 1px #006ce7;outline:0}.tox .tox-toolbar-textfield{border-width:0;margin-bottom:3px;margin-top:2px;max-width:250px}.tox .tox-naked-btn{background-color:transparent;border:0;border-color:transparent;box-shadow:unset;color:#006ce7;cursor:pointer;display:block;margin:0;padding:0}.tox .tox-naked-btn svg{display:block;fill:#222f3e}.tox:not([dir=rtl]) .tox-toolbar-textfield+*{margin-left:4px}.tox[dir=rtl] .tox-toolbar-textfield+*{margin-right:4px}.tox .tox-listboxfield{cursor:pointer;position:relative}.tox .tox-listboxfield .tox-listbox--select[disabled]{background-color:#f2f2f2;color:rgba(34,47,62,.85);cursor:not-allowed}.tox .tox-listbox__select-label{cursor:default;flex:1;margin:0 4px}.tox .tox-listbox__select-chevron{align-items:center;display:flex;justify-content:center;width:16px}.tox .tox-listbox__select-chevron svg{fill:#222f3e}@media (forced-colors:active){.tox .tox-listbox__select-chevron svg{fill:currentColor!important}}.tox .tox-listboxfield .tox-listbox--select{align-items:center;display:flex}.tox:not([dir=rtl]) .tox-listboxfield svg{right:8px}.tox[dir=rtl] .tox-listboxfield svg{left:8px}.tox .tox-selectfield{cursor:pointer;position:relative}.tox .tox-selectfield select{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#fff;border-color:#eee;border-radius:6px;border-style:solid;border-width:1px;box-shadow:none;box-sizing:border-box;color:#222f3e;font-family:-apple-system,BlinkMacSystemFont,\\\"Segoe UI\\\",Roboto,Oxygen-Sans,Ubuntu,Cantarell,\\\"Helvetica Neue\\\",sans-serif;font-size:16px;line-height:24px;margin:0;min-height:34px;outline:0;padding:5px 5.5px;resize:none;width:100%}.tox .tox-selectfield select[disabled]{background-color:#f2f2f2;color:rgba(34,47,62,.85);cursor:not-allowed}.tox .tox-selectfield select::-ms-expand{display:none}.tox .tox-selectfield select:focus{background-color:#fff;border-color:#006ce7;box-shadow:0 0 0 1px #006ce7;outline:0}.tox .tox-selectfield svg{pointer-events:none;position:absolute;top:50%;transform:translateY(-50%)}.tox:not([dir=rtl]) .tox-selectfield select[size=\\\"0\\\"],.tox:not([dir=rtl]) .tox-selectfield select[size=\\\"1\\\"]{padding-right:24px}.tox:not([dir=rtl]) .tox-selectfield svg{right:8px}.tox[dir=rtl] .tox-selectfield select[size=\\\"0\\\"],.tox[dir=rtl] .tox-selectfield select[size=\\\"1\\\"]{padding-left:24px}.tox[dir=rtl] .tox-selectfield svg{left:8px}.tox .tox-textarea-wrap{border-color:#eee;border-radius:6px;border-style:solid;border-width:1px;display:flex;flex:1;overflow:hidden}.tox .tox-textarea{-webkit-appearance:textarea;-moz-appearance:textarea;appearance:textarea;white-space:pre-wrap}.tox .tox-textarea-wrap .tox-textarea{border:none}.tox .tox-textarea-wrap .tox-textarea:focus{border:none}.tox-fullscreen{border:0;height:100%;margin:0;overflow:hidden;overscroll-behavior:none;padding:0;touch-action:pinch-zoom;width:100%}.tox.tox-tinymce.tox-fullscreen .tox-statusbar__resize-handle{display:none}.tox-shadowhost.tox-fullscreen,.tox.tox-tinymce.tox-fullscreen{left:0;position:fixed;top:0;z-index:1200}.tox.tox-tinymce.tox-fullscreen{background-color:transparent}.tox-fullscreen .tox.tox-tinymce-aux,.tox-fullscreen~.tox.tox-tinymce-aux{z-index:1201}.tox .tox-help__more-link{list-style:none;margin-top:1em}.tox .tox-imagepreview{background-color:#666;height:380px;overflow:hidden;position:relative;width:100%}.tox .tox-imagepreview.tox-imagepreview__loaded{overflow:auto}.tox .tox-imagepreview__container{display:flex;left:100vw;position:absolute;top:100vw}.tox .tox-imagepreview__image{background:url(data:image/gif;base64,R0lGODdhDAAMAIABAMzMzP///ywAAAAADAAMAAACFoQfqYeabNyDMkBQb81Uat85nxguUAEAOw==)}.tox .tox-image-tools .tox-spacer{flex:1}.tox .tox-image-tools .tox-bar{align-items:center;display:flex;height:60px;justify-content:center}.tox .tox-image-tools .tox-imagepreview,.tox .tox-image-tools .tox-imagepreview+.tox-bar{margin-top:8px}.tox .tox-image-tools .tox-croprect-block{background:#000;opacity:.5;position:absolute;zoom:1}.tox .tox-image-tools .tox-croprect-handle{border:2px solid #fff;height:20px;left:0;position:absolute;top:0;width:20px}.tox .tox-image-tools .tox-croprect-handle-move{border:0;cursor:move;position:absolute}.tox .tox-image-tools .tox-croprect-handle-nw{border-width:2px 0 0 2px;cursor:nw-resize;left:100px;margin:-2px 0 0 -2px;top:100px}.tox .tox-image-tools .tox-croprect-handle-ne{border-width:2px 2px 0 0;cursor:ne-resize;left:200px;margin:-2px 0 0 -20px;top:100px}.tox .tox-image-tools .tox-croprect-handle-sw{border-width:0 0 2px 2px;cursor:sw-resize;left:100px;margin:-20px 2px 0 -2px;top:200px}.tox .tox-image-tools .tox-croprect-handle-se{border-width:0 2px 2px 0;cursor:se-resize;left:200px;margin:-20px 0 0 -20px;top:200px}.tox .tox-insert-table-picker{background-color:#fff;display:flex;flex-wrap:wrap;width:170px}.tox .tox-insert-table-picker>div{border-color:#eee;border-style:solid;border-width:0 1px 1px 0;box-sizing:border-box;height:17px;width:17px}.tox .tox-collection--list .tox-collection__group .tox-insert-table-picker{margin:-4px -4px}.tox .tox-insert-table-picker .tox-insert-table-picker__selected{background-color:#006ce7;border-color:#eee}@media (forced-colors:active){.tox .tox-insert-table-picker .tox-insert-table-picker__selected{border-color:Highlight;filter:contrast(50%)}}.tox .tox-insert-table-picker__label{color:rgba(34,47,62,.7);display:block;font-size:14px;padding:4px;text-align:center;width:100%}.tox:not([dir=rtl]) .tox-insert-table-picker>div:nth-child(10n){border-right:0}.tox[dir=rtl] .tox-insert-table-picker>div:nth-child(10n+1){border-right:0}.tox .tox-menu{background-color:#fff;border:1px solid transparent;border-radius:6px;box-shadow:0 0 2px 0 rgba(34,47,62,.2),0 4px 8px 0 rgba(34,47,62,.15);display:inline-block;overflow:hidden;vertical-align:top;z-index:1150}.tox .tox-menu.tox-collection.tox-collection--list{padding:0 4px}.tox .tox-menu.tox-collection.tox-collection--toolbar{padding:8px}.tox .tox-menu.tox-collection.tox-collection--grid{padding:8px}@media only screen and (min-width:768px){.tox .tox-menu .tox-collection__item-label{overflow-wrap:break-word;word-break:normal}.tox .tox-dialog__popups .tox-menu .tox-collection__item-label{word-break:break-all}}.tox .tox-menu__label blockquote,.tox .tox-menu__label code,.tox .tox-menu__label h1,.tox .tox-menu__label h2,.tox .tox-menu__label h3,.tox .tox-menu__label h4,.tox .tox-menu__label h5,.tox .tox-menu__label h6,.tox .tox-menu__label p{margin:0}.tox .tox-menubar{background:repeating-linear-gradient(transparent 0 1px,transparent 1px 39px) center top 39px/100% calc(100% - 39px) no-repeat;background-color:#fff;display:flex;flex:0 0 auto;flex-shrink:0;flex-wrap:wrap;grid-column:1/-1;grid-row:1;padding:0 11px 0 12px}.tox .tox-promotion+.tox-menubar{grid-column:1}.tox .tox-promotion{background:repeating-linear-gradient(transparent 0 1px,transparent 1px 39px) center top 39px/100% calc(100% - 39px) no-repeat;background-color:#fff;grid-column:2;grid-row:1;padding-inline-end:8px;padding-inline-start:4px;padding-top:5px}.tox .tox-promotion-link{align-items:unsafe center;background-color:#e8f1f8;border-radius:5px;color:#086be6;cursor:pointer;display:flex;font-size:14px;height:26.6px;padding:4px 8px;white-space:nowrap}.tox .tox-promotion-link:hover{background-color:#b4d7ff}.tox .tox-promotion-link:focus{background-color:#d9edf7}.tox .tox-mbtn{align-items:center;background:#fff;border:0;border-radius:3px;box-shadow:none;color:#222f3e;display:flex;flex:0 0 auto;font-size:14px;font-style:normal;font-weight:400;height:28px;justify-content:center;margin:5px 1px 6px 0;outline:0;padding:0 4px;text-transform:none;width:auto}.tox .tox-mbtn[disabled]{background-color:#fff;border:0;box-shadow:none;color:rgba(34,47,62,.5);cursor:not-allowed}.tox .tox-mbtn:focus:not(:disabled){background:#fff;border:0;box-shadow:none;color:#222f3e;position:relative;z-index:1}.tox .tox-mbtn:focus:not(:disabled)::after{border-radius:3px;bottom:0;box-shadow:0 0 0 2px #006ce7;content:'';left:0;position:absolute;right:0;top:0}@media (forced-colors:active){.tox .tox-mbtn:focus:not(:disabled)::after{border:2px solid highlight}}.tox .tox-mbtn--active,.tox .tox-mbtn:not(:disabled).tox-mbtn--active:focus{background:#a6ccf7;border:0;box-shadow:none;color:#222f3e}.tox .tox-mbtn:hover:not(:disabled):not(.tox-mbtn--active){background:#f0f0f0;border:0;box-shadow:none;color:#222f3e}.tox .tox-mbtn__select-label{cursor:default;font-weight:400;margin:0 4px}.tox .tox-mbtn[disabled] .tox-mbtn__select-label{cursor:not-allowed}.tox .tox-mbtn__select-chevron{align-items:center;display:flex;justify-content:center;width:16px;display:none}.tox .tox-notification{border-radius:6px;border-style:solid;border-width:1px;box-shadow:none;box-sizing:border-box;display:grid;font-size:14px;font-weight:400;grid-template-columns:minmax(40px,1fr) auto minmax(40px,1fr);margin-left:auto;margin-right:auto;margin-top:4px;opacity:0;padding:4px;transition:transform .1s ease-in,opacity 150ms ease-in;width:-moz-max-content;width:max-content}.tox .tox-notification a{cursor:pointer;text-decoration:underline}.tox .tox-notification p{font-size:14px;font-weight:400}.tox .tox-notification:focus{border-color:#006ce7;box-shadow:0 0 0 1px #006ce7}.tox .tox-notification--in{opacity:1}.tox .tox-notification--success{background-color:#e4eeda;border-color:#d7e6c8;color:#222f3e}.tox .tox-notification--success p{color:#222f3e}.tox .tox-notification--success a{color:#517342}.tox .tox-notification--success a:focus,.tox .tox-notification--success a:hover{color:#24321d;text-decoration:underline}.tox .tox-notification--success a:focus-visible{border-radius:1px;outline:2px solid #517342;outline-offset:2px}.tox .tox-notification--success a:active{color:#0d120a;text-decoration:underline}.tox .tox-notification--success svg{fill:#222f3e}.tox .tox-notification--error{background-color:#f5cccc;border-color:#f0b3b3;color:#222f3e}.tox .tox-notification--error p{color:#222f3e}.tox .tox-notification--error a{color:#77181f}.tox .tox-notification--error a:focus,.tox .tox-notification--error a:hover{color:#220709;text-decoration:underline}.tox .tox-notification--error a:focus-visible{border-radius:1px;outline:2px solid #77181f;outline-offset:2px}.tox .tox-notification--error a:active{color:#000;text-decoration:underline}.tox .tox-notification--error svg{fill:#222f3e}.tox .tox-notification--warn,.tox .tox-notification--warning{background-color:#fff5cc;border-color:#fff0b3;color:#222f3e}.tox .tox-notification--warn p,.tox .tox-notification--warning p{color:#222f3e}.tox .tox-notification--warn a,.tox .tox-notification--warning a{color:#7a6e25}.tox .tox-notification--warn a:focus,.tox .tox-notification--warn a:hover,.tox .tox-notification--warning a:focus,.tox .tox-notification--warning a:hover{color:#2c280d;text-decoration:underline}.tox .tox-notification--warn a:focus-visible,.tox .tox-notification--warning a:focus-visible{border-radius:1px;outline:2px solid #7a6e25;outline-offset:2px}.tox .tox-notification--warn a:active,.tox .tox-notification--warning a:active{color:#050502;text-decoration:underline}.tox .tox-notification--warn svg,.tox .tox-notification--warning svg{fill:#222f3e}.tox .tox-notification--info{background-color:#d6e7fb;border-color:#c1dbf9;color:#222f3e}.tox .tox-notification--info p{color:#222f3e}.tox .tox-notification--info a{color:#2a64a6}.tox .tox-notification--info a:focus,.tox .tox-notification--info a:hover{color:#163355;text-decoration:underline}.tox .tox-notification--info a:focus-visible{border-radius:1px;outline:2px solid #2a64a6;outline-offset:2px}.tox .tox-notification--info a:active{color:#0b1a2c;text-decoration:underline}.tox .tox-notification--info svg{fill:#222f3e}.tox .tox-notification__body{align-self:center;color:#222f3e;font-size:14px;grid-column-end:3;grid-column-start:2;grid-row-end:2;grid-row-start:1;text-align:center;white-space:normal;word-break:break-all;word-break:break-word}.tox .tox-notification__body>*{margin:0}.tox .tox-notification__body>*+*{margin-top:1rem}.tox .tox-notification__icon{align-self:center;grid-column-end:2;grid-column-start:1;grid-row-end:2;grid-row-start:1;justify-self:end}.tox .tox-notification__icon svg{display:block}.tox .tox-notification__dismiss{align-self:start;grid-column-end:4;grid-column-start:3;grid-row-end:2;grid-row-start:1;justify-self:end}.tox .tox-notification .tox-progress-bar{grid-column-end:4;grid-column-start:1;grid-row-end:3;grid-row-start:2;justify-self:center}.tox .tox-notification-container-dock-fadeout{opacity:0;visibility:hidden}.tox .tox-notification-container-dock-fadein{opacity:1;visibility:visible}.tox .tox-notification-container-dock-transition{transition:visibility 0s linear .3s,opacity .3s ease}.tox .tox-notification-container-dock-transition.tox-notification-container-dock-fadein{transition-delay:0s}.tox .tox-pop{display:inline-block;position:relative}.tox .tox-pop--resizing{transition:width .1s ease}.tox .tox-pop--resizing .tox-toolbar,.tox .tox-pop--resizing .tox-toolbar__group{flex-wrap:nowrap}.tox .tox-pop--transition{transition:.15s ease;transition-property:left,right,top,bottom}.tox .tox-pop--transition::after,.tox .tox-pop--transition::before{transition:all .15s,visibility 0s,opacity 75ms ease 75ms}.tox .tox-pop__dialog{background-color:#fff;border:1px solid #eee;border-radius:6px;box-shadow:0 0 2px 0 rgba(34,47,62,.2),0 4px 8px 0 rgba(34,47,62,.15);min-width:0;overflow:hidden}.tox .tox-pop__dialog>:not(.tox-toolbar){margin:4px 4px 4px 8px}.tox .tox-pop__dialog .tox-toolbar{background-color:transparent;margin-bottom:-1px}.tox .tox-pop::after,.tox .tox-pop::before{border-style:solid;content:'';display:block;height:0;opacity:1;position:absolute;width:0}@media (forced-colors:active){.tox .tox-pop::after,.tox .tox-pop::before{content:none}}.tox .tox-pop.tox-pop--inset::after,.tox .tox-pop.tox-pop--inset::before{opacity:0;transition:all 0s .15s,visibility 0s,opacity 75ms ease}.tox .tox-pop.tox-pop--bottom::after,.tox .tox-pop.tox-pop--bottom::before{left:50%;top:100%}.tox .tox-pop.tox-pop--bottom::after{border-color:#fff transparent transparent transparent;border-width:8px;margin-left:-8px;margin-top:-1px}.tox .tox-pop.tox-pop--bottom::before{border-color:#eee transparent transparent transparent;border-width:9px;margin-left:-9px}.tox .tox-pop.tox-pop--top::after,.tox .tox-pop.tox-pop--top::before{left:50%;top:0;transform:translateY(-100%)}.tox .tox-pop.tox-pop--top::after{border-color:transparent transparent #fff transparent;border-width:8px;margin-left:-8px;margin-top:1px}.tox .tox-pop.tox-pop--top::before{border-color:transparent transparent #eee transparent;border-width:9px;margin-left:-9px}.tox .tox-pop.tox-pop--left::after,.tox .tox-pop.tox-pop--left::before{left:0;top:calc(50% - 1px);transform:translateY(-50%)}.tox .tox-pop.tox-pop--left::after{border-color:transparent #fff transparent transparent;border-width:8px;margin-left:-15px}.tox .tox-pop.tox-pop--left::before{border-color:transparent #eee transparent transparent;border-width:10px;margin-left:-19px}.tox .tox-pop.tox-pop--right::after,.tox .tox-pop.tox-pop--right::before{left:100%;top:calc(50% + 1px);transform:translateY(-50%)}.tox .tox-pop.tox-pop--right::after{border-color:transparent transparent transparent #fff;border-width:8px;margin-left:-1px}.tox .tox-pop.tox-pop--right::before{border-color:transparent transparent transparent #eee;border-width:10px;margin-left:-1px}.tox .tox-pop.tox-pop--align-left::after,.tox .tox-pop.tox-pop--align-left::before{left:20px}.tox .tox-pop.tox-pop--align-right::after,.tox .tox-pop.tox-pop--align-right::before{left:calc(100% - 20px)}.tox .tox-sidebar-wrap{display:flex;flex-direction:row;flex-grow:1;min-height:0}.tox .tox-sidebar{background-color:#fff;display:flex;flex-direction:row;justify-content:flex-end}.tox .tox-sidebar__slider{display:flex;overflow:hidden}.tox .tox-sidebar__pane-container{display:flex}.tox .tox-sidebar__pane{display:flex}.tox .tox-sidebar--sliding-closed{opacity:0}.tox .tox-sidebar--sliding-open{opacity:1}.tox .tox-sidebar--sliding-growing,.tox .tox-sidebar--sliding-shrinking{transition:width .5s ease,opacity .5s ease}.tox .tox-selector{background-color:#4099ff;border-color:#4099ff;border-style:solid;border-width:1px;box-sizing:border-box;display:inline-block;height:10px;position:absolute;width:10px}.tox.tox-platform-touch .tox-selector{height:12px;width:12px}.tox .tox-slider{align-items:center;display:flex;flex:1;height:24px;justify-content:center;position:relative}.tox .tox-slider__rail{background-color:transparent;border:1px solid #eee;border-radius:6px;height:10px;min-width:120px;width:100%}.tox .tox-slider__handle{background-color:#006ce7;border:2px solid #0054b4;border-radius:6px;box-shadow:none;height:24px;left:50%;position:absolute;top:50%;transform:translateX(-50%) translateY(-50%);width:14px}.tox .tox-form__controls-h-stack>.tox-slider:not(:first-of-type){margin-inline-start:8px}.tox .tox-form__controls-h-stack>.tox-form__group+.tox-slider{margin-inline-start:32px}.tox .tox-form__controls-h-stack>.tox-slider+.tox-form__group{margin-inline-start:32px}.tox .tox-source-code{overflow:auto}.tox .tox-spinner{display:flex}.tox .tox-spinner>div{animation:tam-bouncing-dots 1.5s ease-in-out 0s infinite both;background-color:rgba(34,47,62,.7);border-radius:100%;height:8px;width:8px}.tox .tox-spinner>div:nth-child(1){animation-delay:-.32s}.tox .tox-spinner>div:nth-child(2){animation-delay:-.16s}@keyframes tam-bouncing-dots{0%,100%,80%{transform:scale(0)}40%{transform:scale(1)}}.tox:not([dir=rtl]) .tox-spinner>div:not(:first-child){margin-left:4px}.tox[dir=rtl] .tox-spinner>div:not(:first-child){margin-right:4px}.tox .tox-statusbar{align-items:center;background-color:#fff;border-top:1px solid #e3e3e3;color:rgba(34,47,62,.7);display:flex;flex:0 0 auto;font-size:14px;font-weight:400;height:25px;overflow:hidden;padding:0 8px;position:relative;text-transform:none}.tox .tox-statusbar__path{display:flex;flex:1 1 auto;text-overflow:ellipsis;white-space:nowrap}.tox .tox-statusbar__right-container{display:flex;justify-content:flex-end;white-space:nowrap}.tox .tox-statusbar__help-text{text-align:center}.tox .tox-statusbar__text-container{align-items:flex-start;display:flex;flex:1 1 auto;height:16px;justify-content:space-between;overflow:hidden}@media only screen and (min-width:768px){.tox .tox-statusbar__text-container.tox-statusbar__text-container-3-cols>.tox-statusbar__help-text,.tox .tox-statusbar__text-container.tox-statusbar__text-container-3-cols>.tox-statusbar__path,.tox .tox-statusbar__text-container.tox-statusbar__text-container-3-cols>.tox-statusbar__right-container{flex:0 0 calc(100% / 3)}}.tox .tox-statusbar__text-container.tox-statusbar__text-container--flex-end{justify-content:flex-end}.tox .tox-statusbar__text-container.tox-statusbar__text-container--flex-start{justify-content:flex-start}.tox .tox-statusbar__text-container.tox-statusbar__text-container--space-around{justify-content:space-around}.tox .tox-statusbar__path>*{display:inline;white-space:nowrap}.tox .tox-statusbar__wordcount{flex:0 0 auto;margin-left:1ch}@media only screen and (max-width:767px){.tox .tox-statusbar__text-container .tox-statusbar__help-text{display:none}.tox .tox-statusbar__text-container .tox-statusbar__help-text:only-child{display:block}}.tox .tox-statusbar a,.tox .tox-statusbar__path-item,.tox .tox-statusbar__wordcount{color:rgba(34,47,62,.7);position:relative;text-decoration:none}.tox .tox-statusbar a:focus:not(:disabled):not([aria-disabled=true]),.tox .tox-statusbar a:hover:not(:disabled):not([aria-disabled=true]),.tox .tox-statusbar__path-item:focus:not(:disabled):not([aria-disabled=true]),.tox .tox-statusbar__path-item:hover:not(:disabled):not([aria-disabled=true]),.tox .tox-statusbar__wordcount:focus:not(:disabled):not([aria-disabled=true]),.tox .tox-statusbar__wordcount:hover:not(:disabled):not([aria-disabled=true]){color:#222f3e;cursor:pointer}.tox .tox-statusbar a:focus-visible::after,.tox .tox-statusbar__path-item:focus-visible::after,.tox .tox-statusbar__wordcount:focus-visible::after{border-radius:3px;bottom:0;box-shadow:0 0 0 2px #006ce7;content:'';left:0;position:absolute;right:0;top:0}@media (forced-colors:active){.tox .tox-statusbar a:focus-visible::after,.tox .tox-statusbar__path-item:focus-visible::after,.tox .tox-statusbar__wordcount:focus-visible::after{border:2px solid highlight}}.tox .tox-statusbar__branding svg{fill:rgba(34,47,62,.8);height:1em;margin-left:.3em;width:auto}@media (forced-colors:active){.tox .tox-statusbar__branding svg{fill:currentColor}}.tox .tox-statusbar__branding a{align-items:center;display:inline-flex}.tox .tox-statusbar__branding a:focus:not(:disabled):not([aria-disabled=true]) svg,.tox .tox-statusbar__branding a:hover:not(:disabled):not([aria-disabled=true]) svg{fill:#222f3e}.tox .tox-statusbar__resize-handle{align-items:flex-end;align-self:stretch;cursor:nwse-resize;display:flex;flex:0 0 auto;justify-content:flex-end;margin-bottom:3px;margin-left:4px;margin-right:calc(3px - 8px);margin-top:3px;padding-bottom:0;padding-left:0;padding-right:0;position:relative}.tox .tox-statusbar__resize-handle svg{display:block;fill:rgba(34,47,62,.5)}.tox .tox-statusbar__resize-handle:focus svg,.tox .tox-statusbar__resize-handle:hover svg{fill:#222f3e}.tox .tox-statusbar__resize-handle:focus-visible{background-color:transparent;border-radius:1px 1px 5px 1px;box-shadow:0 0 0 2px transparent}.tox .tox-statusbar__resize-handle:focus-visible::after{border-radius:3px;bottom:0;box-shadow:0 0 0 2px #006ce7;content:'';left:0;position:absolute;right:0;top:0}@media (forced-colors:active){.tox .tox-statusbar__resize-handle:focus-visible::after{border:2px solid highlight}}.tox:not([dir=rtl]) .tox-statusbar__path>*{margin-right:4px}.tox:not([dir=rtl]) .tox-statusbar__branding{margin-left:2ch}.tox[dir=rtl] .tox-statusbar{flex-direction:row-reverse}.tox[dir=rtl] .tox-statusbar__path>*{margin-left:4px}.tox[dir=rtl] .tox-statusbar__branding svg{margin-left:0;margin-right:.3em}.tox .tox-throbber{z-index:1299}.tox .tox-throbber__busy-spinner{align-items:center;background-color:rgba(255,255,255,.6);bottom:0;display:flex;justify-content:center;left:0;position:absolute;right:0;top:0}.tox .tox-tbtn{align-items:center;background:#fff;border:0;border-radius:3px;box-shadow:none;color:#222f3e;display:flex;flex:0 0 auto;font-size:14px;font-style:normal;font-weight:400;height:28px;justify-content:center;margin:6px 1px 5px 0;outline:0;padding:0;text-transform:none;width:34px}@media (forced-colors:active){.tox .tox-tbtn.tox-tbtn:hover,.tox .tox-tbtn:hover{outline:1px dashed currentColor}.tox .tox-tbtn.tox-tbtn--active,.tox .tox-tbtn.tox-tbtn--enabled,.tox .tox-tbtn.tox-tbtn--enabled:focus,.tox .tox-tbtn.tox-tbtn--enabled:hover,.tox .tox-tbtn:focus:not(.tox-tbtn--disabled){outline:1px solid currentColor;position:relative}}.tox .tox-tbtn svg{display:block;fill:#222f3e}@media (forced-colors:active){.tox .tox-tbtn svg{fill:currentColor!important}.tox .tox-tbtn svg.tox-tbtn--enabled,.tox .tox-tbtn svg:focus:not(.tox-tbtn--disabled){fill:currentColor!important}.tox .tox-tbtn svg .tox-tbtn:disabled,.tox .tox-tbtn svg .tox-tbtn:disabled:hover,.tox .tox-tbtn svg.tox-tbtn--disabled,.tox .tox-tbtn svg.tox-tbtn--disabled:hover{filter:contrast(0)}}.tox .tox-tbtn.tox-tbtn-more{padding-left:5px;padding-right:5px;width:inherit}.tox .tox-tbtn:focus{background:#fff;border:0;box-shadow:none;position:relative;z-index:1}.tox .tox-tbtn:focus::after{border-radius:3px;bottom:0;box-shadow:0 0 0 2px #006ce7;content:'';left:0;position:absolute;right:0;top:0}@media (forced-colors:active){.tox .tox-tbtn:focus::after{border:2px solid highlight}}.tox .tox-tbtn:hover{background:#f0f0f0;border:0;box-shadow:none;color:#222f3e}.tox .tox-tbtn:hover svg{fill:#222f3e}.tox .tox-tbtn:active{background:#a6ccf7;border:0;box-shadow:none;color:#222f3e}.tox .tox-tbtn:active svg{fill:#222f3e}.tox .tox-tbtn--disabled .tox-tbtn--enabled svg{fill:rgba(34,47,62,.5)}.tox .tox-tbtn--disabled,.tox .tox-tbtn--disabled:hover,.tox .tox-tbtn:disabled,.tox .tox-tbtn:disabled:hover{background:#fff;border:0;box-shadow:none;color:rgba(34,47,62,.5);cursor:not-allowed}.tox .tox-tbtn--disabled svg,.tox .tox-tbtn--disabled:hover svg,.tox .tox-tbtn:disabled svg,.tox .tox-tbtn:disabled:hover svg{fill:rgba(34,47,62,.5)}.tox .tox-tbtn--active,.tox .tox-tbtn--enabled,.tox .tox-tbtn--enabled:focus,.tox .tox-tbtn--enabled:hover{background:#a6ccf7;border:0;box-shadow:none;color:#222f3e;position:relative}.tox .tox-tbtn--active>*,.tox .tox-tbtn--enabled:focus>*,.tox .tox-tbtn--enabled:hover>*,.tox .tox-tbtn--enabled>*{transform:none}.tox .tox-tbtn--active svg,.tox .tox-tbtn--enabled svg,.tox .tox-tbtn--enabled:focus svg,.tox .tox-tbtn--enabled:hover svg{fill:#222f3e}.tox .tox-tbtn--active.tox-tbtn--disabled svg,.tox .tox-tbtn--enabled.tox-tbtn--disabled svg,.tox .tox-tbtn--enabled:focus.tox-tbtn--disabled svg,.tox .tox-tbtn--enabled:hover.tox-tbtn--disabled svg{fill:rgba(34,47,62,.5)}.tox .tox-tbtn--enabled:focus::after{border-radius:3px;bottom:0;box-shadow:0 0 0 2px #006ce7;content:'';left:0;position:absolute;right:0;top:0}@media (forced-colors:active){.tox .tox-tbtn--enabled:focus::after{border:2px solid highlight}}.tox .tox-tbtn:focus:not(.tox-tbtn--disabled){color:#222f3e}.tox .tox-tbtn:focus:not(.tox-tbtn--disabled) svg{fill:#222f3e}.tox .tox-tbtn:active>*{transform:none}.tox .tox-tbtn--md{height:42px;width:51px}.tox .tox-tbtn--lg{flex-direction:column;height:56px;width:68px}.tox .tox-tbtn--return{align-self:stretch;height:unset;width:16px}.tox .tox-tbtn--labeled{padding:0 4px;width:unset}.tox .tox-tbtn__vlabel{display:block;font-size:10px;font-weight:400;letter-spacing:-.025em;margin-bottom:4px;white-space:nowrap}.tox .tox-number-input{background:#f7f7f7;border-radius:3px;display:flex;margin:6px 1px 5px 0;position:relative;width:auto}.tox .tox-number-input:focus{background:#f7f7f7}.tox .tox-number-input:focus::after{border-radius:3px;bottom:0;box-shadow:0 0 0 2px #006ce7;content:'';left:0;position:absolute;right:0;top:0}@media (forced-colors:active){.tox .tox-number-input:focus::after{border:2px solid highlight}}.tox .tox-number-input .tox-input-wrapper{display:flex;pointer-events:none;position:relative;text-align:center}.tox .tox-number-input .tox-input-wrapper:focus{background-color:#f7f7f7;z-index:1}.tox .tox-number-input .tox-input-wrapper:focus::after{border-radius:3px;bottom:0;box-shadow:0 0 0 2px #006ce7;content:'';left:0;position:absolute;right:0;top:0}@media (forced-colors:active){.tox .tox-number-input .tox-input-wrapper:focus::after{border:2px solid highlight}}.tox .tox-number-input .tox-input-wrapper:has(input:focus)::after{border-radius:3px;bottom:0;box-shadow:0 0 0 2px #006ce7;content:'';left:0;position:absolute;right:0;top:0}@media (forced-colors:active){.tox .tox-number-input .tox-input-wrapper:has(input:focus)::after{border:2px solid highlight}}.tox .tox-number-input input{border-radius:3px;color:#222f3e;font-size:14px;margin:2px 0;pointer-events:all;position:relative;width:60px}.tox .tox-number-input input:hover{background:#f0f0f0;color:#222f3e}.tox .tox-number-input input:focus{background-color:#f7f7f7}.tox .tox-number-input input:disabled{background:#fff;border:0;box-shadow:none;color:rgba(34,47,62,.5);cursor:not-allowed}.tox .tox-number-input button{color:#222f3e;height:28px;position:relative;text-align:center;width:24px}@media (forced-colors:active){.tox .tox-number-input button:active,.tox .tox-number-input button:focus,.tox .tox-number-input button:hover{outline:1px solid currentColor!important}}.tox .tox-number-input button svg{display:block;fill:#222f3e;margin:0 auto;transform:scale(.67)}@media (forced-colors:active){.tox .tox-number-input button svg,.tox .tox-number-input button svg:active,.tox .tox-number-input button svg:hover{fill:currentColor!important}.tox .tox-number-input button svg:disabled{filter:contrast(0)}}.tox .tox-number-input button:focus{background:#f7f7f7;z-index:1}.tox .tox-number-input button:focus::after{border-radius:3px;bottom:0;box-shadow:0 0 0 2px #006ce7;content:'';left:0;position:absolute;right:0;top:0}@media (forced-colors:active){.tox .tox-number-input button:focus::after{border:2px solid highlight}}.tox .tox-number-input button:hover{background:#f0f0f0;border:0;box-shadow:none;color:#222f3e}.tox .tox-number-input button:hover svg{fill:#222f3e}.tox .tox-number-input button:active{background:#a6ccf7;border:0;box-shadow:none;color:#222f3e}.tox .tox-number-input button:active svg{fill:#222f3e}.tox .tox-number-input button:disabled{background:#fff;border:0;box-shadow:none;color:rgba(34,47,62,.5);cursor:not-allowed}.tox .tox-number-input button:disabled svg{fill:rgba(34,47,62,.5)}.tox .tox-number-input button.minus{border-radius:3px 0 0 3px}.tox .tox-number-input button.plus{border-radius:0 3px 3px 0}.tox .tox-number-input:focus:not(:active)>.tox-input-wrapper,.tox .tox-number-input:focus:not(:active)>button{background:#f7f7f7}.tox .tox-tbtn--select{margin:6px 1px 5px 0;padding:0 4px;width:auto}.tox .tox-tbtn__select-label{cursor:default;font-weight:400;height:initial;margin:0 4px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.tox .tox-tbtn__select-chevron{align-items:center;display:flex;justify-content:center;width:16px}.tox .tox-tbtn__select-chevron svg{fill:rgba(34,47,62,.5)}@media (forced-colors:active){.tox .tox-tbtn__select-chevron svg{fill:currentColor}}.tox .tox-tbtn--bespoke{background:#f7f7f7}.tox .tox-tbtn--bespoke:focus{background:#f7f7f7}.tox .tox-tbtn--bespoke+.tox-tbtn--bespoke{margin-inline-start:4px}.tox .tox-tbtn--bespoke .tox-tbtn__select-label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;width:7em}.tox .tox-tbtn--disabled .tox-tbtn__select-label,.tox .tox-tbtn--select:disabled .tox-tbtn__select-label{cursor:not-allowed}.tox .tox-split-button{border:0;border-radius:3px;box-sizing:border-box;display:flex;margin:6px 1px 5px 0}.tox .tox-split-button:hover{box-shadow:0 0 0 1px #f0f0f0 inset}.tox .tox-split-button:focus{background:#fff;box-shadow:none;color:#222f3e;position:relative;z-index:1}.tox .tox-split-button:focus::after{pointer-events:none;border-radius:3px;bottom:0;box-shadow:0 0 0 2px #006ce7;content:'';left:0;position:absolute;right:0;top:0}@media (forced-colors:active){.tox .tox-split-button:focus::after{border:2px solid highlight}}.tox .tox-split-button>*{border-radius:0}.tox .tox-split-button>:nth-child(1){border-bottom-left-radius:3px;border-top-left-radius:3px}.tox .tox-split-button>:nth-child(2){border-bottom-right-radius:3px;border-top-right-radius:3px}.tox .tox-split-button__chevron{width:16px}.tox .tox-split-button__chevron svg{fill:rgba(34,47,62,.5)}@media (forced-colors:active){.tox .tox-split-button__chevron svg{fill:currentColor}}.tox .tox-split-button .tox-tbtn{margin:0}.tox .tox-split-button:focus .tox-tbtn{background-color:transparent}.tox .tox-split-button.tox-tbtn--disabled .tox-tbtn:focus,.tox .tox-split-button.tox-tbtn--disabled .tox-tbtn:hover,.tox .tox-split-button.tox-tbtn--disabled:focus,.tox .tox-split-button.tox-tbtn--disabled:hover{background:#fff;box-shadow:none;color:rgba(34,47,62,.5)}.tox.tox-platform-touch .tox-split-button .tox-tbtn--select{padding:0 0}.tox.tox-platform-touch .tox-split-button .tox-tbtn:not(.tox-tbtn--select):first-child{width:30px}.tox.tox-platform-touch .tox-split-button__chevron{width:20px}.tox .tox-split-button.tox-tbtn--disabled svg #tox-icon-highlight-bg-color__color,.tox .tox-split-button.tox-tbtn--disabled svg #tox-icon-text-color__color{opacity:.6}.tox .tox-toolbar-overlord{background-color:#fff}.tox .tox-toolbar,.tox .tox-toolbar__overflow,.tox .tox-toolbar__primary{background-attachment:local;background-color:#fff;background-image:repeating-linear-gradient(#e3e3e3 0 1px,transparent 1px 39px);background-position:center top 40px;background-repeat:no-repeat;background-size:calc(100% - 11px * 2) calc(100% - 41px);display:flex;flex:0 0 auto;flex-shrink:0;flex-wrap:wrap;padding:0 0;transform:perspective(1px)}.tox .tox-toolbar-overlord>.tox-toolbar,.tox .tox-toolbar-overlord>.tox-toolbar__overflow,.tox .tox-toolbar-overlord>.tox-toolbar__primary{background-position:center top 0;background-size:calc(100% - 11px * 2) calc(100% - 0px)}.tox .tox-toolbar__overflow.tox-toolbar__overflow--closed{height:0;opacity:0;padding-bottom:0;padding-top:0;visibility:hidden}.tox .tox-toolbar__overflow--growing{transition:height .3s ease,opacity .2s linear .1s}.tox .tox-toolbar__overflow--shrinking{transition:opacity .3s ease,height .2s linear .1s,visibility 0s linear .3s}.tox .tox-anchorbar,.tox .tox-toolbar-overlord{grid-column:1/-1}.tox .tox-menubar+.tox-toolbar,.tox .tox-menubar+.tox-toolbar-overlord{border-top:1px solid transparent;margin-top:-1px;padding-bottom:1px;padding-top:1px}@media (forced-colors:active){.tox .tox-menubar+.tox-toolbar,.tox .tox-menubar+.tox-toolbar-overlord{outline:1px solid currentColor}}.tox .tox-toolbar--scrolling{flex-wrap:nowrap;overflow-x:auto}.tox .tox-pop .tox-toolbar{border-width:0}.tox .tox-toolbar--no-divider{background-image:none}.tox .tox-toolbar-overlord .tox-toolbar:not(.tox-toolbar--scrolling):first-child,.tox .tox-toolbar-overlord .tox-toolbar__primary{background-position:center top 39px}.tox .tox-editor-header>.tox-toolbar--scrolling,.tox .tox-toolbar-overlord .tox-toolbar--scrolling:first-child{background-image:none}.tox.tox-tinymce-aux .tox-toolbar__overflow{background-color:#fff;background-position:center top 43px;background-size:calc(100% - 8px * 2) calc(100% - 51px);border:none;border-radius:6px;box-shadow:0 0 2px 0 rgba(34,47,62,.2),0 4px 8px 0 rgba(34,47,62,.15);overscroll-behavior:none;padding:4px 0}@media (forced-colors:active){.tox.tox-tinymce-aux .tox-toolbar__overflow{border:solid}}.tox-pop .tox-pop__dialog .tox-toolbar{background-position:center top 43px;background-size:calc(100% - 11px * 2) calc(100% - 51px);padding:4px 0}.tox .tox-toolbar__group{align-items:center;display:flex;flex-wrap:wrap;margin:0 0;padding:0 11px 0 12px}.tox .tox-toolbar__group--pull-right{margin-left:auto}.tox .tox-toolbar--scrolling .tox-toolbar__group{flex-shrink:0;flex-wrap:nowrap}.tox:not([dir=rtl]) .tox-toolbar__group:not(:last-of-type){border-right:1px solid transparent}.tox[dir=rtl] .tox-toolbar__group:not(:last-of-type){border-left:1px solid transparent}.tox .tox-tooltip{display:inline-block;max-width:15em;padding:8px;pointer-events:none;position:relative;width:-moz-max-content;width:max-content;z-index:1150}.tox .tox-tooltip__body{background-color:#222f3e;border-radius:6px;box-shadow:none;color:#fff;font-size:12px;font-style:normal;font-weight:600;overflow-wrap:break-word;padding:4px 6px;text-transform:none}@media (forced-colors:active){.tox .tox-tooltip__body{outline:outset 1px}}.tox .tox-tooltip__arrow{position:absolute}.tox .tox-tooltip--down .tox-tooltip__arrow{border-left:8px solid transparent;border-right:8px solid transparent;border-top:8px solid #222f3e;bottom:0;left:50%;position:absolute;transform:translateX(-50%)}.tox .tox-tooltip--up .tox-tooltip__arrow{border-bottom:8px solid #222f3e;border-left:8px solid transparent;border-right:8px solid transparent;left:50%;position:absolute;top:0;transform:translateX(-50%)}.tox .tox-tooltip--right .tox-tooltip__arrow{border-bottom:8px solid transparent;border-left:8px solid #222f3e;border-top:8px solid transparent;position:absolute;right:0;top:50%;transform:translateY(-50%)}.tox .tox-tooltip--left .tox-tooltip__arrow{border-bottom:8px solid transparent;border-right:8px solid #222f3e;border-top:8px solid transparent;left:0;position:absolute;top:50%;transform:translateY(-50%)}.tox .tox-tree{display:flex;flex-direction:column}.tox .tox-tree .tox-trbtn{align-items:center;background:0 0;border:0;border-radius:4px;box-shadow:none;color:#222f3e;display:flex;flex:0 0 auto;font-size:14px;font-style:normal;font-weight:400;height:28px;margin-bottom:4px;margin-top:4px;outline:0;overflow:hidden;padding:0;padding-left:8px;text-transform:none}.tox .tox-tree .tox-trbtn .tox-tree__label{cursor:default;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.tox .tox-tree .tox-trbtn svg{display:block;fill:#222f3e}.tox .tox-tree .tox-trbtn:focus{background:#f0f0f0;border:0;box-shadow:none}.tox .tox-tree .tox-trbtn:hover{background:#f0f0f0;border:0;box-shadow:none;color:#222f3e}.tox .tox-tree .tox-trbtn:hover svg{fill:#222f3e}.tox .tox-tree .tox-trbtn:active{background:#a6ccf7;border:0;box-shadow:none;color:#222f3e}.tox .tox-tree .tox-trbtn:active svg{fill:#222f3e}.tox .tox-tree .tox-trbtn--disabled,.tox .tox-tree .tox-trbtn--disabled:hover,.tox .tox-tree .tox-trbtn:disabled,.tox .tox-tree .tox-trbtn:disabled:hover{background:0 0;border:0;box-shadow:none;color:rgba(34,47,62,.5);cursor:not-allowed}.tox .tox-tree .tox-trbtn--disabled svg,.tox .tox-tree .tox-trbtn--disabled:hover svg,.tox .tox-tree .tox-trbtn:disabled svg,.tox .tox-tree .tox-trbtn:disabled:hover svg{fill:rgba(34,47,62,.5)}.tox .tox-tree .tox-trbtn--enabled,.tox .tox-tree .tox-trbtn--enabled:hover{background:#a6ccf7;border:0;box-shadow:none;color:#222f3e}.tox .tox-tree .tox-trbtn--enabled:hover>*,.tox .tox-tree .tox-trbtn--enabled>*{transform:none}.tox .tox-tree .tox-trbtn--enabled svg,.tox .tox-tree .tox-trbtn--enabled:hover svg{fill:#222f3e}.tox .tox-tree .tox-trbtn:focus:not(.tox-trbtn--disabled){color:#222f3e}.tox .tox-tree .tox-trbtn:focus:not(.tox-trbtn--disabled) svg{fill:#222f3e}.tox .tox-tree .tox-trbtn:active>*{transform:none}.tox .tox-tree .tox-trbtn--return{align-self:stretch;height:unset;width:16px}.tox .tox-tree .tox-trbtn--labeled{padding:0 4px;width:unset}.tox .tox-tree .tox-trbtn__vlabel{display:block;font-size:10px;font-weight:400;letter-spacing:-.025em;margin-bottom:4px;white-space:nowrap}.tox .tox-tree .tox-tree--directory{display:flex;flex-direction:column}.tox .tox-tree .tox-tree--directory .tox-tree--directory__label{font-weight:700}.tox .tox-tree .tox-tree--directory .tox-tree--directory__label .tox-mbtn{margin-left:auto}.tox .tox-tree .tox-tree--directory .tox-tree--directory__label .tox-mbtn svg{fill:transparent}.tox .tox-tree .tox-tree--directory .tox-tree--directory__label .tox-mbtn.tox-mbtn--active svg,.tox .tox-tree .tox-tree--directory .tox-tree--directory__label .tox-mbtn:focus svg{fill:#222f3e}.tox .tox-tree .tox-tree--directory .tox-tree--directory__label:focus .tox-mbtn svg,.tox .tox-tree .tox-tree--directory .tox-tree--directory__label:hover .tox-mbtn svg{fill:#222f3e}.tox .tox-tree .tox-tree--directory .tox-tree--directory__label:hover:has(.tox-mbtn:hover){background-color:transparent;color:#222f3e}.tox .tox-tree .tox-tree--directory .tox-tree--directory__label:hover:has(.tox-mbtn:hover) .tox-chevron svg{fill:#222f3e}.tox .tox-tree .tox-tree--directory .tox-tree--directory__label .tox-chevron{margin-right:6px}.tox .tox-tree .tox-tree--directory .tox-tree--directory__label:has(+.tox-tree--directory__children--growing) .tox-chevron,.tox .tox-tree .tox-tree--directory .tox-tree--directory__label:has(+.tox-tree--directory__children--shrinking) .tox-chevron{transition:transform .5s ease-in-out}.tox .tox-tree .tox-tree--directory .tox-tree--directory__label:has(+.tox-tree--directory__children--growing) .tox-chevron,.tox .tox-tree .tox-tree--directory .tox-tree--directory__label:has(+.tox-tree--directory__children--open) .tox-chevron{transform:rotate(90deg)}.tox .tox-tree .tox-tree--leaf__label{font-weight:400}.tox .tox-tree .tox-tree--leaf__label .tox-mbtn{margin-left:auto}.tox .tox-tree .tox-tree--leaf__label .tox-mbtn svg{fill:transparent}.tox .tox-tree .tox-tree--leaf__label .tox-mbtn.tox-mbtn--active svg,.tox .tox-tree .tox-tree--leaf__label .tox-mbtn:focus svg{fill:#222f3e}.tox .tox-tree .tox-tree--leaf__label:hover .tox-mbtn svg{fill:#222f3e}.tox .tox-tree .tox-tree--leaf__label:hover:has(.tox-mbtn:hover){background-color:transparent;color:#222f3e}.tox .tox-tree .tox-tree--leaf__label:hover:has(.tox-mbtn:hover) .tox-chevron svg{fill:#222f3e}.tox .tox-tree .tox-tree--directory__children{overflow:hidden;padding-left:16px}.tox .tox-tree .tox-tree--directory__children.tox-tree--directory__children--growing,.tox .tox-tree .tox-tree--directory__children.tox-tree--directory__children--shrinking{transition:height .5s ease-in-out}.tox .tox-tree .tox-trbtn.tox-tree--leaf__label{display:flex;justify-content:space-between}.tox .tox-revisionhistory__pane{padding:0!important}.tox .tox-revisionhistory__container{display:flex;flex-direction:column;height:100%}.tox .tox-revisionhistory{background-color:#fff;border-radius:4px;border-top:1px solid #eee;display:flex;flex:1;height:100%;margin-top:8px;overflow-x:auto;overflow-y:hidden;position:relative;width:100%}.tox .tox-revisionhistory--align-right{margin-left:auto}.tox .tox-revisionhistory__iframe{flex:1}.tox .tox-revisionhistory__sidebar{border-left:1px solid #eee;height:100%;max-width:360px}.tox .tox-revisionhistory__sidebar .tox-revisionhistory__sidebar-title{border-bottom:1px solid #eee;color:#222f3e;font-size:20px;font-weight:400;height:60px;min-width:192px;padding:16px}.tox .tox-revisionhistory__sidebar .tox-revisionhistory__revisions{flex-direction:column;max-height:calc(100% - 60px);min-width:192px;overflow-y:auto;padding:8px}.tox .tox-revisionhistory__sidebar .tox-revisionhistory__revisions:focus{height:100%;position:relative;z-index:1}.tox .tox-revisionhistory__sidebar .tox-revisionhistory__revisions:focus::after{border-radius:3px;bottom:0;box-shadow:0 0 0 2px #006ce7;content:'';left:0;position:absolute;right:0;top:0;border-radius:6px;bottom:1px;left:1px;right:1px;top:1px}@media (forced-colors:active){.tox .tox-revisionhistory__sidebar .tox-revisionhistory__revisions:focus::after{border:2px solid highlight}}.tox .tox-revisionhistory__sidebar .tox-revisionhistory__revisions .tox-revisionhistory__card{border:1px solid #eee;border-radius:6px;color:#222f3e;cursor:pointer;font-size:14px;margin-bottom:8px;padding:8px;text-overflow:ellipsis;text-wrap:nowrap;width:100%}.tox .tox-revisionhistory__sidebar .tox-revisionhistory__revisions .tox-revisionhistory__card:hover{background-color:#f0f0f0;box-shadow:none;color:#222f3e}.tox .tox-revisionhistory__sidebar .tox-revisionhistory__revisions .tox-revisionhistory__card:focus{position:relative;z-index:1}.tox .tox-revisionhistory__sidebar .tox-revisionhistory__revisions .tox-revisionhistory__card:focus::after{border-radius:6px!important;border-radius:3px;bottom:0;box-shadow:0 0 0 2px #006ce7;content:'';left:0;position:absolute;right:0;top:0}@media (forced-colors:active){.tox .tox-revisionhistory__sidebar .tox-revisionhistory__revisions .tox-revisionhistory__card:focus::after{border:2px solid highlight}}.tox .tox-revisionhistory__sidebar .tox-revisionhistory__revisions .tox-revisionhistory__card.tox-revisionhistory__card--selected{background-color:#a6ccf7;box-shadow:none;color:#222f3e}.tox .tox-revisionhistory__sidebar .tox-revisionhistory__revisions .tox-revisionhistory__norevision{color:rgba(34,47,62,.7);font-size:16px;line-height:24px;padding:5px 5.5px}.tox .tox-view-wrap,.tox .tox-view-wrap__slot-container{background-color:#fff;display:flex;flex:1;flex-direction:column;height:100%}.tox .tox-view{display:flex;flex:1 1 auto;flex-direction:column;overflow:hidden}.tox .tox-view__header{align-items:center;display:flex;font-size:16px;justify-content:space-between;padding:10px 10px 2px 10px;position:relative}.tox .tox-view__label{color:#222f3e;font-weight:700;line-height:24px;padding:4px 16px;text-align:center;white-space:nowrap}.tox .tox-view__label--normal{font-size:16px}.tox .tox-view__label--large{font-size:20px}.tox .tox-view--mobile.tox-view__header,.tox .tox-view--mobile.tox-view__toolbar{padding:8px}.tox .tox-view--scrolling{flex-wrap:nowrap;overflow-x:auto}.tox .tox-view__toolbar{display:flex;flex-direction:row;gap:8px;justify-content:space-between;overflow-x:auto;padding:10px 10px 2px 10px}.tox .tox-view__toolbar__group{display:flex;flex-direction:row;gap:12px}.tox .tox-view__header-end,.tox .tox-view__header-start{display:flex}.tox .tox-view__pane{height:100%;padding:8px;position:relative;width:100%}.tox .tox-view__pane_panel{border:1px solid #eee;border-radius:6px}.tox:not([dir=rtl]) .tox-view__header .tox-view__header-end>*,.tox:not([dir=rtl]) .tox-view__header .tox-view__header-start>*{margin-left:8px}.tox[dir=rtl] .tox-view__header .tox-view__header-end>*,.tox[dir=rtl] .tox-view__header .tox-view__header-start>*{margin-right:8px}.tox .tox-well{border:1px solid #eee;border-radius:6px;padding:8px;width:100%}.tox .tox-well>:first-child{margin-top:0}.tox .tox-well>:last-child{margin-bottom:0}.tox .tox-well>:only-child{margin:0}.tox .tox-custom-editor{border:1px solid #eee;border-radius:6px;display:flex;flex:1;overflow:hidden;position:relative}.tox .tox-dialog-loading::before{background-color:rgba(0,0,0,.5);content:\\\"\\\";height:100%;position:absolute;width:100%;z-index:1000}.tox .tox-tab{cursor:pointer}.tox .tox-dialog__content-js{display:flex;flex:1}.tox .tox-dialog__body-content .tox-collection{display:flex;flex:1}\")\n//# sourceMappingURL=skin.js.map\n"
  },
  {
    "path": "apps/web-antd/public/tinymce/skins/ui/oxide/skin.shadowdom.js",
    "content": "tinymce.Resource.add('ui/default/skin.shadowdom.css', \"body.tox-dialog__disable-scroll{overflow:hidden}.tox-fullscreen{border:0;height:100%;margin:0;overflow:hidden;overscroll-behavior:none;padding:0;touch-action:pinch-zoom;width:100%}.tox.tox-tinymce.tox-fullscreen .tox-statusbar__resize-handle{display:none}.tox-shadowhost.tox-fullscreen,.tox.tox-tinymce.tox-fullscreen{left:0;position:fixed;top:0;z-index:1200}.tox.tox-tinymce.tox-fullscreen{background-color:transparent}.tox-fullscreen .tox.tox-tinymce-aux,.tox-fullscreen~.tox.tox-tinymce-aux{z-index:1201}\")\n//# sourceMappingURL=skin.shadowdom.js.map\n"
  },
  {
    "path": "apps/web-antd/public/tinymce/skins/ui/oxide-dark/content.inline.js",
    "content": "tinymce.Resource.add('ui/dark/content.inline.css', \".mce-content-body .mce-item-anchor{background:transparent url(\\\"data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D'8'%20height%3D'12'%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%3E%3Cpath%20d%3D'M0%200L8%200%208%2012%204.09117821%209%200%2012z'%2F%3E%3C%2Fsvg%3E%0A\\\") no-repeat center}.mce-content-body .mce-item-anchor:empty{cursor:default;display:inline-block;height:12px!important;padding:0 2px;-webkit-user-modify:read-only;-moz-user-modify:read-only;-webkit-user-select:all;-moz-user-select:all;user-select:all;width:8px!important}.mce-content-body .mce-item-anchor:not(:empty){background-position-x:2px;display:inline-block;padding-left:12px}.mce-content-body .mce-item-anchor[data-mce-selected]{outline-offset:1px}.tox-comments-visible .tox-comment[contenteditable=false]:not([data-mce-selected]),.tox-comments-visible span.tox-comment img:not([data-mce-selected]),.tox-comments-visible span.tox-comment span.mce-preview-object:not([data-mce-selected]),.tox-comments-visible span.tox-comment>audio:not([data-mce-selected]),.tox-comments-visible span.tox-comment>video:not([data-mce-selected]){outline:3px solid #ffe89d}.tox-comments-visible .tox-comment[contenteditable=false][data-mce-annotation-active=true]:not([data-mce-selected]){outline:3px solid #fed635}.tox-comments-visible span.tox-comment[data-mce-annotation-active=true] img:not([data-mce-selected]),.tox-comments-visible span.tox-comment[data-mce-annotation-active=true] span.mce-preview-object:not([data-mce-selected]),.tox-comments-visible span.tox-comment[data-mce-annotation-active=true]>audio:not([data-mce-selected]),.tox-comments-visible span.tox-comment[data-mce-annotation-active=true]>video:not([data-mce-selected]){outline:3px solid #fed635}.tox-comments-visible span.tox-comment:not([data-mce-selected]){background-color:#ffe89d;outline:0}.tox-comments-visible span.tox-comment[data-mce-annotation-active=true]:not([data-mce-selected=inline-boundary]){background-color:#fed635}.tox-checklist>li:not(.tox-checklist--hidden){list-style:none;margin:.25em 0}.tox-checklist>li:not(.tox-checklist--hidden)::before{content:url(\\\"data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2216%22%20height%3D%2216%22%20viewBox%3D%220%200%2016%2016%22%3E%3Cg%20id%3D%22checklist-unchecked%22%20fill%3D%22none%22%20fill-rule%3D%22evenodd%22%3E%3Crect%20id%3D%22Rectangle%22%20width%3D%2215%22%20height%3D%2215%22%20x%3D%22.5%22%20y%3D%22.5%22%20fill-rule%3D%22nonzero%22%20stroke%3D%22%234C4C4C%22%20rx%3D%222%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E%0A\\\");cursor:pointer;height:1em;margin-left:-1.5em;margin-top:.125em;position:absolute;width:1em}.tox-checklist li:not(.tox-checklist--hidden).tox-checklist--checked::before{content:url(\\\"data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2216%22%20height%3D%2216%22%20viewBox%3D%220%200%2016%2016%22%3E%3Cg%20id%3D%22checklist-checked%22%20fill%3D%22none%22%20fill-rule%3D%22evenodd%22%3E%3Crect%20id%3D%22Rectangle%22%20width%3D%2216%22%20height%3D%2216%22%20fill%3D%22%234099FF%22%20fill-rule%3D%22nonzero%22%20rx%3D%222%22%2F%3E%3Cpath%20id%3D%22Path%22%20fill%3D%22%23FFF%22%20fill-rule%3D%22nonzero%22%20d%3D%22M11.5703186%2C3.14417309%20C11.8516238%2C2.73724603%2012.4164781%2C2.62829933%2012.83558%2C2.89774797%20C13.260121%2C3.17069355%2013.3759736%2C3.72932262%2013.0909105%2C4.14168582%20L7.7580587%2C11.8560195%20C7.43776896%2C12.3193404%206.76483983%2C12.3852142%206.35607322%2C11.9948725%20L3.02491697%2C8.8138662%20C2.66090143%2C8.46625845%202.65798871%2C7.89594698%203.01850234%2C7.54483354%20C3.373942%2C7.19866177%203.94940006%2C7.19592841%204.30829608%2C7.5386474%20L6.85276923%2C9.9684299%20L11.5703186%2C3.14417309%20Z%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E%0A\\\")}[dir=rtl] .tox-checklist>li:not(.tox-checklist--hidden)::before{margin-left:0;margin-right:-1.5em}code[class*=language-],pre[class*=language-]{color:#000;background:0 0;text-shadow:0 1px #fff;font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;tab-size:4;-webkit-hyphens:none;hyphens:none}code[class*=language-] ::-moz-selection,code[class*=language-]::-moz-selection,pre[class*=language-] ::-moz-selection,pre[class*=language-]::-moz-selection{text-shadow:none;background:#b3d4fc}code[class*=language-] ::selection,code[class*=language-]::selection,pre[class*=language-] ::selection,pre[class*=language-]::selection{text-shadow:none;background:#b3d4fc}@media print{code[class*=language-],pre[class*=language-]{text-shadow:none}}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto}:not(pre)>code[class*=language-],pre[class*=language-]{background:#f5f2f0}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#708090}.token.punctuation{color:#999}.token.namespace{opacity:.7}.token.boolean,.token.constant,.token.deleted,.token.number,.token.property,.token.symbol,.token.tag{color:#905}.token.attr-name,.token.builtin,.token.char,.token.inserted,.token.selector,.token.string{color:#690}.language-css .token.string,.style .token.string,.token.entity,.token.operator,.token.url{color:#9a6e3a;background:hsla(0,0%,100%,.5)}.token.atrule,.token.attr-value,.token.keyword{color:#07a}.token.class-name,.token.function{color:#dd4a68}.token.important,.token.regex,.token.variable{color:#e90}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}.mce-content-body{overflow-wrap:break-word;word-wrap:break-word}.mce-content-body .mce-visual-caret{background-color:#000;background-color:currentColor;position:absolute}.mce-content-body .mce-visual-caret-hidden{display:none}.mce-content-body [data-mce-caret]{left:-1000px;margin:0;padding:0;position:absolute;right:auto;top:0}.mce-content-body .mce-offscreen-selection{left:-2000000px;max-width:1000000px;position:absolute}.mce-content-body [contentEditable=false]{cursor:default}.mce-content-body [contentEditable=true]{cursor:text}.tox-cursor-format-painter{cursor:url(\\\"data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%20viewBox%3D%220%200%2024%2024%22%3E%0A%20%20%3Cg%20fill%3D%22none%22%20fill-rule%3D%22evenodd%22%3E%0A%20%20%20%20%3Cpath%20fill%3D%22%23000%22%20fill-rule%3D%22nonzero%22%20d%3D%22M15%2C6%20C15%2C5.45%2014.55%2C5%2014%2C5%20L6%2C5%20C5.45%2C5%205%2C5.45%205%2C6%20L5%2C10%20C5%2C10.55%205.45%2C11%206%2C11%20L14%2C11%20C14.55%2C11%2015%2C10.55%2015%2C10%20L15%2C9%20L16%2C9%20L16%2C12%20L9%2C12%20L9%2C19%20C9%2C19.55%209.45%2C20%2010%2C20%20L11%2C20%20C11.55%2C20%2012%2C19.55%2012%2C19%20L12%2C14%20L18%2C14%20L18%2C7%20L15%2C7%20L15%2C6%20Z%22%2F%3E%0A%20%20%20%20%3Cpath%20fill%3D%22%23000%22%20fill-rule%3D%22nonzero%22%20d%3D%22M1%2C1%20L8.25%2C1%20C8.66421356%2C1%209%2C1.33578644%209%2C1.75%20L9%2C1.75%20C9%2C2.16421356%208.66421356%2C2.5%208.25%2C2.5%20L2.5%2C2.5%20L2.5%2C8.25%20C2.5%2C8.66421356%202.16421356%2C9%201.75%2C9%20L1.75%2C9%20C1.33578644%2C9%201%2C8.66421356%201%2C8.25%20L1%2C1%20Z%22%2F%3E%0A%20%20%3C%2Fg%3E%0A%3C%2Fsvg%3E%0A\\\"),default}div.mce-footnotes hr{margin-inline-end:auto;margin-inline-start:0;width:25%}div.mce-footnotes li>a.mce-footnotes-backlink{text-decoration:none}@media print{sup.mce-footnote a{color:#000;text-decoration:none}div.mce-footnotes{break-inside:avoid;width:100%}div.mce-footnotes li>a.mce-footnotes-backlink{display:none}}tiny-math-block{display:flex;justify-content:center;margin:16px 0 16px 0}tiny-math-inline{display:inline-block}.mce-content-body figure.align-left{float:left}.mce-content-body figure.align-right{float:right}.mce-content-body figure.image.align-center{display:table;margin-left:auto;margin-right:auto}.mce-preview-object{border:1px solid gray;display:inline-block;line-height:0;margin:0 2px 0 2px;position:relative}.mce-preview-object .mce-shim{background:url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7);height:100%;left:0;position:absolute;top:0;width:100%}.mce-preview-object[data-mce-selected=\\\"2\\\"] .mce-shim{display:none}.mce-content-body .mce-mergetag{cursor:default!important;-webkit-user-select:none;-moz-user-select:none;user-select:none}.mce-content-body .mce-mergetag:hover{background-color:rgba(0,108,231,.1)}.mce-content-body .mce-mergetag-affix{background-color:rgba(0,108,231,.1);color:#006ce7}.mce-object{background:transparent url(\\\"data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%3E%3Cpath%20d%3D%22M4%203h16a1%201%200%200%201%201%201v16a1%201%200%200%201-1%201H4a1%201%200%200%201-1-1V4a1%201%200%200%201%201-1zm1%202v14h14V5H5zm4.79%202.565l5.64%204.028a.5.5%200%200%201%200%20.814l-5.64%204.028a.5.5%200%200%201-.79-.407V7.972a.5.5%200%200%201%20.79-.407z%22%2F%3E%3C%2Fsvg%3E%0A\\\") no-repeat center;border:1px dashed #aaa}.mce-pagebreak{border:1px dashed #aaa;cursor:default;display:block;height:5px;margin-top:15px;page-break-before:always;width:100%}@media print{.mce-pagebreak{border:0}}.tiny-pageembed .mce-shim{background:url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7);height:100%;left:0;position:absolute;top:0;width:100%}.tiny-pageembed[data-mce-selected=\\\"2\\\"] .mce-shim{display:none}.tiny-pageembed{display:inline-block;position:relative}.tiny-pageembed--16by9,.tiny-pageembed--1by1,.tiny-pageembed--21by9,.tiny-pageembed--4by3{display:block;overflow:hidden;padding:0;position:relative;width:100%}.tiny-pageembed--21by9{padding-top:42.857143%}.tiny-pageembed--16by9{padding-top:56.25%}.tiny-pageembed--4by3{padding-top:75%}.tiny-pageembed--1by1{padding-top:100%}.tiny-pageembed--16by9 iframe,.tiny-pageembed--1by1 iframe,.tiny-pageembed--21by9 iframe,.tiny-pageembed--4by3 iframe{border:0;height:100%;left:0;position:absolute;top:0;width:100%}.mce-content-body[data-mce-placeholder]{position:relative}.mce-content-body[data-mce-placeholder]:not(.mce-visualblocks)::before{color:rgba(34,47,62,.7);content:attr(data-mce-placeholder);position:absolute}@media (forced-colors:active){.mce-content-body[data-mce-placeholder]:not(.mce-visualblocks)::before{color:highlight;filter:brightness(30%);z-index:-1}}.mce-content-body:not([dir=rtl])[data-mce-placeholder]:not(.mce-visualblocks)::before{left:1px}.mce-content-body[dir=rtl][data-mce-placeholder]:not(.mce-visualblocks)::before{right:1px}.mce-content-body div.mce-resizehandle{background-color:#4099ff;border-color:#4099ff;border-style:solid;border-width:1px;box-sizing:border-box;height:10px;position:absolute;width:10px;z-index:1298}.mce-content-body div.mce-resizehandle:hover{background-color:#4099ff}.mce-content-body div.mce-resizehandle:nth-of-type(1){cursor:nwse-resize}.mce-content-body div.mce-resizehandle:nth-of-type(2){cursor:nesw-resize}.mce-content-body div.mce-resizehandle:nth-of-type(3){cursor:nwse-resize}.mce-content-body div.mce-resizehandle:nth-of-type(4){cursor:nesw-resize}.mce-content-body .mce-resize-backdrop{z-index:10000}.mce-content-body .mce-clonedresizable{cursor:default;opacity:.5;outline:1px dashed #000;position:absolute;z-index:10001}.mce-content-body .mce-clonedresizable.mce-resizetable-columns td,.mce-content-body .mce-clonedresizable.mce-resizetable-columns th{border:0}.mce-content-body .mce-resize-helper{background:#555;background:rgba(0,0,0,.75);border:1px;border-radius:3px;color:#fff;display:none;font-family:sans-serif;font-size:12px;line-height:14px;margin:5px 10px;padding:5px;position:absolute;white-space:nowrap;z-index:10002}.tox-rtc-user-selection{position:relative}.tox-rtc-user-cursor{bottom:0;cursor:default;position:absolute;top:0;width:2px}.tox-rtc-user-cursor::before{background-color:inherit;border-radius:50%;content:'';display:block;height:8px;position:absolute;right:-3px;top:-3px;width:8px}.tox-rtc-user-cursor:hover::after{background-color:inherit;border-radius:100px;box-sizing:border-box;color:#fff;content:attr(data-user);display:block;font-size:12px;font-weight:700;left:-5px;min-height:8px;min-width:8px;padding:0 12px;position:absolute;top:-11px;white-space:nowrap;z-index:1000}.tox-rtc-user-selection--1 .tox-rtc-user-cursor{background-color:#2dc26b}.tox-rtc-user-selection--2 .tox-rtc-user-cursor{background-color:#e03e2d}.tox-rtc-user-selection--3 .tox-rtc-user-cursor{background-color:#f1c40f}.tox-rtc-user-selection--4 .tox-rtc-user-cursor{background-color:#3598db}.tox-rtc-user-selection--5 .tox-rtc-user-cursor{background-color:#b96ad9}.tox-rtc-user-selection--6 .tox-rtc-user-cursor{background-color:#e67e23}.tox-rtc-user-selection--7 .tox-rtc-user-cursor{background-color:#aaa69d}.tox-rtc-user-selection--8 .tox-rtc-user-cursor{background-color:#f368e0}.tox-rtc-remote-image{background:#eaeaea url(\\\"data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%2236%22%20height%3D%2212%22%20viewBox%3D%220%200%2036%2012%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%3Ccircle%20cx%3D%226%22%20cy%3D%226%22%20r%3D%223%22%20fill%3D%22rgba(0%2C%200%2C%200%2C%20.2)%22%3E%0A%20%20%20%20%3Canimate%20attributeName%3D%22r%22%20values%3D%223%3B5%3B3%22%20calcMode%3D%22linear%22%20dur%3D%221s%22%20repeatCount%3D%22indefinite%22%20%2F%3E%0A%20%20%3C%2Fcircle%3E%0A%20%20%3Ccircle%20cx%3D%2218%22%20cy%3D%226%22%20r%3D%223%22%20fill%3D%22rgba(0%2C%200%2C%200%2C%20.2)%22%3E%0A%20%20%20%20%3Canimate%20attributeName%3D%22r%22%20values%3D%223%3B5%3B3%22%20calcMode%3D%22linear%22%20begin%3D%22.33s%22%20dur%3D%221s%22%20repeatCount%3D%22indefinite%22%20%2F%3E%0A%20%20%3C%2Fcircle%3E%0A%20%20%3Ccircle%20cx%3D%2230%22%20cy%3D%226%22%20r%3D%223%22%20fill%3D%22rgba(0%2C%200%2C%200%2C%20.2)%22%3E%0A%20%20%20%20%3Canimate%20attributeName%3D%22r%22%20values%3D%223%3B5%3B3%22%20calcMode%3D%22linear%22%20begin%3D%22.66s%22%20dur%3D%221s%22%20repeatCount%3D%22indefinite%22%20%2F%3E%0A%20%20%3C%2Fcircle%3E%0A%3C%2Fsvg%3E%0A\\\") no-repeat center center;border:1px solid #ccc;min-height:240px;min-width:320px}.mce-match-marker{background:#aaa;color:#fff}.mce-match-marker-selected{background:#39f;color:#fff}.mce-match-marker-selected::-moz-selection{background:#39f;color:#fff}.mce-match-marker-selected::selection{background:#39f;color:#fff}.mce-content-body audio[data-mce-selected],.mce-content-body details[data-mce-selected],.mce-content-body embed[data-mce-selected],.mce-content-body img[data-mce-selected],.mce-content-body object[data-mce-selected],.mce-content-body table[data-mce-selected],.mce-content-body video[data-mce-selected]{outline:3px solid #b4d7ff}.mce-content-body hr[data-mce-selected]{outline:3px solid #b4d7ff;outline-offset:1px}.mce-content-body [contentEditable=false] [contentEditable=true]:focus{outline:3px solid #b4d7ff}.mce-content-body [contentEditable=false] [contentEditable=true]:hover{outline:3px solid #b4d7ff}.mce-content-body [contentEditable=false][data-mce-selected]{cursor:not-allowed;outline:3px solid #b4d7ff}.mce-content-body.mce-content-readonly [contentEditable=true]:focus,.mce-content-body.mce-content-readonly [contentEditable=true]:hover{outline:0}.mce-content-body [data-mce-selected=inline-boundary]{background-color:#b4d7ff}.mce-content-body .mce-edit-focus{outline:3px solid #b4d7ff}.mce-content-body td[data-mce-selected],.mce-content-body th[data-mce-selected]{position:relative}.mce-content-body td[data-mce-selected]::-moz-selection,.mce-content-body th[data-mce-selected]::-moz-selection{background:0 0}.mce-content-body td[data-mce-selected]::selection,.mce-content-body th[data-mce-selected]::selection{background:0 0}.mce-content-body td[data-mce-selected] *,.mce-content-body th[data-mce-selected] *{outline:0;-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}.mce-content-body td[data-mce-selected]::after,.mce-content-body th[data-mce-selected]::after{background-color:rgba(180,215,255,.7);border:1px solid rgba(180,215,255,.7);bottom:-1px;content:'';left:-1px;mix-blend-mode:multiply;position:absolute;right:-1px;top:-1px}@media screen and (-ms-high-contrast:active),(-ms-high-contrast:none){.mce-content-body td[data-mce-selected]::after,.mce-content-body th[data-mce-selected]::after{border-color:rgba(0,84,180,.7)}}.mce-content-body img[data-mce-selected]::-moz-selection{background:0 0}.mce-content-body img[data-mce-selected]::selection{background:0 0}.ephox-snooker-resizer-bar{background-color:#b4d7ff;opacity:0;-webkit-user-select:none;-moz-user-select:none;user-select:none}.ephox-snooker-resizer-cols{cursor:col-resize}.ephox-snooker-resizer-rows{cursor:row-resize}.ephox-snooker-resizer-bar.ephox-snooker-resizer-bar-dragging{opacity:1}.mce-spellchecker-word{background-image:url(\\\"data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D'4'%20height%3D'4'%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%3E%3Cpath%20stroke%3D'%23ff0000'%20fill%3D'none'%20stroke-linecap%3D'round'%20stroke-opacity%3D'.75'%20d%3D'M0%203L2%201%204%203'%2F%3E%3C%2Fsvg%3E%0A\\\");background-position:0 calc(100% + 1px);background-repeat:repeat-x;background-size:auto 6px;cursor:default;height:2rem}.mce-spellchecker-grammar{background-image:url(\\\"data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D'4'%20height%3D'4'%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%3E%3Cpath%20stroke%3D'%2300A835'%20fill%3D'none'%20stroke-linecap%3D'round'%20d%3D'M0%203L2%201%204%203'%2F%3E%3C%2Fsvg%3E%0A\\\");background-position:0 calc(100% + 1px);background-repeat:repeat-x;background-size:auto 6px;cursor:default}.mce-toc{border:1px solid gray}.mce-toc h2{margin:4px}.mce-toc ul>li{list-style-type:none}[data-mce-block]{display:block}.mce-item-table:not([border]),.mce-item-table:not([border]) caption,.mce-item-table:not([border]) td,.mce-item-table:not([border]) th,.mce-item-table[border=\\\"0\\\"],.mce-item-table[border=\\\"0\\\"] caption,.mce-item-table[border=\\\"0\\\"] td,.mce-item-table[border=\\\"0\\\"] th,table[style*=\\\"border-width: 0px\\\"],table[style*=\\\"border-width: 0px\\\"] caption,table[style*=\\\"border-width: 0px\\\"] td,table[style*=\\\"border-width: 0px\\\"] th{border:1px dashed #bbb}.mce-visualblocks address,.mce-visualblocks article,.mce-visualblocks aside,.mce-visualblocks blockquote,.mce-visualblocks div:not([data-mce-bogus]),.mce-visualblocks dl,.mce-visualblocks figcaption,.mce-visualblocks figure,.mce-visualblocks h1,.mce-visualblocks h2,.mce-visualblocks h3,.mce-visualblocks h4,.mce-visualblocks h5,.mce-visualblocks h6,.mce-visualblocks hgroup,.mce-visualblocks ol,.mce-visualblocks p,.mce-visualblocks pre,.mce-visualblocks section,.mce-visualblocks ul{background-repeat:no-repeat;border:1px dashed #bbb;margin-left:3px;padding-top:10px}.mce-visualblocks p{background-image:url(data:image/gif;base64,R0lGODlhCQAJAJEAAAAAAP///7u7u////yH5BAEAAAMALAAAAAAJAAkAAAIQnG+CqCN/mlyvsRUpThG6AgA7)}.mce-visualblocks h1{background-image:url(data:image/gif;base64,R0lGODlhDQAKAIABALu7u////yH5BAEAAAEALAAAAAANAAoAAAIXjI8GybGu1JuxHoAfRNRW3TWXyF2YiRUAOw==)}.mce-visualblocks h2{background-image:url(data:image/gif;base64,R0lGODlhDgAKAIABALu7u////yH5BAEAAAEALAAAAAAOAAoAAAIajI8Hybbx4oOuqgTynJd6bGlWg3DkJzoaUAAAOw==)}.mce-visualblocks h3{background-image:url(data:image/gif;base64,R0lGODlhDgAKAIABALu7u////yH5BAEAAAEALAAAAAAOAAoAAAIZjI8Hybbx4oOuqgTynJf2Ln2NOHpQpmhAAQA7)}.mce-visualblocks h4{background-image:url(data:image/gif;base64,R0lGODlhDgAKAIABALu7u////yH5BAEAAAEALAAAAAAOAAoAAAIajI8HybbxInR0zqeAdhtJlXwV1oCll2HaWgAAOw==)}.mce-visualblocks h5{background-image:url(data:image/gif;base64,R0lGODlhDgAKAIABALu7u////yH5BAEAAAEALAAAAAAOAAoAAAIajI8HybbxIoiuwjane4iq5GlW05GgIkIZUAAAOw==)}.mce-visualblocks h6{background-image:url(data:image/gif;base64,R0lGODlhDgAKAIABALu7u////yH5BAEAAAEALAAAAAAOAAoAAAIajI8HybbxIoiuwjan04jep1iZ1XRlAo5bVgAAOw==)}.mce-visualblocks div:not([data-mce-bogus]){background-image:url(data:image/gif;base64,R0lGODlhEgAKAIABALu7u////yH5BAEAAAEALAAAAAASAAoAAAIfjI9poI0cgDywrhuxfbrzDEbQM2Ei5aRjmoySW4pAAQA7)}.mce-visualblocks section{background-image:url(data:image/gif;base64,R0lGODlhKAAKAIABALu7u////yH5BAEAAAEALAAAAAAoAAoAAAI5jI+pywcNY3sBWHdNrplytD2ellDeSVbp+GmWqaDqDMepc8t17Y4vBsK5hDyJMcI6KkuYU+jpjLoKADs=)}.mce-visualblocks article{background-image:url(data:image/gif;base64,R0lGODlhKgAKAIABALu7u////yH5BAEAAAEALAAAAAAqAAoAAAI6jI+pywkNY3wG0GBvrsd2tXGYSGnfiF7ikpXemTpOiJScasYoDJJrjsG9gkCJ0ag6KhmaIe3pjDYBBQA7)}.mce-visualblocks blockquote{background-image:url(data:image/gif;base64,R0lGODlhPgAKAIABALu7u////yH5BAEAAAEALAAAAAA+AAoAAAJPjI+py+0Knpz0xQDyuUhvfoGgIX5iSKZYgq5uNL5q69asZ8s5rrf0yZmpNkJZzFesBTu8TOlDVAabUyatguVhWduud3EyiUk45xhTTgMBBQA7)}.mce-visualblocks address{background-image:url(data:image/gif;base64,R0lGODlhLQAKAIABALu7u////yH5BAEAAAEALAAAAAAtAAoAAAI/jI+pywwNozSP1gDyyZcjb3UaRpXkWaXmZW4OqKLhBmLs+K263DkJK7OJeifh7FicKD9A1/IpGdKkyFpNmCkAADs=)}.mce-visualblocks pre{background-image:url(data:image/gif;base64,R0lGODlhFQAKAIABALu7uwAAACH5BAEAAAEALAAAAAAVAAoAAAIjjI+ZoN0cgDwSmnpz1NCueYERhnibZVKLNnbOq8IvKpJtVQAAOw==)}.mce-visualblocks figure{background-image:url(data:image/gif;base64,R0lGODlhJAAKAIAAALu7u////yH5BAEAAAEALAAAAAAkAAoAAAI0jI+py+2fwAHUSFvD3RlvG4HIp4nX5JFSpnZUJ6LlrM52OE7uSWosBHScgkSZj7dDKnWAAgA7)}.mce-visualblocks figcaption{border:1px dashed #bbb}.mce-visualblocks hgroup{background-image:url(data:image/gif;base64,R0lGODlhJwAKAIABALu7uwAAACH5BAEAAAEALAAAAAAnAAoAAAI3jI+pywYNI3uB0gpsRtt5fFnfNZaVSYJil4Wo03Hv6Z62uOCgiXH1kZIIJ8NiIxRrAZNMZAtQAAA7)}.mce-visualblocks aside{background-image:url(data:image/gif;base64,R0lGODlhHgAKAIABAKqqqv///yH5BAEAAAEALAAAAAAeAAoAAAItjI+pG8APjZOTzgtqy7I3f1yehmQcFY4WKZbqByutmW4aHUd6vfcVbgudgpYCADs=)}.mce-visualblocks ul{background-image:url(data:image/gif;base64,R0lGODlhDQAKAIAAALu7u////yH5BAEAAAEALAAAAAANAAoAAAIXjI8GybGuYnqUVSjvw26DzzXiqIDlVwAAOw==)}.mce-visualblocks ol{background-image:url(data:image/gif;base64,R0lGODlhDQAKAIABALu7u////yH5BAEAAAEALAAAAAANAAoAAAIXjI8GybH6HHt0qourxC6CvzXieHyeWQAAOw==)}.mce-visualblocks dl{background-image:url(data:image/gif;base64,R0lGODlhDQAKAIABALu7u////yH5BAEAAAEALAAAAAANAAoAAAIXjI8GybEOnmOvUoWznTqeuEjNSCqeGRUAOw==)}.mce-visualblocks:not([dir=rtl]) address,.mce-visualblocks:not([dir=rtl]) article,.mce-visualblocks:not([dir=rtl]) aside,.mce-visualblocks:not([dir=rtl]) blockquote,.mce-visualblocks:not([dir=rtl]) div:not([data-mce-bogus]),.mce-visualblocks:not([dir=rtl]) dl,.mce-visualblocks:not([dir=rtl]) figcaption,.mce-visualblocks:not([dir=rtl]) figure,.mce-visualblocks:not([dir=rtl]) h1,.mce-visualblocks:not([dir=rtl]) h2,.mce-visualblocks:not([dir=rtl]) h3,.mce-visualblocks:not([dir=rtl]) h4,.mce-visualblocks:not([dir=rtl]) h5,.mce-visualblocks:not([dir=rtl]) h6,.mce-visualblocks:not([dir=rtl]) hgroup,.mce-visualblocks:not([dir=rtl]) ol,.mce-visualblocks:not([dir=rtl]) p,.mce-visualblocks:not([dir=rtl]) pre,.mce-visualblocks:not([dir=rtl]) section,.mce-visualblocks:not([dir=rtl]) ul{margin-left:3px}.mce-visualblocks[dir=rtl] address,.mce-visualblocks[dir=rtl] article,.mce-visualblocks[dir=rtl] aside,.mce-visualblocks[dir=rtl] blockquote,.mce-visualblocks[dir=rtl] div:not([data-mce-bogus]),.mce-visualblocks[dir=rtl] dl,.mce-visualblocks[dir=rtl] figcaption,.mce-visualblocks[dir=rtl] figure,.mce-visualblocks[dir=rtl] h1,.mce-visualblocks[dir=rtl] h2,.mce-visualblocks[dir=rtl] h3,.mce-visualblocks[dir=rtl] h4,.mce-visualblocks[dir=rtl] h5,.mce-visualblocks[dir=rtl] h6,.mce-visualblocks[dir=rtl] hgroup,.mce-visualblocks[dir=rtl] ol,.mce-visualblocks[dir=rtl] p,.mce-visualblocks[dir=rtl] pre,.mce-visualblocks[dir=rtl] section,.mce-visualblocks[dir=rtl] ul{background-position-x:right;margin-right:3px}.mce-nbsp,.mce-shy{background:#aaa}.mce-shy::after{content:'-'}\")\n//# sourceMappingURL=content.inline.js.map\n"
  },
  {
    "path": "apps/web-antd/public/tinymce/skins/ui/oxide-dark/content.js",
    "content": "tinymce.Resource.add('ui/dark/content.css', \".mce-content-body .mce-item-anchor{background:transparent url(\\\"data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D'8'%20height%3D'12'%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%3E%3Cpath%20d%3D'M0%200L8%200%208%2012%204.09117821%209%200%2012z'%20fill%3D%22%23cccccc%22%2F%3E%3C%2Fsvg%3E%0A\\\") no-repeat center}.mce-content-body .mce-item-anchor:empty{cursor:default;display:inline-block;height:12px!important;padding:0 2px;-webkit-user-modify:read-only;-moz-user-modify:read-only;-webkit-user-select:all;-moz-user-select:all;user-select:all;width:8px!important}.mce-content-body .mce-item-anchor:not(:empty){background-position-x:2px;display:inline-block;padding-left:12px}.mce-content-body .mce-item-anchor[data-mce-selected]{outline-offset:1px}.tox-comments-visible .tox-comment[contenteditable=false]:not([data-mce-selected]),.tox-comments-visible span.tox-comment img:not([data-mce-selected]),.tox-comments-visible span.tox-comment span.mce-preview-object:not([data-mce-selected]),.tox-comments-visible span.tox-comment>audio:not([data-mce-selected]),.tox-comments-visible span.tox-comment>video:not([data-mce-selected]){outline:3px solid #ffe89d}.tox-comments-visible .tox-comment[contenteditable=false][data-mce-annotation-active=true]:not([data-mce-selected]){outline:3px solid #fed635}.tox-comments-visible span.tox-comment[data-mce-annotation-active=true] img:not([data-mce-selected]),.tox-comments-visible span.tox-comment[data-mce-annotation-active=true] span.mce-preview-object:not([data-mce-selected]),.tox-comments-visible span.tox-comment[data-mce-annotation-active=true]>audio:not([data-mce-selected]),.tox-comments-visible span.tox-comment[data-mce-annotation-active=true]>video:not([data-mce-selected]){outline:3px solid #fed635}.tox-comments-visible span.tox-comment:not([data-mce-selected]){background-color:#ffe89d;outline:0}.tox-comments-visible span.tox-comment[data-mce-annotation-active=true]:not([data-mce-selected=inline-boundary]){background-color:#fed635}.tox-checklist>li:not(.tox-checklist--hidden){list-style:none;margin:.25em 0}.tox-checklist>li:not(.tox-checklist--hidden)::before{content:url(\\\"data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2216%22%20height%3D%2216%22%20viewBox%3D%220%200%2016%2016%22%3E%3Cg%20id%3D%22checklist-unchecked%22%20fill%3D%22none%22%20fill-rule%3D%22evenodd%22%3E%3Crect%20id%3D%22Rectangle%22%20width%3D%2215%22%20height%3D%2215%22%20x%3D%22.5%22%20y%3D%22.5%22%20fill-rule%3D%22nonzero%22%20stroke%3D%22%236d737b%22%20rx%3D%222%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E%0A\\\");cursor:pointer;height:1em;margin-left:-1.5em;margin-top:.125em;position:absolute;width:1em}.tox-checklist li:not(.tox-checklist--hidden).tox-checklist--checked::before{content:url(\\\"data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2216%22%20height%3D%2216%22%20viewBox%3D%220%200%2016%2016%22%3E%3Cg%20id%3D%22checklist-checked%22%20fill%3D%22none%22%20fill-rule%3D%22evenodd%22%3E%3Crect%20id%3D%22Rectangle%22%20width%3D%2216%22%20height%3D%2216%22%20fill%3D%22%234099FF%22%20fill-rule%3D%22nonzero%22%20rx%3D%222%22%2F%3E%3Cpath%20id%3D%22Path%22%20fill%3D%22%23FFF%22%20fill-rule%3D%22nonzero%22%20d%3D%22M11.5703186%2C3.14417309%20C11.8516238%2C2.73724603%2012.4164781%2C2.62829933%2012.83558%2C2.89774797%20C13.260121%2C3.17069355%2013.3759736%2C3.72932262%2013.0909105%2C4.14168582%20L7.7580587%2C11.8560195%20C7.43776896%2C12.3193404%206.76483983%2C12.3852142%206.35607322%2C11.9948725%20L3.02491697%2C8.8138662%20C2.66090143%2C8.46625845%202.65798871%2C7.89594698%203.01850234%2C7.54483354%20C3.373942%2C7.19866177%203.94940006%2C7.19592841%204.30829608%2C7.5386474%20L6.85276923%2C9.9684299%20L11.5703186%2C3.14417309%20Z%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E%0A\\\")}[dir=rtl] .tox-checklist>li:not(.tox-checklist--hidden)::before{margin-left:0;margin-right:-1.5em}code[class*=language-],pre[class*=language-]{color:#f8f8f2;background:0 0;text-shadow:0 1px rgba(0,0,0,.3);font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;tab-size:4;-webkit-hyphens:none;hyphens:none}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto;border-radius:.3em}:not(pre)>code[class*=language-],pre[class*=language-]{background:#282a36}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#6272a4}.token.punctuation{color:#f8f8f2}.namespace{opacity:.7}.token.constant,.token.deleted,.token.property,.token.symbol,.token.tag{color:#ff79c6}.token.boolean,.token.number{color:#bd93f9}.token.attr-name,.token.builtin,.token.char,.token.inserted,.token.selector,.token.string{color:#50fa7b}.language-css .token.string,.style .token.string,.token.entity,.token.operator,.token.url,.token.variable{color:#f8f8f2}.token.atrule,.token.attr-value,.token.class-name,.token.function{color:#f1fa8c}.token.keyword{color:#8be9fd}.token.important,.token.regex{color:#ffb86c}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}.mce-content-body{overflow-wrap:break-word;word-wrap:break-word}.mce-content-body .mce-visual-caret{background-color:#000;background-color:currentColor;position:absolute}.mce-content-body .mce-visual-caret-hidden{display:none}.mce-content-body [data-mce-caret]{left:-1000px;margin:0;padding:0;position:absolute;right:auto;top:0}.mce-content-body .mce-offscreen-selection{left:-2000000px;max-width:1000000px;position:absolute}.mce-content-body [contentEditable=false]{cursor:default}.mce-content-body [contentEditable=true]{cursor:text}.tox-cursor-format-painter{cursor:url(\\\"data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%20viewBox%3D%220%200%2024%2024%22%3E%0A%20%20%3Cg%20fill%3D%22none%22%20fill-rule%3D%22evenodd%22%3E%0A%20%20%20%20%3Cpath%20fill%3D%22%23000%22%20fill-rule%3D%22nonzero%22%20d%3D%22M15%2C6%20C15%2C5.45%2014.55%2C5%2014%2C5%20L6%2C5%20C5.45%2C5%205%2C5.45%205%2C6%20L5%2C10%20C5%2C10.55%205.45%2C11%206%2C11%20L14%2C11%20C14.55%2C11%2015%2C10.55%2015%2C10%20L15%2C9%20L16%2C9%20L16%2C12%20L9%2C12%20L9%2C19%20C9%2C19.55%209.45%2C20%2010%2C20%20L11%2C20%20C11.55%2C20%2012%2C19.55%2012%2C19%20L12%2C14%20L18%2C14%20L18%2C7%20L15%2C7%20L15%2C6%20Z%22%2F%3E%0A%20%20%20%20%3Cpath%20fill%3D%22%23000%22%20fill-rule%3D%22nonzero%22%20d%3D%22M1%2C1%20L8.25%2C1%20C8.66421356%2C1%209%2C1.33578644%209%2C1.75%20L9%2C1.75%20C9%2C2.16421356%208.66421356%2C2.5%208.25%2C2.5%20L2.5%2C2.5%20L2.5%2C8.25%20C2.5%2C8.66421356%202.16421356%2C9%201.75%2C9%20L1.75%2C9%20C1.33578644%2C9%201%2C8.66421356%201%2C8.25%20L1%2C1%20Z%22%2F%3E%0A%20%20%3C%2Fg%3E%0A%3C%2Fsvg%3E%0A\\\"),default}div.mce-footnotes hr{margin-inline-end:auto;margin-inline-start:0;width:25%}div.mce-footnotes li>a.mce-footnotes-backlink{text-decoration:none}@media print{sup.mce-footnote a{color:#000;text-decoration:none}div.mce-footnotes{break-inside:avoid;width:100%}div.mce-footnotes li>a.mce-footnotes-backlink{display:none}}tiny-math-block{display:flex;justify-content:center;margin:16px 0 16px 0}tiny-math-inline{display:inline-block}.mce-content-body figure.align-left{float:left}.mce-content-body figure.align-right{float:right}.mce-content-body figure.image.align-center{display:table;margin-left:auto;margin-right:auto}.mce-preview-object{border:1px solid gray;display:inline-block;line-height:0;margin:0 2px 0 2px;position:relative}.mce-preview-object .mce-shim{background:url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7);height:100%;left:0;position:absolute;top:0;width:100%}.mce-preview-object[data-mce-selected=\\\"2\\\"] .mce-shim{display:none}.mce-content-body .mce-mergetag{cursor:default!important;-webkit-user-select:none;-moz-user-select:none;user-select:none}.mce-content-body .mce-mergetag:hover{background-color:rgba(0,108,231,.3)}.mce-content-body .mce-mergetag-affix{background-color:rgba(0,108,231,.3);color:#006ce7}.mce-object{background:transparent url(\\\"data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%3E%3Cpath%20d%3D%22M4%203h16a1%201%200%200%201%201%201v16a1%201%200%200%201-1%201H4a1%201%200%200%201-1-1V4a1%201%200%200%201%201-1zm1%202v14h14V5H5zm4.79%202.565l5.64%204.028a.5.5%200%200%201%200%20.814l-5.64%204.028a.5.5%200%200%201-.79-.407V7.972a.5.5%200%200%201%20.79-.407z%22%20fill%3D%22%23cccccc%22%2F%3E%3C%2Fsvg%3E%0A\\\") no-repeat center;border:1px dashed #aaa}.mce-pagebreak{border:1px dashed #aaa;cursor:default;display:block;height:5px;margin-top:15px;page-break-before:always;width:100%}@media print{.mce-pagebreak{border:0}}.tiny-pageembed .mce-shim{background:url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7);height:100%;left:0;position:absolute;top:0;width:100%}.tiny-pageembed[data-mce-selected=\\\"2\\\"] .mce-shim{display:none}.tiny-pageembed{display:inline-block;position:relative}.tiny-pageembed--16by9,.tiny-pageembed--1by1,.tiny-pageembed--21by9,.tiny-pageembed--4by3{display:block;overflow:hidden;padding:0;position:relative;width:100%}.tiny-pageembed--21by9{padding-top:42.857143%}.tiny-pageembed--16by9{padding-top:56.25%}.tiny-pageembed--4by3{padding-top:75%}.tiny-pageembed--1by1{padding-top:100%}.tiny-pageembed--16by9 iframe,.tiny-pageembed--1by1 iframe,.tiny-pageembed--21by9 iframe,.tiny-pageembed--4by3 iframe{border:0;height:100%;left:0;position:absolute;top:0;width:100%}.mce-content-body[data-mce-placeholder]{position:relative}.mce-content-body[data-mce-placeholder]:not(.mce-visualblocks)::before{color:rgba(34,47,62,.7);content:attr(data-mce-placeholder);position:absolute}@media (forced-colors:active){.mce-content-body[data-mce-placeholder]:not(.mce-visualblocks)::before{color:highlight;filter:brightness(30%);z-index:-1}}.mce-content-body:not([dir=rtl])[data-mce-placeholder]:not(.mce-visualblocks)::before{left:1px}.mce-content-body[dir=rtl][data-mce-placeholder]:not(.mce-visualblocks)::before{right:1px}.mce-content-body div.mce-resizehandle{background-color:#4099ff;border-color:#4099ff;border-style:solid;border-width:1px;box-sizing:border-box;height:10px;position:absolute;width:10px;z-index:1298}.mce-content-body div.mce-resizehandle:hover{background-color:#4099ff}.mce-content-body div.mce-resizehandle:nth-of-type(1){cursor:nwse-resize}.mce-content-body div.mce-resizehandle:nth-of-type(2){cursor:nesw-resize}.mce-content-body div.mce-resizehandle:nth-of-type(3){cursor:nwse-resize}.mce-content-body div.mce-resizehandle:nth-of-type(4){cursor:nesw-resize}.mce-content-body .mce-resize-backdrop{z-index:10000}.mce-content-body .mce-clonedresizable{cursor:default;opacity:.5;outline:1px dashed #000;position:absolute;z-index:10001}.mce-content-body .mce-clonedresizable.mce-resizetable-columns td,.mce-content-body .mce-clonedresizable.mce-resizetable-columns th{border:0}.mce-content-body .mce-resize-helper{background:#555;background:rgba(0,0,0,.75);border:1px;border-radius:3px;color:#fff;display:none;font-family:sans-serif;font-size:12px;line-height:14px;margin:5px 10px;padding:5px;position:absolute;white-space:nowrap;z-index:10002}.tox-rtc-user-selection{position:relative}.tox-rtc-user-cursor{bottom:0;cursor:default;position:absolute;top:0;width:2px}.tox-rtc-user-cursor::before{background-color:inherit;border-radius:50%;content:'';display:block;height:8px;position:absolute;right:-3px;top:-3px;width:8px}.tox-rtc-user-cursor:hover::after{background-color:inherit;border-radius:100px;box-sizing:border-box;color:#fff;content:attr(data-user);display:block;font-size:12px;font-weight:700;left:-5px;min-height:8px;min-width:8px;padding:0 12px;position:absolute;top:-11px;white-space:nowrap;z-index:1000}.tox-rtc-user-selection--1 .tox-rtc-user-cursor{background-color:#2dc26b}.tox-rtc-user-selection--2 .tox-rtc-user-cursor{background-color:#e03e2d}.tox-rtc-user-selection--3 .tox-rtc-user-cursor{background-color:#f1c40f}.tox-rtc-user-selection--4 .tox-rtc-user-cursor{background-color:#3598db}.tox-rtc-user-selection--5 .tox-rtc-user-cursor{background-color:#b96ad9}.tox-rtc-user-selection--6 .tox-rtc-user-cursor{background-color:#e67e23}.tox-rtc-user-selection--7 .tox-rtc-user-cursor{background-color:#aaa69d}.tox-rtc-user-selection--8 .tox-rtc-user-cursor{background-color:#f368e0}.tox-rtc-remote-image{background:#eaeaea url(\\\"data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%2236%22%20height%3D%2212%22%20viewBox%3D%220%200%2036%2012%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%3Ccircle%20cx%3D%226%22%20cy%3D%226%22%20r%3D%223%22%20fill%3D%22rgba(0%2C%200%2C%200%2C%20.2)%22%3E%0A%20%20%20%20%3Canimate%20attributeName%3D%22r%22%20values%3D%223%3B5%3B3%22%20calcMode%3D%22linear%22%20dur%3D%221s%22%20repeatCount%3D%22indefinite%22%20%2F%3E%0A%20%20%3C%2Fcircle%3E%0A%20%20%3Ccircle%20cx%3D%2218%22%20cy%3D%226%22%20r%3D%223%22%20fill%3D%22rgba(0%2C%200%2C%200%2C%20.2)%22%3E%0A%20%20%20%20%3Canimate%20attributeName%3D%22r%22%20values%3D%223%3B5%3B3%22%20calcMode%3D%22linear%22%20begin%3D%22.33s%22%20dur%3D%221s%22%20repeatCount%3D%22indefinite%22%20%2F%3E%0A%20%20%3C%2Fcircle%3E%0A%20%20%3Ccircle%20cx%3D%2230%22%20cy%3D%226%22%20r%3D%223%22%20fill%3D%22rgba(0%2C%200%2C%200%2C%20.2)%22%3E%0A%20%20%20%20%3Canimate%20attributeName%3D%22r%22%20values%3D%223%3B5%3B3%22%20calcMode%3D%22linear%22%20begin%3D%22.66s%22%20dur%3D%221s%22%20repeatCount%3D%22indefinite%22%20%2F%3E%0A%20%20%3C%2Fcircle%3E%0A%3C%2Fsvg%3E%0A\\\") no-repeat center center;border:1px solid #ccc;min-height:240px;min-width:320px}.mce-match-marker{background:#aaa;color:#fff}.mce-match-marker-selected{background:#39f;color:#fff}.mce-match-marker-selected::-moz-selection{background:#39f;color:#fff}.mce-match-marker-selected::selection{background:#39f;color:#fff}.mce-content-body audio[data-mce-selected],.mce-content-body details[data-mce-selected],.mce-content-body embed[data-mce-selected],.mce-content-body img[data-mce-selected],.mce-content-body object[data-mce-selected],.mce-content-body table[data-mce-selected],.mce-content-body video[data-mce-selected]{outline:3px solid #4099ff}.mce-content-body hr[data-mce-selected]{outline:3px solid #4099ff;outline-offset:1px}.mce-content-body [contentEditable=false] [contentEditable=true]:focus{outline:3px solid #4099ff}.mce-content-body [contentEditable=false] [contentEditable=true]:hover{outline:3px solid #4099ff}.mce-content-body [contentEditable=false][data-mce-selected]{cursor:not-allowed;outline:3px solid #4099ff}.mce-content-body.mce-content-readonly [contentEditable=true]:focus,.mce-content-body.mce-content-readonly [contentEditable=true]:hover{outline:0}.mce-content-body [data-mce-selected=inline-boundary]{background-color:#4099ff}.mce-content-body .mce-edit-focus{outline:3px solid #4099ff}.mce-content-body td[data-mce-selected],.mce-content-body th[data-mce-selected]{position:relative}.mce-content-body td[data-mce-selected]::-moz-selection,.mce-content-body th[data-mce-selected]::-moz-selection{background:0 0}.mce-content-body td[data-mce-selected]::selection,.mce-content-body th[data-mce-selected]::selection{background:0 0}.mce-content-body td[data-mce-selected] *,.mce-content-body th[data-mce-selected] *{outline:0;-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}.mce-content-body td[data-mce-selected]::after,.mce-content-body th[data-mce-selected]::after{background-color:rgba(180,215,255,.7);border:1px solid transparent;bottom:-1px;content:'';left:-1px;mix-blend-mode:lighten;position:absolute;right:-1px;top:-1px}@media screen and (-ms-high-contrast:active),(-ms-high-contrast:none){.mce-content-body td[data-mce-selected]::after,.mce-content-body th[data-mce-selected]::after{border-color:rgba(0,84,180,.7)}}.mce-content-body img[data-mce-selected]::-moz-selection{background:0 0}.mce-content-body img[data-mce-selected]::selection{background:0 0}.ephox-snooker-resizer-bar{background-color:#4099ff;opacity:0;-webkit-user-select:none;-moz-user-select:none;user-select:none}.ephox-snooker-resizer-cols{cursor:col-resize}.ephox-snooker-resizer-rows{cursor:row-resize}.ephox-snooker-resizer-bar.ephox-snooker-resizer-bar-dragging{opacity:1}.mce-spellchecker-word{background-image:url(\\\"data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D'4'%20height%3D'4'%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%3E%3Cpath%20stroke%3D'%23ff0000'%20fill%3D'none'%20stroke-linecap%3D'round'%20stroke-opacity%3D'.75'%20d%3D'M0%203L2%201%204%203'%2F%3E%3C%2Fsvg%3E%0A\\\");background-position:0 calc(100% + 1px);background-repeat:repeat-x;background-size:auto 6px;cursor:default;height:2rem}.mce-spellchecker-grammar{background-image:url(\\\"data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D'4'%20height%3D'4'%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%3E%3Cpath%20stroke%3D'%2300A835'%20fill%3D'none'%20stroke-linecap%3D'round'%20d%3D'M0%203L2%201%204%203'%2F%3E%3C%2Fsvg%3E%0A\\\");background-position:0 calc(100% + 1px);background-repeat:repeat-x;background-size:auto 6px;cursor:default}.mce-toc{border:1px solid gray}.mce-toc h2{margin:4px}.mce-toc ul>li{list-style-type:none}[data-mce-block]{display:block}.mce-item-table:not([border]),.mce-item-table:not([border]) caption,.mce-item-table:not([border]) td,.mce-item-table:not([border]) th,.mce-item-table[border=\\\"0\\\"],.mce-item-table[border=\\\"0\\\"] caption,.mce-item-table[border=\\\"0\\\"] td,.mce-item-table[border=\\\"0\\\"] th,table[style*=\\\"border-width: 0px\\\"],table[style*=\\\"border-width: 0px\\\"] caption,table[style*=\\\"border-width: 0px\\\"] td,table[style*=\\\"border-width: 0px\\\"] th{border:1px dashed #bbb}.mce-visualblocks address,.mce-visualblocks article,.mce-visualblocks aside,.mce-visualblocks blockquote,.mce-visualblocks div:not([data-mce-bogus]),.mce-visualblocks dl,.mce-visualblocks figcaption,.mce-visualblocks figure,.mce-visualblocks h1,.mce-visualblocks h2,.mce-visualblocks h3,.mce-visualblocks h4,.mce-visualblocks h5,.mce-visualblocks h6,.mce-visualblocks hgroup,.mce-visualblocks ol,.mce-visualblocks p,.mce-visualblocks pre,.mce-visualblocks section,.mce-visualblocks ul{background-repeat:no-repeat;border:1px dashed #bbb;margin-left:3px;padding-top:10px}.mce-visualblocks p{background-image:url(data:image/gif;base64,R0lGODlhCQAJAJEAAAAAAP///7u7u////yH5BAEAAAMALAAAAAAJAAkAAAIQnG+CqCN/mlyvsRUpThG6AgA7)}.mce-visualblocks h1{background-image:url(data:image/gif;base64,R0lGODlhDQAKAIABALu7u////yH5BAEAAAEALAAAAAANAAoAAAIXjI8GybGu1JuxHoAfRNRW3TWXyF2YiRUAOw==)}.mce-visualblocks h2{background-image:url(data:image/gif;base64,R0lGODlhDgAKAIABALu7u////yH5BAEAAAEALAAAAAAOAAoAAAIajI8Hybbx4oOuqgTynJd6bGlWg3DkJzoaUAAAOw==)}.mce-visualblocks h3{background-image:url(data:image/gif;base64,R0lGODlhDgAKAIABALu7u////yH5BAEAAAEALAAAAAAOAAoAAAIZjI8Hybbx4oOuqgTynJf2Ln2NOHpQpmhAAQA7)}.mce-visualblocks h4{background-image:url(data:image/gif;base64,R0lGODlhDgAKAIABALu7u////yH5BAEAAAEALAAAAAAOAAoAAAIajI8HybbxInR0zqeAdhtJlXwV1oCll2HaWgAAOw==)}.mce-visualblocks h5{background-image:url(data:image/gif;base64,R0lGODlhDgAKAIABALu7u////yH5BAEAAAEALAAAAAAOAAoAAAIajI8HybbxIoiuwjane4iq5GlW05GgIkIZUAAAOw==)}.mce-visualblocks h6{background-image:url(data:image/gif;base64,R0lGODlhDgAKAIABALu7u////yH5BAEAAAEALAAAAAAOAAoAAAIajI8HybbxIoiuwjan04jep1iZ1XRlAo5bVgAAOw==)}.mce-visualblocks div:not([data-mce-bogus]){background-image:url(data:image/gif;base64,R0lGODlhEgAKAIABALu7u////yH5BAEAAAEALAAAAAASAAoAAAIfjI9poI0cgDywrhuxfbrzDEbQM2Ei5aRjmoySW4pAAQA7)}.mce-visualblocks section{background-image:url(data:image/gif;base64,R0lGODlhKAAKAIABALu7u////yH5BAEAAAEALAAAAAAoAAoAAAI5jI+pywcNY3sBWHdNrplytD2ellDeSVbp+GmWqaDqDMepc8t17Y4vBsK5hDyJMcI6KkuYU+jpjLoKADs=)}.mce-visualblocks article{background-image:url(data:image/gif;base64,R0lGODlhKgAKAIABALu7u////yH5BAEAAAEALAAAAAAqAAoAAAI6jI+pywkNY3wG0GBvrsd2tXGYSGnfiF7ikpXemTpOiJScasYoDJJrjsG9gkCJ0ag6KhmaIe3pjDYBBQA7)}.mce-visualblocks blockquote{background-image:url(data:image/gif;base64,R0lGODlhPgAKAIABALu7u////yH5BAEAAAEALAAAAAA+AAoAAAJPjI+py+0Knpz0xQDyuUhvfoGgIX5iSKZYgq5uNL5q69asZ8s5rrf0yZmpNkJZzFesBTu8TOlDVAabUyatguVhWduud3EyiUk45xhTTgMBBQA7)}.mce-visualblocks address{background-image:url(data:image/gif;base64,R0lGODlhLQAKAIABALu7u////yH5BAEAAAEALAAAAAAtAAoAAAI/jI+pywwNozSP1gDyyZcjb3UaRpXkWaXmZW4OqKLhBmLs+K263DkJK7OJeifh7FicKD9A1/IpGdKkyFpNmCkAADs=)}.mce-visualblocks pre{background-image:url(data:image/gif;base64,R0lGODlhFQAKAIABALu7uwAAACH5BAEAAAEALAAAAAAVAAoAAAIjjI+ZoN0cgDwSmnpz1NCueYERhnibZVKLNnbOq8IvKpJtVQAAOw==)}.mce-visualblocks figure{background-image:url(data:image/gif;base64,R0lGODlhJAAKAIAAALu7u////yH5BAEAAAEALAAAAAAkAAoAAAI0jI+py+2fwAHUSFvD3RlvG4HIp4nX5JFSpnZUJ6LlrM52OE7uSWosBHScgkSZj7dDKnWAAgA7)}.mce-visualblocks figcaption{border:1px dashed #bbb}.mce-visualblocks hgroup{background-image:url(data:image/gif;base64,R0lGODlhJwAKAIABALu7uwAAACH5BAEAAAEALAAAAAAnAAoAAAI3jI+pywYNI3uB0gpsRtt5fFnfNZaVSYJil4Wo03Hv6Z62uOCgiXH1kZIIJ8NiIxRrAZNMZAtQAAA7)}.mce-visualblocks aside{background-image:url(data:image/gif;base64,R0lGODlhHgAKAIABAKqqqv///yH5BAEAAAEALAAAAAAeAAoAAAItjI+pG8APjZOTzgtqy7I3f1yehmQcFY4WKZbqByutmW4aHUd6vfcVbgudgpYCADs=)}.mce-visualblocks ul{background-image:url(data:image/gif;base64,R0lGODlhDQAKAIAAALu7u////yH5BAEAAAEALAAAAAANAAoAAAIXjI8GybGuYnqUVSjvw26DzzXiqIDlVwAAOw==)}.mce-visualblocks ol{background-image:url(data:image/gif;base64,R0lGODlhDQAKAIABALu7u////yH5BAEAAAEALAAAAAANAAoAAAIXjI8GybH6HHt0qourxC6CvzXieHyeWQAAOw==)}.mce-visualblocks dl{background-image:url(data:image/gif;base64,R0lGODlhDQAKAIABALu7u////yH5BAEAAAEALAAAAAANAAoAAAIXjI8GybEOnmOvUoWznTqeuEjNSCqeGRUAOw==)}.mce-visualblocks:not([dir=rtl]) address,.mce-visualblocks:not([dir=rtl]) article,.mce-visualblocks:not([dir=rtl]) aside,.mce-visualblocks:not([dir=rtl]) blockquote,.mce-visualblocks:not([dir=rtl]) div:not([data-mce-bogus]),.mce-visualblocks:not([dir=rtl]) dl,.mce-visualblocks:not([dir=rtl]) figcaption,.mce-visualblocks:not([dir=rtl]) figure,.mce-visualblocks:not([dir=rtl]) h1,.mce-visualblocks:not([dir=rtl]) h2,.mce-visualblocks:not([dir=rtl]) h3,.mce-visualblocks:not([dir=rtl]) h4,.mce-visualblocks:not([dir=rtl]) h5,.mce-visualblocks:not([dir=rtl]) h6,.mce-visualblocks:not([dir=rtl]) hgroup,.mce-visualblocks:not([dir=rtl]) ol,.mce-visualblocks:not([dir=rtl]) p,.mce-visualblocks:not([dir=rtl]) pre,.mce-visualblocks:not([dir=rtl]) section,.mce-visualblocks:not([dir=rtl]) ul{margin-left:3px}.mce-visualblocks[dir=rtl] address,.mce-visualblocks[dir=rtl] article,.mce-visualblocks[dir=rtl] aside,.mce-visualblocks[dir=rtl] blockquote,.mce-visualblocks[dir=rtl] div:not([data-mce-bogus]),.mce-visualblocks[dir=rtl] dl,.mce-visualblocks[dir=rtl] figcaption,.mce-visualblocks[dir=rtl] figure,.mce-visualblocks[dir=rtl] h1,.mce-visualblocks[dir=rtl] h2,.mce-visualblocks[dir=rtl] h3,.mce-visualblocks[dir=rtl] h4,.mce-visualblocks[dir=rtl] h5,.mce-visualblocks[dir=rtl] h6,.mce-visualblocks[dir=rtl] hgroup,.mce-visualblocks[dir=rtl] ol,.mce-visualblocks[dir=rtl] p,.mce-visualblocks[dir=rtl] pre,.mce-visualblocks[dir=rtl] section,.mce-visualblocks[dir=rtl] ul{background-position-x:right;margin-right:3px}.mce-nbsp,.mce-shy{background:#aaa}.mce-shy::after{content:'-'}body{font-family:sans-serif}table{border-collapse:collapse}\")\n//# sourceMappingURL=content.js.map\n"
  },
  {
    "path": "apps/web-antd/public/tinymce/skins/ui/oxide-dark/skin.js",
    "content": "tinymce.Resource.add('ui/dark/skin.css', \".tox{box-shadow:none;box-sizing:content-box;color:#222f3e;cursor:auto;font-family:-apple-system,BlinkMacSystemFont,\\\"Segoe UI\\\",Roboto,Oxygen-Sans,Ubuntu,Cantarell,\\\"Helvetica Neue\\\",sans-serif;font-size:16px;font-style:normal;font-weight:400;line-height:normal;-webkit-tap-highlight-color:transparent;text-decoration:none;text-shadow:none;text-transform:none;vertical-align:initial;white-space:normal}.tox :not(svg):not(rect){box-sizing:inherit;color:inherit;cursor:inherit;direction:inherit;font-family:inherit;font-size:inherit;font-style:inherit;font-weight:inherit;line-height:inherit;-webkit-tap-highlight-color:inherit;text-align:inherit;text-decoration:inherit;text-shadow:inherit;text-transform:inherit;vertical-align:inherit;white-space:inherit}.tox :not(svg):not(rect){background:0 0;border:0;box-shadow:none;float:none;height:auto;margin:0;max-width:none;outline:0;padding:0;position:static;width:auto}.tox:not([dir=rtl]){direction:ltr;text-align:left}.tox[dir=rtl]{direction:rtl;text-align:right}.tox-tinymce{border:2px solid #161f29;border-radius:10px;box-shadow:none;box-sizing:border-box;display:flex;flex-direction:column;font-family:-apple-system,BlinkMacSystemFont,\\\"Segoe UI\\\",Roboto,Oxygen-Sans,Ubuntu,Cantarell,\\\"Helvetica Neue\\\",sans-serif;overflow:hidden;position:relative;visibility:inherit!important}.tox.tox-tinymce-inline{border:none;box-shadow:none;overflow:initial}.tox.tox-tinymce-inline .tox-editor-container{overflow:initial}.tox.tox-tinymce-inline .tox-editor-header{background-color:#222f3e;border:2px solid #161f29;border-radius:10px;box-shadow:none;overflow:hidden}.tox-tinymce-aux{font-family:-apple-system,BlinkMacSystemFont,\\\"Segoe UI\\\",Roboto,Oxygen-Sans,Ubuntu,Cantarell,\\\"Helvetica Neue\\\",sans-serif;z-index:1300}.tox-tinymce :focus,.tox-tinymce-aux :focus{outline:0}button::-moz-focus-inner{border:0}.tox[dir=rtl] .tox-icon--flip svg{transform:rotateY(180deg)}.tox .accessibility-issue__header{align-items:center;display:flex;margin-bottom:4px}.tox .accessibility-issue__description{align-items:stretch;border-radius:6px;display:flex;justify-content:space-between}.tox .accessibility-issue__description>div{padding-bottom:4px}.tox .accessibility-issue__description>div>div{align-items:center;display:flex;margin-bottom:4px}.tox .accessibility-issue__description>div>div .tox-icon svg{display:block}.tox .accessibility-issue__repair{margin-top:16px}.tox .tox-dialog__body-content .accessibility-issue--info .accessibility-issue__description{background-color:rgba(0,101,216,.4);color:#fff}.tox .tox-dialog__body-content .accessibility-issue--info .tox-form__group h2{color:#fff}.tox .tox-dialog__body-content .accessibility-issue--info .tox-icon svg{fill:#fff}.tox .tox-dialog__body-content .accessibility-issue--info a.tox-button--naked.tox-button--icon{background-color:#006ce7;color:#fff}.tox .tox-dialog__body-content .accessibility-issue--info a.tox-button--naked.tox-button--icon:focus,.tox .tox-dialog__body-content .accessibility-issue--info a.tox-button--naked.tox-button--icon:hover{background-color:#0060ce}.tox .tox-dialog__body-content .accessibility-issue--info a.tox-button--naked.tox-button--icon:active{background-color:#0054b4}.tox .tox-dialog__body-content .accessibility-issue--warn .accessibility-issue__description{background-color:rgba(255,165,0,.5);color:#fff}.tox .tox-dialog__body-content .accessibility-issue--warn .tox-form__group h2{color:#fff}.tox .tox-dialog__body-content .accessibility-issue--warn .tox-icon svg{fill:#fff}.tox .tox-dialog__body-content .accessibility-issue--warn a.tox-button--naked.tox-button--icon{background-color:#ffe89d;color:#222f3e}.tox .tox-dialog__body-content .accessibility-issue--warn a.tox-button--naked.tox-button--icon:focus,.tox .tox-dialog__body-content .accessibility-issue--warn a.tox-button--naked.tox-button--icon:hover{background-color:#f2d574;color:#222f3e}.tox .tox-dialog__body-content .accessibility-issue--warn a.tox-button--naked.tox-button--icon:active{background-color:#e8c657;color:#222f3e}.tox .tox-dialog__body-content .accessibility-issue--error .accessibility-issue__description{background-color:rgba(204,0,0,.5);color:#fff}.tox .tox-dialog__body-content .accessibility-issue--error .tox-form__group h2{color:#fff}.tox .tox-dialog__body-content .accessibility-issue--error .tox-icon svg{fill:#fff}.tox .tox-dialog__body-content .accessibility-issue--error a.tox-button--naked.tox-button--icon{background-color:#f2bfbf;color:#222f3e}.tox .tox-dialog__body-content .accessibility-issue--error a.tox-button--naked.tox-button--icon:focus,.tox .tox-dialog__body-content .accessibility-issue--error a.tox-button--naked.tox-button--icon:hover{background-color:#e9a4a4;color:#222f3e}.tox .tox-dialog__body-content .accessibility-issue--error a.tox-button--naked.tox-button--icon:active{background-color:#ee9494;color:#222f3e}.tox .tox-dialog__body-content .accessibility-issue--success .accessibility-issue__description{background-color:rgba(120,171,70,.5);color:#fff}.tox .tox-dialog__body-content .accessibility-issue--success .accessibility-issue__description>:last-child{display:none}.tox .tox-dialog__body-content .accessibility-issue--success .tox-form__group h2{color:#fff}.tox .tox-dialog__body-content .accessibility-issue--success .tox-icon svg{fill:#fff}.tox .tox-dialog__body-content .accessibility-issue__header .tox-form__group h1,.tox .tox-dialog__body-content .tox-form__group .accessibility-issue__description h2{font-size:14px;margin-top:0}.tox:not([dir=rtl]) .tox-dialog__body-content .accessibility-issue__header .tox-button{margin-left:4px}.tox:not([dir=rtl]) .tox-dialog__body-content .accessibility-issue__header>:nth-last-child(2){margin-left:auto}.tox:not([dir=rtl]) .tox-dialog__body-content .accessibility-issue__description{padding:4px 4px 4px 8px}.tox[dir=rtl] .tox-dialog__body-content .accessibility-issue__header .tox-button{margin-right:4px}.tox[dir=rtl] .tox-dialog__body-content .accessibility-issue__header>:nth-last-child(2){margin-right:auto}.tox[dir=rtl] .tox-dialog__body-content .accessibility-issue__description{padding:4px 8px 4px 4px}.tox .mce-codemirror{background:#fff;bottom:0;font-size:13px;left:0;position:absolute;right:0;top:0;z-index:1}.tox .mce-codemirror.tox-inline-codemirror{margin:8px;position:absolute}.tox .tox-advtemplate .tox-form__grid{flex:1}.tox .tox-advtemplate .tox-form__grid>div:first-child{display:flex;flex-direction:column;width:30%}.tox .tox-advtemplate .tox-form__grid>div:first-child>div:nth-child(2){flex-basis:0;flex-grow:1;overflow:auto}@media only screen and (max-width:767px){body:not(.tox-force-desktop) .tox .tox-advtemplate .tox-form__grid>div:first-child{width:100%}}.tox .tox-advtemplate iframe{border-color:#161f29;border-radius:10px;border-style:solid;border-width:1px;margin:0 10px}.tox .tox-anchorbar{display:flex;flex:0 0 auto}.tox .tox-bottom-anchorbar{display:flex;flex:0 0 auto}.tox .tox-bar{display:flex;flex:0 0 auto}.tox .tox-button{background-color:#006ce7;background-image:none;background-position:0 0;background-repeat:repeat;border-color:#006ce7;border-radius:6px;border-style:solid;border-width:1px;box-shadow:none;box-sizing:border-box;color:#fff;cursor:pointer;display:inline-block;font-family:-apple-system,BlinkMacSystemFont,\\\"Segoe UI\\\",Roboto,Oxygen-Sans,Ubuntu,Cantarell,\\\"Helvetica Neue\\\",sans-serif;font-size:14px;font-style:normal;font-weight:700;letter-spacing:normal;line-height:24px;margin:0;outline:0;padding:4px 16px;position:relative;text-align:center;text-decoration:none;text-transform:none;white-space:nowrap}.tox .tox-button::before{border-radius:6px;bottom:-1px;box-shadow:inset 0 0 0 1px #fff,0 0 0 2px #006ce7;content:'';left:-1px;opacity:0;pointer-events:none;position:absolute;right:-1px;top:-1px}.tox .tox-button[disabled]{background-color:#006ce7;background-image:none;border-color:#006ce7;box-shadow:none;color:rgba(255,255,255,.5);cursor:not-allowed}.tox .tox-button:focus:not(:disabled){background-color:#0060ce;background-image:none;border-color:#0060ce;box-shadow:none;color:#fff}.tox .tox-button:focus:not(:disabled)::before{opacity:1}.tox .tox-button:hover:not(:disabled){background-color:#0060ce;background-image:none;border-color:#0060ce;box-shadow:none;color:#fff}.tox .tox-button:active:not(:disabled){background-color:#0054b4;background-image:none;border-color:#0054b4;box-shadow:none;color:#fff}.tox .tox-button.tox-button--enabled{background-color:#0054b4;background-image:none;border-color:#0054b4;box-shadow:none;color:#fff}.tox .tox-button.tox-button--enabled[disabled]{background-color:#0054b4;background-image:none;border-color:#0054b4;box-shadow:none;color:rgba(255,255,255,.5);cursor:not-allowed}.tox .tox-button.tox-button--enabled:focus:not(:disabled){background-color:#00489b;background-image:none;border-color:#00489b;box-shadow:none;color:#fff}.tox .tox-button.tox-button--enabled:hover:not(:disabled){background-color:#00489b;background-image:none;border-color:#00489b;box-shadow:none;color:#fff}.tox .tox-button.tox-button--enabled:active:not(:disabled){background-color:#003c81;background-image:none;border-color:#003c81;box-shadow:none;color:#fff}.tox .tox-button--icon-and-text,.tox .tox-button.tox-button--icon-and-text,.tox .tox-button.tox-button--secondary.tox-button--icon-and-text{display:flex;padding:5px 4px}.tox .tox-button--icon-and-text .tox-icon svg,.tox .tox-button.tox-button--icon-and-text .tox-icon svg,.tox .tox-button.tox-button--secondary.tox-button--icon-and-text .tox-icon svg{display:block;fill:currentColor}.tox .tox-button--secondary{background-color:#3d546f;background-image:none;background-position:0 0;background-repeat:repeat;border-color:#3d546f;border-radius:6px;border-style:solid;border-width:1px;box-shadow:none;color:#fff;font-size:14px;font-style:normal;font-weight:700;letter-spacing:normal;outline:0;padding:4px 16px;text-decoration:none;text-transform:none}.tox .tox-button--secondary[disabled]{background-color:#3d546f;background-image:none;border-color:#3d546f;box-shadow:none;color:rgba(255,255,255,.5)}.tox .tox-button--secondary:focus:not(:disabled){background-color:#34485f;background-image:none;border-color:#34485f;box-shadow:none;color:#fff}.tox .tox-button--secondary:hover:not(:disabled){background-color:#34485f;background-image:none;border-color:#34485f;box-shadow:none;color:#fff}.tox .tox-button--secondary:active:not(:disabled){background-color:#2b3b4e;background-image:none;border-color:#2b3b4e;box-shadow:none;color:#fff}.tox .tox-button--secondary.tox-button--enabled{background-color:#2b5c93;background-image:none;border-color:#2b5c93;box-shadow:none;color:#fff}.tox .tox-button--secondary.tox-button--enabled[disabled]{background-color:#2b5c93;background-image:none;border-color:#2b5c93;box-shadow:none;color:rgba(255,255,255,.5)}.tox .tox-button--secondary.tox-button--enabled:focus:not(:disabled){background-color:#254f80;background-image:none;border-color:#254f80;box-shadow:none;color:#fff}.tox .tox-button--secondary.tox-button--enabled:hover:not(:disabled){background-color:#254f80;background-image:none;border-color:#254f80;box-shadow:none;color:#fff}.tox .tox-button--secondary.tox-button--enabled:active:not(:disabled){background-color:#1f436c;background-image:none;border-color:#1f436c;box-shadow:none;color:#fff}.tox .tox-button--icon,.tox .tox-button.tox-button--icon,.tox .tox-button.tox-button--secondary.tox-button--icon{padding:4px}.tox .tox-button--icon .tox-icon svg,.tox .tox-button.tox-button--icon .tox-icon svg,.tox .tox-button.tox-button--secondary.tox-button--icon .tox-icon svg{display:block;fill:currentColor}.tox .tox-button-link{background:0;border:none;box-sizing:border-box;cursor:pointer;display:inline-block;font-family:-apple-system,BlinkMacSystemFont,\\\"Segoe UI\\\",Roboto,Oxygen-Sans,Ubuntu,Cantarell,\\\"Helvetica Neue\\\",sans-serif;font-size:16px;font-weight:400;line-height:1.3;margin:0;padding:0;white-space:nowrap}.tox .tox-button-link--sm{font-size:14px}.tox .tox-button--naked{background-color:transparent;border-color:transparent;box-shadow:unset;color:#fff}.tox .tox-button--naked[disabled]{background-color:rgba(255,255,255,.2);border-color:transparent;box-shadow:unset;color:rgba(255,255,255,.5)}.tox .tox-button--naked:hover:not(:disabled){background-color:rgba(255,255,255,.2);border-color:transparent;box-shadow:unset;color:#fff}.tox .tox-button--naked:focus:not(:disabled){background-color:rgba(255,255,255,.2);border-color:transparent;box-shadow:unset;color:#fff}.tox .tox-button--naked:active:not(:disabled){background-color:rgba(255,255,255,.3);border-color:transparent;box-shadow:unset;color:#fff}.tox .tox-button--naked .tox-icon svg{fill:currentColor}.tox .tox-button--naked.tox-button--icon:hover:not(:disabled){color:#fff}.tox .tox-checkbox{align-items:center;border-radius:6px;cursor:pointer;display:flex;height:36px;min-width:36px}.tox .tox-checkbox__input{height:1px;overflow:hidden;position:absolute;top:auto;width:1px}.tox .tox-checkbox__icons{align-items:center;border-radius:6px;box-shadow:0 0 0 2px transparent;box-sizing:content-box;display:flex;height:24px;justify-content:center;padding:calc(4px - 1px);width:24px}.tox .tox-checkbox__icons .tox-checkbox-icon__unchecked svg{display:block;fill:rgba(255,255,255,.2)}@media (forced-colors:active){.tox .tox-checkbox__icons .tox-checkbox-icon__unchecked svg{fill:currentColor!important}}.tox .tox-checkbox__icons .tox-checkbox-icon__indeterminate svg{display:none;fill:#006ce7}.tox .tox-checkbox__icons .tox-checkbox-icon__checked svg{display:none;fill:#006ce7}.tox .tox-checkbox--disabled{color:rgba(255,255,255,.5);cursor:not-allowed}.tox .tox-checkbox--disabled .tox-checkbox__icons .tox-checkbox-icon__checked svg{fill:rgba(255,255,255,.5)}.tox .tox-checkbox--disabled .tox-checkbox__icons .tox-checkbox-icon__unchecked svg{fill:rgba(255,255,255,.5)}.tox .tox-checkbox--disabled .tox-checkbox__icons .tox-checkbox-icon__indeterminate svg{fill:rgba(255,255,255,.5)}.tox input.tox-checkbox__input:checked+.tox-checkbox__icons .tox-checkbox-icon__unchecked svg{display:none}.tox input.tox-checkbox__input:checked+.tox-checkbox__icons .tox-checkbox-icon__checked svg{display:block}.tox input.tox-checkbox__input:indeterminate+.tox-checkbox__icons .tox-checkbox-icon__unchecked svg{display:none}.tox input.tox-checkbox__input:indeterminate+.tox-checkbox__icons .tox-checkbox-icon__indeterminate svg{display:block}.tox input.tox-checkbox__input:focus+.tox-checkbox__icons{border-radius:6px;box-shadow:inset 0 0 0 1px #006ce7;padding:calc(4px - 1px)}.tox:not([dir=rtl]) .tox-checkbox__label{margin-left:4px}.tox:not([dir=rtl]) .tox-checkbox__input{left:-10000px}.tox:not([dir=rtl]) .tox-bar .tox-checkbox{margin-left:4px}.tox[dir=rtl] .tox-checkbox__label{margin-right:4px}.tox[dir=rtl] .tox-checkbox__input{right:-10000px}.tox[dir=rtl] .tox-bar .tox-checkbox{margin-right:4px}.tox .tox-collection--toolbar .tox-collection__group{display:flex;padding:0}.tox .tox-collection--grid .tox-collection__group{display:flex;flex-wrap:wrap;max-height:208px;overflow-x:hidden;overflow-y:auto;padding:0}.tox .tox-collection--list .tox-collection__group{border-bottom-width:0;border-color:rgba(255,255,255,.15);border-left-width:0;border-right-width:0;border-style:solid;border-top-width:1px;padding:4px 0}.tox .tox-collection--list .tox-collection__group:first-child{border-top-width:0}.tox .tox-collection__group-heading{background-color:rgba(255,255,255,.15);color:rgba(255,255,255,.5);cursor:default;font-size:12px;font-style:normal;font-weight:400;margin-bottom:4px;margin-top:-4px;padding:4px 8px;text-transform:none;-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}.tox .tox-collection__item{align-items:center;border-radius:3px;color:#fff;display:flex;-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}.tox .tox-collection--list .tox-collection__item{padding:4px 8px}.tox .tox-collection--toolbar .tox-collection__item{border-radius:3px;padding:4px}.tox .tox-collection--grid .tox-collection__item{border-radius:3px;padding:4px}.tox .tox-collection--list .tox-collection__item--enabled{background-color:#2b3b4e;color:#fff}.tox .tox-collection--list .tox-collection__item--active{background-color:#006ce7}.tox .tox-collection--toolbar .tox-collection__item--enabled,.tox .tox-collection--toolbar .tox-collection__item--enabled.tox-collection__item--active,.tox .tox-collection--toolbar .tox-collection__item--enabled.tox-collection__item--active:hover{background-color:#599fef;color:#fff}@media (forced-colors:active){.tox .tox-collection--toolbar .tox-collection__item--enabled,.tox .tox-collection--toolbar .tox-collection__item--enabled.tox-collection__item--active,.tox .tox-collection--toolbar .tox-collection__item--enabled.tox-collection__item--active:hover{border-radius:3px;outline:solid 1px}}.tox .tox-collection--toolbar .tox-collection__item--active{background-color:#2b3b4e;position:relative}.tox .tox-collection--toolbar .tox-collection__item--active:hover{background-color:#2f4055;color:#fff}.tox .tox-collection--toolbar .tox-collection__item--active:focus{background-color:#2f4055;color:#fff}.tox .tox-collection--toolbar .tox-collection__item--active:focus::after{border-radius:3px;bottom:0;box-shadow:0 0 0 2px #fff;content:'';left:0;position:absolute;right:0;top:0}@media (forced-colors:active){.tox .tox-collection--toolbar .tox-collection__item--active:focus::after{border:2px solid highlight}}.tox .tox-collection--grid .tox-collection__item--enabled{background-color:#599fef;color:#fff}.tox .tox-collection--grid .tox-collection__item--active:not(.tox-collection__item--state-disabled){background-color:#2f4055;color:#fff;position:relative;z-index:1}.tox .tox-collection--grid .tox-collection__item--active:not(.tox-collection__item--state-disabled):focus::after{border-radius:3px;bottom:0;box-shadow:0 0 0 2px #fff inset;content:'';left:0;position:absolute;right:0;top:0}@media (forced-colors:active){.tox .tox-collection--grid .tox-collection__item--active:not(.tox-collection__item--state-disabled):focus::after{border:2px solid highlight}}.tox .tox-collection--list .tox-collection__item--active:not(.tox-collection__item--state-disabled){color:#fff}@media (forced-colors:active){.tox .tox-collection--list .tox-collection__item--active:not(.tox-collection__item--state-disabled){border:solid 1px}}.tox .tox-collection--toolbar .tox-collection__item--active:not(.tox-collection__item--state-disabled){color:#fff}@media (forced-colors:active){.tox .tox-collection--toolbar .tox-collection__item--active:not(.tox-collection__item--state-disabled):hover{border-radius:3px;outline:solid 1px}}.tox .tox-collection__item-checkmark,.tox .tox-collection__item-icon{align-items:center;display:flex;height:24px;justify-content:center;width:24px}.tox .tox-collection__item-checkmark svg,.tox .tox-collection__item-icon svg{fill:currentColor}.tox .tox-collection--toolbar-lg .tox-collection__item-icon{height:48px;width:48px}.tox .tox-collection__item-label{color:currentColor;display:inline-block;flex:1;font-size:14px;font-style:normal;font-weight:400;line-height:24px;max-width:100%;text-transform:none;word-break:break-all}.tox .tox-collection__item-accessory{color:currentColor;display:inline-block;font-size:14px;height:24px;line-height:24px;text-transform:none}.tox .tox-collection__item-caret{align-items:center;display:flex;min-height:24px}.tox .tox-collection__item-caret::after{content:'';font-size:0;min-height:inherit}.tox .tox-collection__item-caret svg{fill:currentColor}.tox .tox-collection__item--state-disabled{background-color:transparent;color:rgba(255,255,255,.5);cursor:not-allowed}.tox .tox-collection__item--state-disabled .tox-collection__item-caret svg{fill:rgba(255,255,255,.5)}.tox .tox-collection--list .tox-collection__item:not(.tox-collection__item--enabled) .tox-collection__item-checkmark svg{display:none}.tox .tox-collection--list .tox-collection__item:not(.tox-collection__item--enabled) .tox-collection__item-accessory+.tox-collection__item-checkmark{display:none}.tox .tox-collection--horizontal{background-color:#2b3b4e;border:1px solid rgba(255,255,255,.15);border-radius:6px;box-shadow:0 0 2px 0 rgba(34,47,62,.2),0 4px 8px 0 rgba(34,47,62,.15);display:flex;flex:0 0 auto;flex-shrink:0;flex-wrap:nowrap;margin-bottom:0;overflow-x:auto;padding:0}.tox .tox-collection--horizontal .tox-collection__group{align-items:center;display:flex;flex-wrap:nowrap;margin:0;padding:0 4px}.tox .tox-collection--horizontal .tox-collection__item{height:28px;margin:6px 1px 5px 0;padding:0 4px}.tox .tox-collection--horizontal .tox-collection__item-label{white-space:nowrap}.tox .tox-collection--horizontal .tox-collection__item-caret{margin-left:4px}.tox .tox-collection__item-container{display:flex}.tox .tox-collection__item-container--row{align-items:center;flex:1 1 auto;flex-direction:row}.tox .tox-collection__item-container--row.tox-collection__item-container--align-left{margin-right:auto}.tox .tox-collection__item-container--row.tox-collection__item-container--align-right{justify-content:flex-end;margin-left:auto}.tox .tox-collection__item-container--row.tox-collection__item-container--valign-top{align-items:flex-start;margin-bottom:auto}.tox .tox-collection__item-container--row.tox-collection__item-container--valign-middle{align-items:center}.tox .tox-collection__item-container--row.tox-collection__item-container--valign-bottom{align-items:flex-end;margin-top:auto}.tox .tox-collection__item-container--column{align-self:center;flex:1 1 auto;flex-direction:column}.tox .tox-collection__item-container--column.tox-collection__item-container--align-left{align-items:flex-start}.tox .tox-collection__item-container--column.tox-collection__item-container--align-right{align-items:flex-end}.tox .tox-collection__item-container--column.tox-collection__item-container--valign-top{align-self:flex-start}.tox .tox-collection__item-container--column.tox-collection__item-container--valign-middle{align-self:center}.tox .tox-collection__item-container--column.tox-collection__item-container--valign-bottom{align-self:flex-end}.tox:not([dir=rtl]) .tox-collection--horizontal .tox-collection__group:not(:last-of-type){border-right:1px solid transparent}.tox:not([dir=rtl]) .tox-collection--list .tox-collection__item>:not(:first-child){margin-left:8px}.tox:not([dir=rtl]) .tox-collection--list .tox-collection__item>.tox-collection__item-label:first-child{margin-left:4px}.tox:not([dir=rtl]) .tox-collection__item-accessory{margin-left:16px;text-align:right}.tox:not([dir=rtl]) .tox-collection .tox-collection__item-caret{margin-left:16px}.tox[dir=rtl] .tox-collection--horizontal .tox-collection__group:not(:last-of-type){border-left:1px solid transparent}.tox[dir=rtl] .tox-collection--list .tox-collection__item>:not(:first-child){margin-right:8px}.tox[dir=rtl] .tox-collection--list .tox-collection__item>.tox-collection__item-label:first-child{margin-right:4px}.tox[dir=rtl] .tox-collection__item-accessory{margin-right:16px;text-align:left}.tox[dir=rtl] .tox-collection .tox-collection__item-caret{margin-right:16px;transform:rotateY(180deg)}.tox[dir=rtl] .tox-collection--horizontal .tox-collection__item-caret{margin-right:4px}@media (forced-colors:active){.tox .tox-hue-slider,.tox .tox-rgb-form .tox-rgba-preview{background-color:currentColor!important;border:1px solid highlight!important;forced-color-adjust:none}}.tox .tox-color-picker-container{display:flex;flex-direction:row;height:225px;margin:0}.tox .tox-sv-palette{box-sizing:border-box;display:flex;height:100%}.tox .tox-sv-palette-spectrum{height:100%}.tox .tox-sv-palette,.tox .tox-sv-palette-spectrum{width:225px}.tox .tox-sv-palette-thumb{background:0 0;border:1px solid #000;border-radius:50%;box-sizing:content-box;height:12px;position:absolute;width:12px}.tox .tox-sv-palette-inner-thumb{border:1px solid #fff;border-radius:50%;height:10px;position:absolute;width:10px}.tox .tox-hue-slider{box-sizing:border-box;height:100%;width:25px}.tox .tox-hue-slider-spectrum{background:linear-gradient(to bottom,red,#ff0080,#f0f,#8000ff,#00f,#0080ff,#0ff,#00ff80,#0f0,#80ff00,#ff0,#ff8000,red);height:100%;width:100%}.tox .tox-hue-slider,.tox .tox-hue-slider-spectrum{width:20px}.tox .tox-hue-slider-spectrum:focus,.tox .tox-sv-palette-spectrum:focus{outline:#08f solid}.tox .tox-hue-slider-thumb{background:#fff;border:1px solid #000;box-sizing:content-box;height:4px;width:100%}.tox .tox-rgb-form{display:flex;flex-direction:column;justify-content:space-between}.tox .tox-rgb-form div{align-items:center;display:flex;justify-content:space-between;margin-bottom:5px;width:inherit}.tox .tox-rgb-form input{width:6em}.tox .tox-rgb-form input.tox-invalid{border:1px solid red!important}.tox .tox-rgb-form .tox-rgba-preview{border:1px solid #000;flex-grow:2;margin-bottom:0}.tox:not([dir=rtl]) .tox-sv-palette{margin-right:15px}.tox:not([dir=rtl]) .tox-hue-slider{margin-right:15px}.tox:not([dir=rtl]) .tox-hue-slider-thumb{margin-left:-1px}.tox:not([dir=rtl]) .tox-rgb-form label{margin-right:.5em}.tox[dir=rtl] .tox-sv-palette{margin-left:15px}.tox[dir=rtl] .tox-hue-slider{margin-left:15px}.tox[dir=rtl] .tox-hue-slider-thumb{margin-right:-1px}.tox[dir=rtl] .tox-rgb-form label{margin-left:.5em}.tox .tox-toolbar .tox-swatches,.tox .tox-toolbar__overflow .tox-swatches,.tox .tox-toolbar__primary .tox-swatches{margin:5px 0 6px 11px}.tox .tox-collection--list .tox-collection__group .tox-swatches-menu{border:0;margin:-4px -4px}.tox .tox-swatches__row{display:flex}@media (forced-colors:active){.tox .tox-swatches__row{forced-color-adjust:none}}.tox .tox-swatch{height:30px;transition:transform .15s,box-shadow .15s;width:30px}.tox .tox-swatch:focus,.tox .tox-swatch:hover{box-shadow:0 0 0 1px rgba(127,127,127,.3) inset;transform:scale(.8)}.tox .tox-swatch--remove{align-items:center;display:flex;justify-content:center}.tox .tox-swatch--remove svg path{stroke:#e74c3c}.tox .tox-swatches__picker-btn{align-items:center;background-color:transparent;border:0;cursor:pointer;display:flex;height:30px;justify-content:center;outline:0;padding:0;width:30px}.tox .tox-swatches__picker-btn svg{fill:#fff;height:24px;width:24px}.tox .tox-swatches__picker-btn:hover{background:#2f4055}.tox div.tox-swatch:not(.tox-swatch--remove) svg{display:none;fill:#fff;height:24px;margin:calc((30px - 24px)/ 2) calc((30px - 24px)/ 2);width:24px}.tox div.tox-swatch:not(.tox-swatch--remove) svg path{fill:#fff;paint-order:stroke;stroke:#222f3e;stroke-width:2px}.tox div.tox-swatch:not(.tox-swatch--remove).tox-collection__item--enabled svg{display:block}.tox:not([dir=rtl]) .tox-swatches__picker-btn{margin-left:auto}.tox[dir=rtl] .tox-swatches__picker-btn{margin-right:auto}.tox .tox-comment-thread{background:#2b3b4e;position:relative}.tox .tox-comment-thread>:not(:first-child){margin-top:8px}.tox .tox-comment{background:#2b3b4e;border:1px solid #161f29;border-radius:6px;box-shadow:0 4px 8px 0 rgba(34,47,62,.1);padding:8px 8px 16px 8px;position:relative}.tox .tox-comment__header{align-items:center;color:#fff;display:flex;justify-content:space-between}.tox .tox-comment__date{color:#fff;font-size:12px;line-height:18px}.tox .tox-comment__body{color:#fff;font-size:14px;font-style:normal;font-weight:400;line-height:1.3;margin-top:8px;position:relative;text-transform:initial}.tox .tox-comment__body textarea{resize:none;white-space:normal;width:100%}.tox .tox-comment__expander{padding-top:8px}.tox .tox-comment__expander p{color:rgba(255,255,255,.5);font-size:14px;font-style:normal}.tox .tox-comment__body p{margin:0}.tox .tox-comment__buttonspacing{padding-top:16px;text-align:center}.tox .tox-comment-thread__overlay::after{background:#2b3b4e;bottom:0;content:\\\"\\\";display:flex;left:0;opacity:.9;position:absolute;right:0;top:0;z-index:5}.tox .tox-comment__reply{display:flex;flex-shrink:0;flex-wrap:wrap;justify-content:flex-end;margin-top:8px}.tox .tox-comment__reply>:first-child{margin-bottom:8px;width:100%}.tox .tox-comment__edit{display:flex;flex-wrap:wrap;justify-content:flex-end;margin-top:16px}.tox .tox-comment__gradient::after{background:linear-gradient(rgba(43,59,78,0),#2b3b4e);bottom:0;content:\\\"\\\";display:block;height:5em;margin-top:-40px;position:absolute;width:100%}.tox .tox-comment__overlay{background:#2b3b4e;bottom:0;display:flex;flex-direction:column;flex-grow:1;left:0;opacity:.9;position:absolute;right:0;text-align:center;top:0;z-index:5}.tox .tox-comment__loading-text{align-items:center;color:#fff;display:flex;flex-direction:column;position:relative}.tox .tox-comment__loading-text>div{padding-bottom:16px}.tox .tox-comment__overlaytext{bottom:0;flex-direction:column;font-size:14px;left:0;padding:1em;position:absolute;right:0;top:0;z-index:10}.tox .tox-comment__overlaytext p{background-color:#2b3b4e;box-shadow:0 0 8px 8px #2b3b4e;color:#fff;text-align:center}.tox .tox-comment__overlaytext div:nth-of-type(2){font-size:.8em}.tox .tox-comment__busy-spinner{align-items:center;background-color:#2b3b4e;bottom:0;display:flex;justify-content:center;left:0;position:absolute;right:0;top:0;z-index:20}.tox .tox-comment__scroll{display:flex;flex-direction:column;flex-shrink:1;overflow:auto}.tox .tox-conversations{margin:8px}.tox:not([dir=rtl]) .tox-comment__edit{margin-left:8px}.tox:not([dir=rtl]) .tox-comment__buttonspacing>:last-child,.tox:not([dir=rtl]) .tox-comment__edit>:last-child,.tox:not([dir=rtl]) .tox-comment__reply>:last-child{margin-left:8px}.tox[dir=rtl] .tox-comment__edit{margin-right:8px}.tox[dir=rtl] .tox-comment__buttonspacing>:last-child,.tox[dir=rtl] .tox-comment__edit>:last-child,.tox[dir=rtl] .tox-comment__reply>:last-child{margin-right:8px}.tox .tox-user{align-items:center;display:flex}.tox .tox-user__avatar svg{fill:rgba(255,255,255,.5)}.tox .tox-user__avatar img{border-radius:50%;height:36px;object-fit:cover;vertical-align:middle;width:36px}.tox .tox-user__name{color:#fff;font-size:14px;font-style:normal;font-weight:700;line-height:18px;text-transform:none}.tox:not([dir=rtl]) .tox-user__avatar img,.tox:not([dir=rtl]) .tox-user__avatar svg{margin-right:8px}.tox:not([dir=rtl]) .tox-user__avatar+.tox-user__name{margin-left:8px}.tox[dir=rtl] .tox-user__avatar img,.tox[dir=rtl] .tox-user__avatar svg{margin-left:8px}.tox[dir=rtl] .tox-user__avatar+.tox-user__name{margin-right:8px}.tox .tox-dialog-wrap{align-items:center;bottom:0;display:flex;justify-content:center;left:0;position:fixed;right:0;top:0;z-index:1100}.tox .tox-dialog-wrap__backdrop{background-color:rgba(34,47,62,.75);bottom:0;left:0;position:absolute;right:0;top:0;z-index:1}.tox .tox-dialog-wrap__backdrop--opaque{background-color:#222f3e}.tox .tox-dialog{background-color:#2b3b4e;border-color:#161f29;border-radius:10px;border-style:solid;border-width:0;box-shadow:0 16px 16px -10px rgba(34,47,62,.15),0 0 40px 1px rgba(34,47,62,.15);display:flex;flex-direction:column;max-height:100%;max-width:480px;overflow:hidden;position:relative;width:95vw;z-index:2}@media only screen and (max-width:767px){body:not(.tox-force-desktop) .tox .tox-dialog{align-self:flex-start;margin:8px auto;max-height:calc(100vh - 8px * 2);width:calc(100vw - 16px)}}.tox .tox-dialog-inline{z-index:1100}.tox .tox-dialog__header{align-items:center;background-color:#2b3b4e;border-bottom:none;color:#fff;display:flex;font-size:16px;justify-content:space-between;padding:8px 16px 0 16px;position:relative}.tox .tox-dialog__header .tox-button{z-index:1}.tox .tox-dialog__draghandle{cursor:grab;height:100%;left:0;position:absolute;top:0;width:100%}.tox .tox-dialog__draghandle:active{cursor:grabbing}.tox .tox-dialog__dismiss{margin-left:auto}.tox .tox-dialog__title{font-family:-apple-system,BlinkMacSystemFont,\\\"Segoe UI\\\",Roboto,Oxygen-Sans,Ubuntu,Cantarell,\\\"Helvetica Neue\\\",sans-serif;font-size:20px;font-style:normal;font-weight:400;line-height:1.3;margin:0;text-transform:none}.tox .tox-dialog__body{color:#fff;display:flex;flex:1;font-size:16px;font-style:normal;font-weight:400;line-height:1.3;min-width:0;text-align:left;text-transform:none}@media only screen and (max-width:767px){body:not(.tox-force-desktop) .tox .tox-dialog__body{flex-direction:column}}.tox .tox-dialog__body-nav{align-items:flex-start;display:flex;flex-direction:column;flex-shrink:0;padding:16px 16px}@media only screen and (min-width:768px){.tox .tox-dialog__body-nav{max-width:11em}}@media only screen and (max-width:767px){body:not(.tox-force-desktop) .tox .tox-dialog__body-nav{flex-direction:row;-webkit-overflow-scrolling:touch;overflow-x:auto;padding-bottom:0}}.tox .tox-dialog__body-nav-item{border-bottom:2px solid transparent;color:rgba(255,255,255,.5);display:inline-block;flex-shrink:0;font-size:14px;line-height:1.3;margin-bottom:8px;max-width:13em;text-decoration:none}.tox .tox-dialog__body-nav-item:focus{background-color:rgba(0,108,231,.1)}.tox .tox-dialog__body-nav-item--active{border-bottom:2px solid #67aeff;color:#67aeff}@media (forced-colors:active){.tox .tox-dialog__body-nav-item--active{border-bottom:2px solid highlight;color:highlight}}.tox .tox-dialog__body-content{box-sizing:border-box;display:flex;flex:1;flex-direction:column;max-height:min(650px,calc(100vh - 110px));overflow:auto;-webkit-overflow-scrolling:touch;padding:16px 16px}.tox .tox-dialog__body-content>*{margin-bottom:0;margin-top:16px}.tox .tox-dialog__body-content>:first-child{margin-top:0}.tox .tox-dialog__body-content>:last-child{margin-bottom:0}.tox .tox-dialog__body-content>:only-child{margin-bottom:0;margin-top:0}.tox .tox-dialog__body-content a{color:#67aeff;cursor:pointer;text-decoration:underline}.tox .tox-dialog__body-content a:focus,.tox .tox-dialog__body-content a:hover{color:#cde5ff;text-decoration:underline}.tox .tox-dialog__body-content a:focus-visible{border-radius:1px;outline:2px solid #67aeff;outline-offset:2px}.tox .tox-dialog__body-content a:active{color:#fff;text-decoration:underline}.tox .tox-dialog__body-content svg{fill:#fff}.tox .tox-dialog__body-content strong{font-weight:700}.tox .tox-dialog__body-content ul{list-style-type:disc}.tox .tox-dialog__body-content dd,.tox .tox-dialog__body-content ol,.tox .tox-dialog__body-content ul{padding-inline-start:2.5rem}.tox .tox-dialog__body-content dl,.tox .tox-dialog__body-content ol,.tox .tox-dialog__body-content ul{margin-bottom:16px}.tox .tox-dialog__body-content dd,.tox .tox-dialog__body-content dl,.tox .tox-dialog__body-content dt,.tox .tox-dialog__body-content ol,.tox .tox-dialog__body-content ul{display:block;margin-inline-end:0;margin-inline-start:0}.tox .tox-dialog__body-content .tox-form__group h1{color:#fff;font-size:20px;font-style:normal;font-weight:700;letter-spacing:normal;margin-bottom:16px;margin-top:2rem;text-transform:none}.tox .tox-dialog__body-content .tox-form__group h2{color:#fff;font-size:16px;font-style:normal;font-weight:700;letter-spacing:normal;margin-bottom:16px;margin-top:2rem;text-transform:none}.tox .tox-dialog__body-content .tox-form__group p{margin-bottom:16px}.tox .tox-dialog__body-content .tox-form__group h1:first-child,.tox .tox-dialog__body-content .tox-form__group h2:first-child,.tox .tox-dialog__body-content .tox-form__group p:first-child{margin-top:0}.tox .tox-dialog__body-content .tox-form__group h1:last-child,.tox .tox-dialog__body-content .tox-form__group h2:last-child,.tox .tox-dialog__body-content .tox-form__group p:last-child{margin-bottom:0}.tox .tox-dialog__body-content .tox-form__group h1:only-child,.tox .tox-dialog__body-content .tox-form__group h2:only-child,.tox .tox-dialog__body-content .tox-form__group p:only-child{margin-bottom:0;margin-top:0}.tox .tox-dialog__body-content .tox-form__group .tox-label.tox-label--center{text-align:center}.tox .tox-dialog__body-content .tox-form__group .tox-label.tox-label--end{text-align:end}.tox .tox-dialog--width-lg{height:650px;max-width:1200px}.tox .tox-dialog--fullscreen{height:100%;max-width:100%}.tox .tox-dialog--fullscreen .tox-dialog__body-content{max-height:100%}.tox .tox-dialog--width-md{max-width:800px}.tox .tox-dialog--width-md .tox-dialog__body-content{overflow:auto}.tox .tox-dialog__body-content--centered{text-align:center}.tox .tox-dialog__footer{align-items:center;background-color:#2b3b4e;border-top:none;display:flex;justify-content:space-between;padding:8px 16px}.tox .tox-dialog__footer-end,.tox .tox-dialog__footer-start{display:flex}.tox .tox-dialog__busy-spinner{align-items:center;background-color:rgba(34,47,62,.75);bottom:0;display:flex;justify-content:center;left:0;position:absolute;right:0;top:0;z-index:3}.tox .tox-dialog__table{border-collapse:collapse;width:100%}.tox .tox-dialog__table thead th{font-weight:700;padding-bottom:8px}.tox .tox-dialog__table thead th:first-child{padding-right:8px}.tox .tox-dialog__table tbody tr{border-bottom:1px solid #000}.tox .tox-dialog__table tbody tr:last-child{border-bottom:none}.tox .tox-dialog__table td{padding-bottom:8px;padding-top:8px}.tox .tox-dialog__table td:first-child{padding-right:8px}.tox .tox-dialog__iframe{min-height:200px}.tox .tox-dialog__iframe.tox-dialog__iframe--opaque{background:#fff}.tox .tox-navobj-bordered{position:relative}.tox .tox-navobj-bordered::before{border:1px solid #161f29;border-radius:6px;content:'';inset:0;opacity:1;pointer-events:none;position:absolute;z-index:1}.tox .tox-navobj-bordered iframe{border-radius:6px}.tox .tox-navobj-bordered-focus.tox-navobj-bordered::before{border-color:#006ce7;box-shadow:0 0 0 1px #006ce7;outline:0}.tox .tox-dialog__popups{position:absolute;width:100%;z-index:1100}.tox .tox-dialog__body-iframe{display:flex;flex:1;flex-direction:column}.tox .tox-dialog__body-iframe .tox-navobj{display:flex;flex:1}.tox .tox-dialog__body-iframe .tox-navobj :nth-child(2){flex:1;height:100%}.tox .tox-dialog-dock-fadeout{opacity:0;visibility:hidden}.tox .tox-dialog-dock-fadein{opacity:1;visibility:visible}.tox .tox-dialog-dock-transition{transition:visibility 0s linear .3s,opacity .3s ease}.tox .tox-dialog-dock-transition.tox-dialog-dock-fadein{transition-delay:0s}@media only screen and (max-width:767px){body:not(.tox-force-desktop) .tox:not([dir=rtl]) .tox-dialog__body-nav{margin-right:0}}@media only screen and (max-width:767px){body:not(.tox-force-desktop) .tox:not([dir=rtl]) .tox-dialog__body-nav-item:not(:first-child){margin-left:8px}}.tox:not([dir=rtl]) .tox-dialog__footer .tox-dialog__footer-end>*,.tox:not([dir=rtl]) .tox-dialog__footer .tox-dialog__footer-start>*{margin-left:8px}.tox[dir=rtl] .tox-dialog__body{text-align:right}@media only screen and (max-width:767px){body:not(.tox-force-desktop) .tox[dir=rtl] .tox-dialog__body-nav{margin-left:0}}@media only screen and (max-width:767px){body:not(.tox-force-desktop) .tox[dir=rtl] .tox-dialog__body-nav-item:not(:first-child){margin-right:8px}}.tox[dir=rtl] .tox-dialog__footer .tox-dialog__footer-end>*,.tox[dir=rtl] .tox-dialog__footer .tox-dialog__footer-start>*{margin-right:8px}body.tox-dialog__disable-scroll{overflow:hidden}.tox .tox-dropzone-container{display:flex;flex:1}.tox .tox-dropzone{align-items:center;background:#fff;border:2px dashed #161f29;box-sizing:border-box;display:flex;flex-direction:column;flex-grow:1;justify-content:center;min-height:100px;padding:10px}.tox .tox-dropzone p{color:rgba(255,255,255,.5);margin:0 0 16px 0}.tox .tox-edit-area{display:flex;flex:1;overflow:hidden;position:relative}.tox .tox-edit-area::before{border:2px solid #fff;border-radius:4px;content:'';inset:0;opacity:0;pointer-events:none;position:absolute;transition:opacity .15s;z-index:1}@media (forced-colors:active){.tox .tox-edit-area::before{border:2px solid highlight}}.tox .tox-edit-area__iframe{background-color:#fff;border:0;box-sizing:border-box;flex:1;height:100%;position:absolute;width:100%}.tox.tox-edit-focus .tox-edit-area::before{opacity:1}.tox.tox-inline-edit-area{border:1px dotted #161f29}.tox .tox-editor-container{display:flex;flex:1 1 auto;flex-direction:column;overflow:hidden}.tox .tox-editor-header{display:grid;grid-template-columns:1fr min-content;z-index:2}.tox:not(.tox-tinymce-inline) .tox-editor-header{background-color:#222f3e;border-bottom:1px solid rgba(255,255,255,.15);box-shadow:none;padding:4px 0}.tox:not(.tox-tinymce-inline) .tox-editor-header:not(.tox-editor-dock-transition){transition:box-shadow .5s}.tox:not(.tox-tinymce-inline).tox-tinymce--toolbar-bottom .tox-editor-header{border-top:1px solid rgba(255,255,255,.15);box-shadow:none}.tox:not(.tox-tinymce-inline).tox-tinymce--toolbar-sticky-on .tox-editor-header{background-color:#222f3e;box-shadow:none;padding:4px 0}.tox:not(.tox-tinymce-inline).tox-tinymce--toolbar-sticky-on.tox-tinymce--toolbar-bottom .tox-editor-header{box-shadow:none}.tox.tox:not(.tox-tinymce-inline) .tox-editor-header.tox-editor-header--empty{background:0 0;border:none;box-shadow:none;padding:0}.tox-editor-dock-fadeout{opacity:0;visibility:hidden}.tox-editor-dock-fadein{opacity:1;visibility:visible}.tox-editor-dock-transition{transition:visibility 0s linear .25s,opacity .25s ease}.tox-editor-dock-transition.tox-editor-dock-fadein{transition-delay:0s}.tox .tox-control-wrap{flex:1;position:relative}.tox .tox-control-wrap:not(.tox-control-wrap--status-invalid) .tox-control-wrap__status-icon-invalid,.tox .tox-control-wrap:not(.tox-control-wrap--status-unknown) .tox-control-wrap__status-icon-unknown,.tox .tox-control-wrap:not(.tox-control-wrap--status-valid) .tox-control-wrap__status-icon-valid{display:none}.tox .tox-control-wrap svg{display:block}.tox .tox-control-wrap__status-icon-wrap{position:absolute;top:50%;transform:translateY(-50%)}.tox .tox-control-wrap__status-icon-invalid svg{fill:#c00}.tox .tox-control-wrap__status-icon-unknown svg{fill:orange}.tox .tox-control-wrap__status-icon-valid svg{fill:green}.tox:not([dir=rtl]) .tox-control-wrap--status-invalid .tox-textfield,.tox:not([dir=rtl]) .tox-control-wrap--status-unknown .tox-textfield,.tox:not([dir=rtl]) .tox-control-wrap--status-valid .tox-textfield{padding-right:32px}.tox:not([dir=rtl]) .tox-control-wrap__status-icon-wrap{right:4px}.tox[dir=rtl] .tox-control-wrap--status-invalid .tox-textfield,.tox[dir=rtl] .tox-control-wrap--status-unknown .tox-textfield,.tox[dir=rtl] .tox-control-wrap--status-valid .tox-textfield{padding-left:32px}.tox[dir=rtl] .tox-control-wrap__status-icon-wrap{left:4px}.tox .tox-custom-preview{border-color:#161f29;border-radius:6px;border-style:solid;border-width:1px;flex:1;padding:8px}.tox .tox-autocompleter{max-width:25em}.tox .tox-autocompleter .tox-menu{box-sizing:border-box;max-width:25em}.tox .tox-autocompleter .tox-autocompleter-highlight{font-weight:700}.tox .tox-color-input{display:flex;position:relative;z-index:1}.tox .tox-color-input .tox-textfield{z-index:-1}.tox .tox-color-input span{border-color:rgba(34,47,62,.2);border-radius:6px;border-style:solid;border-width:1px;box-shadow:none;box-sizing:border-box;height:24px;position:absolute;top:6px;width:24px}@media (forced-colors:active){.tox .tox-color-input span{border-color:currentColor;border-width:2px!important;forced-color-adjust:none}}.tox .tox-color-input span:focus:not([aria-disabled=true]),.tox .tox-color-input span:hover:not([aria-disabled=true]){border-color:#006ce7;cursor:pointer}.tox .tox-color-input span::before{background-image:linear-gradient(45deg,rgba(255,255,255,.25) 25%,transparent 25%),linear-gradient(-45deg,rgba(255,255,255,.25) 25%,transparent 25%),linear-gradient(45deg,transparent 75%,rgba(255,255,255,.25) 75%),linear-gradient(-45deg,transparent 75%,rgba(255,255,255,.25) 75%);background-position:0 0,0 6px,6px -6px,-6px 0;background-size:12px 12px;border:1px solid #2b3b4e;border-radius:6px;box-sizing:border-box;content:'';height:24px;left:-1px;position:absolute;top:-1px;width:24px;z-index:-1}@media (forced-colors:active){.tox .tox-color-input span::before{border:none}}.tox .tox-color-input span[aria-disabled=true]{cursor:not-allowed}.tox:not([dir=rtl]) .tox-color-input .tox-textfield{padding-left:36px}.tox:not([dir=rtl]) .tox-color-input span{left:6px}.tox[dir=rtl] .tox-color-input .tox-textfield{padding-right:36px}.tox[dir=rtl] .tox-color-input span{right:6px}.tox .tox-label,.tox .tox-toolbar-label{color:rgba(255,255,255,.5);display:block;font-size:14px;font-style:normal;font-weight:400;line-height:1.3;padding:0 8px 0 0;text-transform:none;white-space:nowrap}.tox .tox-toolbar-label{padding:0 8px}.tox[dir=rtl] .tox-label{padding:0 0 0 8px}.tox .tox-form{display:flex;flex:1;flex-direction:column}.tox .tox-form__group{box-sizing:border-box;margin-bottom:4px}.tox .tox-form-group--maximize{flex:1}.tox .tox-form__group--error{color:#c00}.tox .tox-form__group--collection{display:flex}.tox .tox-form__grid{display:flex;flex-direction:row;flex-wrap:wrap;justify-content:space-between}.tox .tox-form__grid--2col>.tox-form__group{width:calc(50% - (8px / 2))}.tox .tox-form__grid--3col>.tox-form__group{width:calc(100% / 3 - (8px / 2))}.tox .tox-form__grid--4col>.tox-form__group{width:calc(25% - (8px / 2))}.tox .tox-form__controls-h-stack{align-items:center;display:flex}.tox .tox-form__group--inline{align-items:center;display:flex}.tox .tox-form__group--stretched{display:flex;flex:1;flex-direction:column}.tox .tox-form__group--stretched .tox-textarea{flex:1}.tox .tox-form__group--stretched .tox-navobj{display:flex;flex:1}.tox .tox-form__group--stretched .tox-navobj :nth-child(2){flex:1;height:100%}.tox:not([dir=rtl]) .tox-form__controls-h-stack>:not(:first-child){margin-left:4px}.tox[dir=rtl] .tox-form__controls-h-stack>:not(:first-child){margin-right:4px}.tox .tox-lock.tox-locked .tox-lock-icon__unlock,.tox .tox-lock:not(.tox-locked) .tox-lock-icon__lock{display:none}.tox .tox-listboxfield .tox-listbox--select,.tox .tox-textarea,.tox .tox-textarea-wrap .tox-textarea:focus,.tox .tox-textfield,.tox .tox-toolbar-textfield{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#2b3b4e;border-color:#161f29;border-radius:6px;border-style:solid;border-width:1px;box-shadow:none;box-sizing:border-box;color:#fff;font-family:-apple-system,BlinkMacSystemFont,\\\"Segoe UI\\\",Roboto,Oxygen-Sans,Ubuntu,Cantarell,\\\"Helvetica Neue\\\",sans-serif;font-size:16px;line-height:24px;margin:0;min-height:34px;outline:0;padding:5px 5.5px;resize:none;width:100%}.tox .tox-textarea[disabled],.tox .tox-textfield[disabled]{background-color:#222f3e;color:rgba(255,255,255,.85);cursor:not-allowed}.tox .tox-custom-editor:focus-within,.tox .tox-listboxfield .tox-listbox--select:focus,.tox .tox-textarea-wrap:focus-within,.tox .tox-textarea:focus,.tox .tox-textfield:focus{background-color:#2b3b4e;border-color:#006ce7;box-shadow:0 0 0 1px #006ce7;outline:0}.tox .tox-toolbar-textfield{border-width:0;margin-bottom:3px;margin-top:2px;max-width:250px}.tox .tox-naked-btn{background-color:transparent;border:0;border-color:transparent;box-shadow:unset;color:#006ce7;cursor:pointer;display:block;margin:0;padding:0}.tox .tox-naked-btn svg{display:block;fill:#fff}.tox:not([dir=rtl]) .tox-toolbar-textfield+*{margin-left:4px}.tox[dir=rtl] .tox-toolbar-textfield+*{margin-right:4px}.tox .tox-listboxfield{cursor:pointer;position:relative}.tox .tox-listboxfield .tox-listbox--select[disabled]{background-color:#19232e;color:rgba(255,255,255,.85);cursor:not-allowed}.tox .tox-listbox__select-label{cursor:default;flex:1;margin:0 4px}.tox .tox-listbox__select-chevron{align-items:center;display:flex;justify-content:center;width:16px}.tox .tox-listbox__select-chevron svg{fill:#fff}@media (forced-colors:active){.tox .tox-listbox__select-chevron svg{fill:currentColor!important}}.tox .tox-listboxfield .tox-listbox--select{align-items:center;display:flex}.tox:not([dir=rtl]) .tox-listboxfield svg{right:8px}.tox[dir=rtl] .tox-listboxfield svg{left:8px}.tox .tox-selectfield{cursor:pointer;position:relative}.tox .tox-selectfield select{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#2b3b4e;border-color:#161f29;border-radius:6px;border-style:solid;border-width:1px;box-shadow:none;box-sizing:border-box;color:#fff;font-family:-apple-system,BlinkMacSystemFont,\\\"Segoe UI\\\",Roboto,Oxygen-Sans,Ubuntu,Cantarell,\\\"Helvetica Neue\\\",sans-serif;font-size:16px;line-height:24px;margin:0;min-height:34px;outline:0;padding:5px 5.5px;resize:none;width:100%}.tox .tox-selectfield select[disabled]{background-color:#19232e;color:rgba(255,255,255,.85);cursor:not-allowed}.tox .tox-selectfield select::-ms-expand{display:none}.tox .tox-selectfield select:focus{background-color:#2b3b4e;border-color:#006ce7;box-shadow:0 0 0 1px #006ce7;outline:0}.tox .tox-selectfield svg{pointer-events:none;position:absolute;top:50%;transform:translateY(-50%)}.tox:not([dir=rtl]) .tox-selectfield select[size=\\\"0\\\"],.tox:not([dir=rtl]) .tox-selectfield select[size=\\\"1\\\"]{padding-right:24px}.tox:not([dir=rtl]) .tox-selectfield svg{right:8px}.tox[dir=rtl] .tox-selectfield select[size=\\\"0\\\"],.tox[dir=rtl] .tox-selectfield select[size=\\\"1\\\"]{padding-left:24px}.tox[dir=rtl] .tox-selectfield svg{left:8px}.tox .tox-textarea-wrap{border-color:#161f29;border-radius:6px;border-style:solid;border-width:1px;display:flex;flex:1;overflow:hidden}.tox .tox-textarea{-webkit-appearance:textarea;-moz-appearance:textarea;appearance:textarea;white-space:pre-wrap}.tox .tox-textarea-wrap .tox-textarea{border:none}.tox .tox-textarea-wrap .tox-textarea:focus{border:none}.tox-fullscreen{border:0;height:100%;margin:0;overflow:hidden;overscroll-behavior:none;padding:0;touch-action:pinch-zoom;width:100%}.tox.tox-tinymce.tox-fullscreen .tox-statusbar__resize-handle{display:none}.tox-shadowhost.tox-fullscreen,.tox.tox-tinymce.tox-fullscreen{left:0;position:fixed;top:0;z-index:1200}.tox.tox-tinymce.tox-fullscreen{background-color:transparent}.tox-fullscreen .tox.tox-tinymce-aux,.tox-fullscreen~.tox.tox-tinymce-aux{z-index:1201}.tox .tox-help__more-link{list-style:none;margin-top:1em}.tox .tox-imagepreview{background-color:#666;height:380px;overflow:hidden;position:relative;width:100%}.tox .tox-imagepreview.tox-imagepreview__loaded{overflow:auto}.tox .tox-imagepreview__container{display:flex;left:100vw;position:absolute;top:100vw}.tox .tox-imagepreview__image{background:url(data:image/gif;base64,R0lGODdhDAAMAIABAMzMzP///ywAAAAADAAMAAACFoQfqYeabNyDMkBQb81Uat85nxguUAEAOw==)}.tox .tox-image-tools .tox-spacer{flex:1}.tox .tox-image-tools .tox-bar{align-items:center;display:flex;height:60px;justify-content:center}.tox .tox-image-tools .tox-imagepreview,.tox .tox-image-tools .tox-imagepreview+.tox-bar{margin-top:8px}.tox .tox-image-tools .tox-croprect-block{background:#000;opacity:.5;position:absolute;zoom:1}.tox .tox-image-tools .tox-croprect-handle{border:2px solid #fff;height:20px;left:0;position:absolute;top:0;width:20px}.tox .tox-image-tools .tox-croprect-handle-move{border:0;cursor:move;position:absolute}.tox .tox-image-tools .tox-croprect-handle-nw{border-width:2px 0 0 2px;cursor:nw-resize;left:100px;margin:-2px 0 0 -2px;top:100px}.tox .tox-image-tools .tox-croprect-handle-ne{border-width:2px 2px 0 0;cursor:ne-resize;left:200px;margin:-2px 0 0 -20px;top:100px}.tox .tox-image-tools .tox-croprect-handle-sw{border-width:0 0 2px 2px;cursor:sw-resize;left:100px;margin:-20px 2px 0 -2px;top:200px}.tox .tox-image-tools .tox-croprect-handle-se{border-width:0 2px 2px 0;cursor:se-resize;left:200px;margin:-20px 0 0 -20px;top:200px}.tox .tox-insert-table-picker{background-color:#222f3e;display:flex;flex-wrap:wrap;width:170px}.tox .tox-insert-table-picker>div{border-color:rgba(255,255,255,.15);border-style:solid;border-width:0 1px 1px 0;box-sizing:border-box;height:17px;width:17px}.tox .tox-collection--list .tox-collection__group .tox-insert-table-picker{margin:-4px -4px}.tox .tox-insert-table-picker .tox-insert-table-picker__selected{background-color:#006ce7;border-color:rgba(255,255,255,.15)}@media (forced-colors:active){.tox .tox-insert-table-picker .tox-insert-table-picker__selected{border-color:Highlight;filter:contrast(50%)}}.tox .tox-insert-table-picker__label{color:#fff;display:block;font-size:14px;padding:4px;text-align:center;width:100%}.tox:not([dir=rtl]) .tox-insert-table-picker>div:nth-child(10n){border-right:0}.tox[dir=rtl] .tox-insert-table-picker>div:nth-child(10n+1){border-right:0}.tox .tox-menu{background-color:#2b3b4e;border:1px solid rgba(255,255,255,.15);border-radius:6px;box-shadow:none;display:inline-block;overflow:hidden;vertical-align:top;z-index:1150}.tox .tox-menu.tox-collection.tox-collection--list{padding:0 4px}.tox .tox-menu.tox-collection.tox-collection--toolbar{padding:8px}.tox .tox-menu.tox-collection.tox-collection--grid{padding:8px}@media only screen and (min-width:768px){.tox .tox-menu .tox-collection__item-label{overflow-wrap:break-word;word-break:normal}.tox .tox-dialog__popups .tox-menu .tox-collection__item-label{word-break:break-all}}.tox .tox-menu__label blockquote,.tox .tox-menu__label code,.tox .tox-menu__label h1,.tox .tox-menu__label h2,.tox .tox-menu__label h3,.tox .tox-menu__label h4,.tox .tox-menu__label h5,.tox .tox-menu__label h6,.tox .tox-menu__label p{margin:0}.tox .tox-menubar{background:repeating-linear-gradient(transparent 0 1px,transparent 1px 39px) center top 39px/100% calc(100% - 39px) no-repeat;background-color:#222f3e;display:flex;flex:0 0 auto;flex-shrink:0;flex-wrap:wrap;grid-column:1/-1;grid-row:1;padding:0 11px 0 12px}.tox .tox-promotion+.tox-menubar{grid-column:1}.tox .tox-promotion{background:repeating-linear-gradient(transparent 0 1px,transparent 1px 39px) center top 39px/100% calc(100% - 39px) no-repeat;background-color:#222f3e;grid-column:2;grid-row:1;padding-inline-end:8px;padding-inline-start:4px;padding-top:5px}.tox .tox-promotion-link{align-items:unsafe center;background-color:#e8f1f8;border-radius:5px;color:#086be6;cursor:pointer;display:flex;font-size:14px;height:26.6px;padding:4px 8px;white-space:nowrap}.tox .tox-promotion-link:hover{background-color:#b4d7ff}.tox .tox-promotion-link:focus{background-color:#d9edf7}.tox .tox-mbtn{align-items:center;background:#222f3e;border:0;border-radius:3px;box-shadow:none;color:#fff;display:flex;flex:0 0 auto;font-size:14px;font-style:normal;font-weight:400;height:28px;justify-content:center;margin:5px 1px 6px 0;outline:0;padding:0 4px;text-transform:none;width:auto}.tox .tox-mbtn[disabled]{background-color:#222f3e;border:0;box-shadow:none;color:rgba(255,255,255,.5);cursor:not-allowed}.tox .tox-mbtn:focus:not(:disabled){background:#222f3e;border:0;box-shadow:none;color:#fff;position:relative;z-index:1}.tox .tox-mbtn:focus:not(:disabled)::after{border-radius:3px;bottom:0;box-shadow:0 0 0 2px #fff;content:'';left:0;position:absolute;right:0;top:0}@media (forced-colors:active){.tox .tox-mbtn:focus:not(:disabled)::after{border:2px solid highlight}}.tox .tox-mbtn--active,.tox .tox-mbtn:not(:disabled).tox-mbtn--active:focus{background:#599fef;border:0;box-shadow:none;color:#fff}.tox .tox-mbtn:hover:not(:disabled):not(.tox-mbtn--active){background:#2f4055;border:0;box-shadow:none;color:#fff}.tox .tox-mbtn__select-label{cursor:default;font-weight:400;margin:0 4px}.tox .tox-mbtn[disabled] .tox-mbtn__select-label{cursor:not-allowed}.tox .tox-mbtn__select-chevron{align-items:center;display:flex;justify-content:center;width:16px;display:none}.tox .tox-notification{border-radius:6px;border-style:solid;border-width:1px;box-shadow:none;box-sizing:border-box;display:grid;font-size:14px;font-weight:400;grid-template-columns:minmax(40px,1fr) auto minmax(40px,1fr);margin-left:auto;margin-right:auto;margin-top:4px;opacity:0;padding:4px;transition:transform .1s ease-in,opacity 150ms ease-in;width:-moz-max-content;width:max-content}.tox .tox-notification a{cursor:pointer;text-decoration:underline}.tox .tox-notification p{font-size:14px;font-weight:400}.tox .tox-notification:focus{border-color:#006ce7;box-shadow:0 0 0 1px #006ce7}.tox .tox-notification--in{opacity:1}.tox .tox-notification--success{background-color:#334840;border-color:#3c5440;color:#fff}.tox .tox-notification--success p{color:#fff}.tox .tox-notification--success a{color:#b5d199}.tox .tox-notification--success a:focus,.tox .tox-notification--success a:hover{color:#82b153;text-decoration:underline}.tox .tox-notification--success a:focus-visible{border-radius:1px;outline:2px solid #b5d199;outline-offset:2px}.tox .tox-notification--success a:active{color:#689041;text-decoration:underline}.tox .tox-notification--success svg{fill:#fff}.tox .tox-notification--error{background-color:#442632;border-color:#55212b;color:#fff}.tox .tox-notification--error p{color:#fff}.tox .tox-notification--error a{color:#e68080}.tox .tox-notification--error a:focus,.tox .tox-notification--error a:hover{color:#d42b2b;text-decoration:underline}.tox .tox-notification--error a:focus-visible{border-radius:1px;outline:2px solid #e68080;outline-offset:2px}.tox .tox-notification--error a:active{color:#a22;text-decoration:underline}.tox .tox-notification--error svg{fill:#fff}.tox .tox-notification--warn,.tox .tox-notification--warning{background-color:#222f3e;border-color:rgba(255,255,255,.15);color:#fff0b3}.tox .tox-notification--warn p,.tox .tox-notification--warning p{color:#fff0b3}.tox .tox-notification--warn a,.tox .tox-notification--warning a{color:#fc0}.tox .tox-notification--warn a:focus,.tox .tox-notification--warn a:hover,.tox .tox-notification--warning a:focus,.tox .tox-notification--warning a:hover{color:#997a00;text-decoration:underline}.tox .tox-notification--warn a:focus-visible,.tox .tox-notification--warning a:focus-visible{border-radius:1px;outline:2px solid #fc0;outline-offset:2px}.tox .tox-notification--warn a:active,.tox .tox-notification--warning a:active{color:#665200;text-decoration:underline}.tox .tox-notification--warn svg,.tox .tox-notification--warning svg{fill:#fff0b3}.tox .tox-notification--info{background-color:#254161;border-color:#264972;color:#fff}.tox .tox-notification--info p{color:#fff}.tox .tox-notification--info a{color:#83b7f3}.tox .tox-notification--info a:focus,.tox .tox-notification--info a:hover{color:#2681ea;text-decoration:underline}.tox .tox-notification--info a:focus-visible{border-radius:1px;outline:2px solid #83b7f3;outline-offset:2px}.tox .tox-notification--info a:active{color:#1368c9;text-decoration:underline}.tox .tox-notification--info svg{fill:#fff}.tox .tox-notification__body{align-self:center;color:#fff;font-size:14px;grid-column-end:3;grid-column-start:2;grid-row-end:2;grid-row-start:1;text-align:center;white-space:normal;word-break:break-all;word-break:break-word}.tox .tox-notification__body>*{margin:0}.tox .tox-notification__body>*+*{margin-top:1rem}.tox .tox-notification__icon{align-self:center;grid-column-end:2;grid-column-start:1;grid-row-end:2;grid-row-start:1;justify-self:end}.tox .tox-notification__icon svg{display:block}.tox .tox-notification__dismiss{align-self:start;grid-column-end:4;grid-column-start:3;grid-row-end:2;grid-row-start:1;justify-self:end}.tox .tox-notification .tox-progress-bar{grid-column-end:4;grid-column-start:1;grid-row-end:3;grid-row-start:2;justify-self:center}.tox .tox-notification-container-dock-fadeout{opacity:0;visibility:hidden}.tox .tox-notification-container-dock-fadein{opacity:1;visibility:visible}.tox .tox-notification-container-dock-transition{transition:visibility 0s linear .3s,opacity .3s ease}.tox .tox-notification-container-dock-transition.tox-notification-container-dock-fadein{transition-delay:0s}.tox .tox-pop{display:inline-block;position:relative}.tox .tox-pop--resizing{transition:width .1s ease}.tox .tox-pop--resizing .tox-toolbar,.tox .tox-pop--resizing .tox-toolbar__group{flex-wrap:nowrap}.tox .tox-pop--transition{transition:.15s ease;transition-property:left,right,top,bottom}.tox .tox-pop--transition::after,.tox .tox-pop--transition::before{transition:all .15s,visibility 0s,opacity 75ms ease 75ms}.tox .tox-pop__dialog{background-color:#222f3e;border:1px solid #161f29;border-radius:6px;box-shadow:0 0 2px 0 rgba(34,47,62,.2),0 4px 8px 0 rgba(34,47,62,.15);min-width:0;overflow:hidden}.tox .tox-pop__dialog>:not(.tox-toolbar){margin:4px 4px 4px 8px}.tox .tox-pop__dialog .tox-toolbar{background-color:transparent;margin-bottom:-1px}.tox .tox-pop::after,.tox .tox-pop::before{border-style:solid;content:'';display:block;height:0;opacity:1;position:absolute;width:0}@media (forced-colors:active){.tox .tox-pop::after,.tox .tox-pop::before{content:none}}.tox .tox-pop.tox-pop--inset::after,.tox .tox-pop.tox-pop--inset::before{opacity:0;transition:all 0s .15s,visibility 0s,opacity 75ms ease}.tox .tox-pop.tox-pop--bottom::after,.tox .tox-pop.tox-pop--bottom::before{left:50%;top:100%}.tox .tox-pop.tox-pop--bottom::after{border-color:#222f3e transparent transparent transparent;border-width:8px;margin-left:-8px;margin-top:-1px}.tox .tox-pop.tox-pop--bottom::before{border-color:#161f29 transparent transparent transparent;border-width:9px;margin-left:-9px}.tox .tox-pop.tox-pop--top::after,.tox .tox-pop.tox-pop--top::before{left:50%;top:0;transform:translateY(-100%)}.tox .tox-pop.tox-pop--top::after{border-color:transparent transparent #222f3e transparent;border-width:8px;margin-left:-8px;margin-top:1px}.tox .tox-pop.tox-pop--top::before{border-color:transparent transparent #161f29 transparent;border-width:9px;margin-left:-9px}.tox .tox-pop.tox-pop--left::after,.tox .tox-pop.tox-pop--left::before{left:0;top:calc(50% - 1px);transform:translateY(-50%)}.tox .tox-pop.tox-pop--left::after{border-color:transparent #222f3e transparent transparent;border-width:8px;margin-left:-15px}.tox .tox-pop.tox-pop--left::before{border-color:transparent #161f29 transparent transparent;border-width:10px;margin-left:-19px}.tox .tox-pop.tox-pop--right::after,.tox .tox-pop.tox-pop--right::before{left:100%;top:calc(50% + 1px);transform:translateY(-50%)}.tox .tox-pop.tox-pop--right::after{border-color:transparent transparent transparent #222f3e;border-width:8px;margin-left:-1px}.tox .tox-pop.tox-pop--right::before{border-color:transparent transparent transparent #161f29;border-width:10px;margin-left:-1px}.tox .tox-pop.tox-pop--align-left::after,.tox .tox-pop.tox-pop--align-left::before{left:20px}.tox .tox-pop.tox-pop--align-right::after,.tox .tox-pop.tox-pop--align-right::before{left:calc(100% - 20px)}.tox .tox-sidebar-wrap{display:flex;flex-direction:row;flex-grow:1;min-height:0}.tox .tox-sidebar{background-color:#222f3e;display:flex;flex-direction:row;justify-content:flex-end}.tox .tox-sidebar__slider{display:flex;overflow:hidden}.tox .tox-sidebar__pane-container{display:flex}.tox .tox-sidebar__pane{display:flex}.tox .tox-sidebar--sliding-closed{opacity:0}.tox .tox-sidebar--sliding-open{opacity:1}.tox .tox-sidebar--sliding-growing,.tox .tox-sidebar--sliding-shrinking{transition:width .5s ease,opacity .5s ease}.tox .tox-selector{background-color:#4099ff;border-color:#4099ff;border-style:solid;border-width:1px;box-sizing:border-box;display:inline-block;height:10px;position:absolute;width:10px}.tox.tox-platform-touch .tox-selector{height:12px;width:12px}.tox .tox-slider{align-items:center;display:flex;flex:1;height:24px;justify-content:center;position:relative}.tox .tox-slider__rail{background-color:transparent;border:1px solid #161f29;border-radius:6px;height:10px;min-width:120px;width:100%}.tox .tox-slider__handle{background-color:#006ce7;border:2px solid #0054b4;border-radius:6px;box-shadow:none;height:24px;left:50%;position:absolute;top:50%;transform:translateX(-50%) translateY(-50%);width:14px}.tox .tox-form__controls-h-stack>.tox-slider:not(:first-of-type){margin-inline-start:8px}.tox .tox-form__controls-h-stack>.tox-form__group+.tox-slider{margin-inline-start:32px}.tox .tox-form__controls-h-stack>.tox-slider+.tox-form__group{margin-inline-start:32px}.tox .tox-source-code{overflow:auto}.tox .tox-spinner{display:flex}.tox .tox-spinner>div{animation:tam-bouncing-dots 1.5s ease-in-out 0s infinite both;background-color:rgba(255,255,255,.5);border-radius:100%;height:8px;width:8px}.tox .tox-spinner>div:nth-child(1){animation-delay:-.32s}.tox .tox-spinner>div:nth-child(2){animation-delay:-.16s}@keyframes tam-bouncing-dots{0%,100%,80%{transform:scale(0)}40%{transform:scale(1)}}.tox:not([dir=rtl]) .tox-spinner>div:not(:first-child){margin-left:4px}.tox[dir=rtl] .tox-spinner>div:not(:first-child){margin-right:4px}.tox .tox-statusbar{align-items:center;background-color:#222f3e;border-top:1px solid rgba(255,255,255,.15);color:rgba(255,255,255,.75);display:flex;flex:0 0 auto;font-size:14px;font-weight:400;height:25px;overflow:hidden;padding:0 8px;position:relative;text-transform:none}.tox .tox-statusbar__path{display:flex;flex:1 1 auto;text-overflow:ellipsis;white-space:nowrap}.tox .tox-statusbar__right-container{display:flex;justify-content:flex-end;white-space:nowrap}.tox .tox-statusbar__help-text{text-align:center}.tox .tox-statusbar__text-container{align-items:flex-start;display:flex;flex:1 1 auto;height:16px;justify-content:space-between;overflow:hidden}@media only screen and (min-width:768px){.tox .tox-statusbar__text-container.tox-statusbar__text-container-3-cols>.tox-statusbar__help-text,.tox .tox-statusbar__text-container.tox-statusbar__text-container-3-cols>.tox-statusbar__path,.tox .tox-statusbar__text-container.tox-statusbar__text-container-3-cols>.tox-statusbar__right-container{flex:0 0 calc(100% / 3)}}.tox .tox-statusbar__text-container.tox-statusbar__text-container--flex-end{justify-content:flex-end}.tox .tox-statusbar__text-container.tox-statusbar__text-container--flex-start{justify-content:flex-start}.tox .tox-statusbar__text-container.tox-statusbar__text-container--space-around{justify-content:space-around}.tox .tox-statusbar__path>*{display:inline;white-space:nowrap}.tox .tox-statusbar__wordcount{flex:0 0 auto;margin-left:1ch}@media only screen and (max-width:767px){.tox .tox-statusbar__text-container .tox-statusbar__help-text{display:none}.tox .tox-statusbar__text-container .tox-statusbar__help-text:only-child{display:block}}.tox .tox-statusbar a,.tox .tox-statusbar__path-item,.tox .tox-statusbar__wordcount{color:rgba(255,255,255,.75);position:relative;text-decoration:none}.tox .tox-statusbar a:focus:not(:disabled):not([aria-disabled=true]),.tox .tox-statusbar a:hover:not(:disabled):not([aria-disabled=true]),.tox .tox-statusbar__path-item:focus:not(:disabled):not([aria-disabled=true]),.tox .tox-statusbar__path-item:hover:not(:disabled):not([aria-disabled=true]),.tox .tox-statusbar__wordcount:focus:not(:disabled):not([aria-disabled=true]),.tox .tox-statusbar__wordcount:hover:not(:disabled):not([aria-disabled=true]){color:#fff;cursor:pointer}.tox .tox-statusbar a:focus-visible::after,.tox .tox-statusbar__path-item:focus-visible::after,.tox .tox-statusbar__wordcount:focus-visible::after{border-radius:3px;bottom:0;box-shadow:0 0 0 2px #fff;content:'';left:0;position:absolute;right:0;top:0}@media (forced-colors:active){.tox .tox-statusbar a:focus-visible::after,.tox .tox-statusbar__path-item:focus-visible::after,.tox .tox-statusbar__wordcount:focus-visible::after{border:2px solid highlight}}.tox .tox-statusbar__branding svg{fill:rgba(255,255,255,.8);height:1em;margin-left:.3em;width:auto}@media (forced-colors:active){.tox .tox-statusbar__branding svg{fill:currentColor}}.tox .tox-statusbar__branding a{align-items:center;display:inline-flex}.tox .tox-statusbar__branding a:focus:not(:disabled):not([aria-disabled=true]) svg,.tox .tox-statusbar__branding a:hover:not(:disabled):not([aria-disabled=true]) svg{fill:#fff}.tox .tox-statusbar__resize-handle{align-items:flex-end;align-self:stretch;cursor:nwse-resize;display:flex;flex:0 0 auto;justify-content:flex-end;margin-bottom:3px;margin-left:4px;margin-right:calc(3px - 8px);margin-top:3px;padding-bottom:0;padding-left:0;padding-right:0;position:relative}.tox .tox-statusbar__resize-handle svg{display:block;fill:rgba(255,255,255,.5)}.tox .tox-statusbar__resize-handle:focus svg,.tox .tox-statusbar__resize-handle:hover svg{fill:#fff}.tox .tox-statusbar__resize-handle:focus-visible{background-color:transparent;border-radius:1px 1px 5px 1px;box-shadow:0 0 0 2px transparent}.tox .tox-statusbar__resize-handle:focus-visible::after{border-radius:3px;bottom:0;box-shadow:0 0 0 2px #fff;content:'';left:0;position:absolute;right:0;top:0}@media (forced-colors:active){.tox .tox-statusbar__resize-handle:focus-visible::after{border:2px solid highlight}}.tox:not([dir=rtl]) .tox-statusbar__path>*{margin-right:4px}.tox:not([dir=rtl]) .tox-statusbar__branding{margin-left:2ch}.tox[dir=rtl] .tox-statusbar{flex-direction:row-reverse}.tox[dir=rtl] .tox-statusbar__path>*{margin-left:4px}.tox[dir=rtl] .tox-statusbar__branding svg{margin-left:0;margin-right:.3em}.tox .tox-throbber{z-index:1299}.tox .tox-throbber__busy-spinner{align-items:center;background-color:rgba(34,47,62,.6);bottom:0;display:flex;justify-content:center;left:0;position:absolute;right:0;top:0}.tox .tox-tbtn{align-items:center;background:#222f3e;border:0;border-radius:3px;box-shadow:none;color:#fff;display:flex;flex:0 0 auto;font-size:14px;font-style:normal;font-weight:400;height:28px;justify-content:center;margin:6px 1px 5px 0;outline:0;padding:0;text-transform:none;width:34px}@media (forced-colors:active){.tox .tox-tbtn.tox-tbtn:hover,.tox .tox-tbtn:hover{outline:1px dashed currentColor}.tox .tox-tbtn.tox-tbtn--active,.tox .tox-tbtn.tox-tbtn--enabled,.tox .tox-tbtn.tox-tbtn--enabled:focus,.tox .tox-tbtn.tox-tbtn--enabled:hover,.tox .tox-tbtn:focus:not(.tox-tbtn--disabled){outline:1px solid currentColor;position:relative}}.tox .tox-tbtn svg{display:block;fill:#fff}@media (forced-colors:active){.tox .tox-tbtn svg{fill:currentColor!important}.tox .tox-tbtn svg.tox-tbtn--enabled,.tox .tox-tbtn svg:focus:not(.tox-tbtn--disabled){fill:currentColor!important}.tox .tox-tbtn svg .tox-tbtn:disabled,.tox .tox-tbtn svg .tox-tbtn:disabled:hover,.tox .tox-tbtn svg.tox-tbtn--disabled,.tox .tox-tbtn svg.tox-tbtn--disabled:hover{filter:contrast(0)}}.tox .tox-tbtn.tox-tbtn-more{padding-left:5px;padding-right:5px;width:inherit}.tox .tox-tbtn:focus{background:#222f3e;border:0;box-shadow:none;position:relative;z-index:1}.tox .tox-tbtn:focus::after{border-radius:3px;bottom:0;box-shadow:0 0 0 2px #fff;content:'';left:0;position:absolute;right:0;top:0}@media (forced-colors:active){.tox .tox-tbtn:focus::after{border:2px solid highlight}}.tox .tox-tbtn:hover{background:#2f4055;border:0;box-shadow:none;color:#fff}.tox .tox-tbtn:hover svg{fill:#fff}.tox .tox-tbtn:active{background:#599fef;border:0;box-shadow:none;color:#fff}.tox .tox-tbtn:active svg{fill:#fff}.tox .tox-tbtn--disabled .tox-tbtn--enabled svg{fill:rgba(255,255,255,.5)}.tox .tox-tbtn--disabled,.tox .tox-tbtn--disabled:hover,.tox .tox-tbtn:disabled,.tox .tox-tbtn:disabled:hover{background:#222f3e;border:0;box-shadow:none;color:rgba(255,255,255,.5);cursor:not-allowed}.tox .tox-tbtn--disabled svg,.tox .tox-tbtn--disabled:hover svg,.tox .tox-tbtn:disabled svg,.tox .tox-tbtn:disabled:hover svg{fill:rgba(255,255,255,.5)}.tox .tox-tbtn--active,.tox .tox-tbtn--enabled,.tox .tox-tbtn--enabled:focus,.tox .tox-tbtn--enabled:hover{background:#599fef;border:0;box-shadow:none;color:#fff;position:relative}.tox .tox-tbtn--active>*,.tox .tox-tbtn--enabled:focus>*,.tox .tox-tbtn--enabled:hover>*,.tox .tox-tbtn--enabled>*{transform:none}.tox .tox-tbtn--active svg,.tox .tox-tbtn--enabled svg,.tox .tox-tbtn--enabled:focus svg,.tox .tox-tbtn--enabled:hover svg{fill:#fff}.tox .tox-tbtn--active.tox-tbtn--disabled svg,.tox .tox-tbtn--enabled.tox-tbtn--disabled svg,.tox .tox-tbtn--enabled:focus.tox-tbtn--disabled svg,.tox .tox-tbtn--enabled:hover.tox-tbtn--disabled svg{fill:rgba(255,255,255,.5)}.tox .tox-tbtn--enabled:focus::after{border-radius:3px;bottom:0;box-shadow:0 0 0 2px #fff;content:'';left:0;position:absolute;right:0;top:0}@media (forced-colors:active){.tox .tox-tbtn--enabled:focus::after{border:2px solid highlight}}.tox .tox-tbtn:focus:not(.tox-tbtn--disabled){color:#fff}.tox .tox-tbtn:focus:not(.tox-tbtn--disabled) svg{fill:#fff}.tox .tox-tbtn:active>*{transform:none}.tox .tox-tbtn--md{height:42px;width:51px}.tox .tox-tbtn--lg{flex-direction:column;height:56px;width:68px}.tox .tox-tbtn--return{align-self:stretch;height:unset;width:16px}.tox .tox-tbtn--labeled{padding:0 4px;width:unset}.tox .tox-tbtn__vlabel{display:block;font-size:10px;font-weight:400;letter-spacing:-.025em;margin-bottom:4px;white-space:nowrap}.tox .tox-number-input{background:#2f4055;border-radius:3px;display:flex;margin:6px 1px 5px 0;position:relative;width:auto}.tox .tox-number-input:focus{background:#2f4055}.tox .tox-number-input:focus::after{border-radius:3px;bottom:0;box-shadow:0 0 0 2px #fff;content:'';left:0;position:absolute;right:0;top:0}@media (forced-colors:active){.tox .tox-number-input:focus::after{border:2px solid highlight}}.tox .tox-number-input .tox-input-wrapper{display:flex;pointer-events:none;position:relative;text-align:center}.tox .tox-number-input .tox-input-wrapper:focus{background-color:#2f4055;z-index:1}.tox .tox-number-input .tox-input-wrapper:focus::after{border-radius:3px;bottom:0;box-shadow:0 0 0 2px #fff;content:'';left:0;position:absolute;right:0;top:0}@media (forced-colors:active){.tox .tox-number-input .tox-input-wrapper:focus::after{border:2px solid highlight}}.tox .tox-number-input .tox-input-wrapper:has(input:focus)::after{border-radius:3px;bottom:0;box-shadow:0 0 0 2px #fff;content:'';left:0;position:absolute;right:0;top:0}@media (forced-colors:active){.tox .tox-number-input .tox-input-wrapper:has(input:focus)::after{border:2px solid highlight}}.tox .tox-number-input input{border-radius:3px;color:#fff;font-size:14px;margin:2px 0;pointer-events:all;position:relative;width:60px}.tox .tox-number-input input:hover{background:#2f4055;color:#fff}.tox .tox-number-input input:focus{background-color:#2f4055}.tox .tox-number-input input:disabled{background:#222f3e;border:0;box-shadow:none;color:rgba(255,255,255,.5);cursor:not-allowed}.tox .tox-number-input button{color:#fff;height:28px;position:relative;text-align:center;width:24px}@media (forced-colors:active){.tox .tox-number-input button:active,.tox .tox-number-input button:focus,.tox .tox-number-input button:hover{outline:1px solid currentColor!important}}.tox .tox-number-input button svg{display:block;fill:#fff;margin:0 auto;transform:scale(.67)}@media (forced-colors:active){.tox .tox-number-input button svg,.tox .tox-number-input button svg:active,.tox .tox-number-input button svg:hover{fill:currentColor!important}.tox .tox-number-input button svg:disabled{filter:contrast(0)}}.tox .tox-number-input button:focus{background:#2f4055;z-index:1}.tox .tox-number-input button:focus::after{border-radius:3px;bottom:0;box-shadow:0 0 0 2px #fff;content:'';left:0;position:absolute;right:0;top:0}@media (forced-colors:active){.tox .tox-number-input button:focus::after{border:2px solid highlight}}.tox .tox-number-input button:hover{background:#2f4055;border:0;box-shadow:none;color:#fff}.tox .tox-number-input button:hover svg{fill:#fff}.tox .tox-number-input button:active{background:#599fef;border:0;box-shadow:none;color:#fff}.tox .tox-number-input button:active svg{fill:#fff}.tox .tox-number-input button:disabled{background:#222f3e;border:0;box-shadow:none;color:rgba(255,255,255,.5);cursor:not-allowed}.tox .tox-number-input button:disabled svg{fill:rgba(255,255,255,.5)}.tox .tox-number-input button.minus{border-radius:3px 0 0 3px}.tox .tox-number-input button.plus{border-radius:0 3px 3px 0}.tox .tox-number-input:focus:not(:active)>.tox-input-wrapper,.tox .tox-number-input:focus:not(:active)>button{background:#2f4055}.tox .tox-tbtn--select{margin:6px 1px 5px 0;padding:0 4px;width:auto}.tox .tox-tbtn__select-label{cursor:default;font-weight:400;height:initial;margin:0 4px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.tox .tox-tbtn__select-chevron{align-items:center;display:flex;justify-content:center;width:16px}.tox .tox-tbtn__select-chevron svg{fill:rgba(255,255,255,.5)}@media (forced-colors:active){.tox .tox-tbtn__select-chevron svg{fill:currentColor}}.tox .tox-tbtn--bespoke{background:#2f4055}.tox .tox-tbtn--bespoke:focus{background:#2f4055}.tox .tox-tbtn--bespoke+.tox-tbtn--bespoke{margin-inline-start:4px}.tox .tox-tbtn--bespoke .tox-tbtn__select-label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;width:7em}.tox .tox-tbtn--disabled .tox-tbtn__select-label,.tox .tox-tbtn--select:disabled .tox-tbtn__select-label{cursor:not-allowed}.tox .tox-split-button{border:0;border-radius:3px;box-sizing:border-box;display:flex;margin:6px 1px 5px 0}.tox .tox-split-button:hover{box-shadow:0 0 0 1px #2f4055 inset}.tox .tox-split-button:focus{background:#222f3e;box-shadow:none;color:#fff;position:relative;z-index:1}.tox .tox-split-button:focus::after{pointer-events:none;border-radius:3px;bottom:0;box-shadow:0 0 0 2px #fff;content:'';left:0;position:absolute;right:0;top:0}@media (forced-colors:active){.tox .tox-split-button:focus::after{border:2px solid highlight}}.tox .tox-split-button>*{border-radius:0}.tox .tox-split-button>:nth-child(1){border-bottom-left-radius:3px;border-top-left-radius:3px}.tox .tox-split-button>:nth-child(2){border-bottom-right-radius:3px;border-top-right-radius:3px}.tox .tox-split-button__chevron{width:16px}.tox .tox-split-button__chevron svg{fill:rgba(255,255,255,.5)}@media (forced-colors:active){.tox .tox-split-button__chevron svg{fill:currentColor}}.tox .tox-split-button .tox-tbtn{margin:0}.tox .tox-split-button:focus .tox-tbtn{background-color:transparent}.tox .tox-split-button.tox-tbtn--disabled .tox-tbtn:focus,.tox .tox-split-button.tox-tbtn--disabled .tox-tbtn:hover,.tox .tox-split-button.tox-tbtn--disabled:focus,.tox .tox-split-button.tox-tbtn--disabled:hover{background:#222f3e;box-shadow:none;color:rgba(255,255,255,.5)}.tox.tox-platform-touch .tox-split-button .tox-tbtn--select{padding:0 0}.tox.tox-platform-touch .tox-split-button .tox-tbtn:not(.tox-tbtn--select):first-child{width:30px}.tox.tox-platform-touch .tox-split-button__chevron{width:20px}.tox .tox-split-button.tox-tbtn--disabled svg #tox-icon-highlight-bg-color__color,.tox .tox-split-button.tox-tbtn--disabled svg #tox-icon-text-color__color{opacity:.6}.tox .tox-toolbar-overlord{background-color:#222f3e}.tox .tox-toolbar,.tox .tox-toolbar__overflow,.tox .tox-toolbar__primary{background-attachment:local;background-color:#222f3e;background-image:repeating-linear-gradient(rgba(255,255,255,.15) 0 1px,transparent 1px 39px);background-position:center top 40px;background-repeat:no-repeat;background-size:calc(100% - 11px * 2) calc(100% - 41px);display:flex;flex:0 0 auto;flex-shrink:0;flex-wrap:wrap;padding:0 0;transform:perspective(1px)}.tox .tox-toolbar-overlord>.tox-toolbar,.tox .tox-toolbar-overlord>.tox-toolbar__overflow,.tox .tox-toolbar-overlord>.tox-toolbar__primary{background-position:center top 0;background-size:calc(100% - 11px * 2) calc(100% - 0px)}.tox .tox-toolbar__overflow.tox-toolbar__overflow--closed{height:0;opacity:0;padding-bottom:0;padding-top:0;visibility:hidden}.tox .tox-toolbar__overflow--growing{transition:height .3s ease,opacity .2s linear .1s}.tox .tox-toolbar__overflow--shrinking{transition:opacity .3s ease,height .2s linear .1s,visibility 0s linear .3s}.tox .tox-anchorbar,.tox .tox-toolbar-overlord{grid-column:1/-1}.tox .tox-menubar+.tox-toolbar,.tox .tox-menubar+.tox-toolbar-overlord{border-top:1px solid transparent;margin-top:-1px;padding-bottom:1px;padding-top:1px}@media (forced-colors:active){.tox .tox-menubar+.tox-toolbar,.tox .tox-menubar+.tox-toolbar-overlord{outline:1px solid currentColor}}.tox .tox-toolbar--scrolling{flex-wrap:nowrap;overflow-x:auto}.tox .tox-pop .tox-toolbar{border-width:0}.tox .tox-toolbar--no-divider{background-image:none}.tox .tox-toolbar-overlord .tox-toolbar:not(.tox-toolbar--scrolling):first-child,.tox .tox-toolbar-overlord .tox-toolbar__primary{background-position:center top 39px}.tox .tox-editor-header>.tox-toolbar--scrolling,.tox .tox-toolbar-overlord .tox-toolbar--scrolling:first-child{background-image:none}.tox.tox-tinymce-aux .tox-toolbar__overflow{background-color:#222f3e;background-position:center top 43px;background-size:calc(100% - 8px * 2) calc(100% - 51px);border:none;border-radius:6px;box-shadow:0 0 2px 0 rgba(34,47,62,.2),0 4px 8px 0 rgba(34,47,62,.15);overscroll-behavior:none;padding:4px 0}@media (forced-colors:active){.tox.tox-tinymce-aux .tox-toolbar__overflow{border:solid}}.tox-pop .tox-pop__dialog .tox-toolbar{background-position:center top 43px;background-size:calc(100% - 11px * 2) calc(100% - 51px);padding:4px 0}.tox .tox-toolbar__group{align-items:center;display:flex;flex-wrap:wrap;margin:0 0;padding:0 11px 0 12px}.tox .tox-toolbar__group--pull-right{margin-left:auto}.tox .tox-toolbar--scrolling .tox-toolbar__group{flex-shrink:0;flex-wrap:nowrap}.tox:not([dir=rtl]) .tox-toolbar__group:not(:last-of-type){border-right:1px solid transparent}.tox[dir=rtl] .tox-toolbar__group:not(:last-of-type){border-left:1px solid transparent}.tox .tox-tooltip{display:inline-block;max-width:15em;padding:8px;pointer-events:none;position:relative;width:-moz-max-content;width:max-content;z-index:1150}.tox .tox-tooltip__body{background-color:#324053;border-radius:6px;box-shadow:none;color:#fff;font-size:12px;font-style:normal;font-weight:600;overflow-wrap:break-word;padding:4px 6px;text-transform:none}@media (forced-colors:active){.tox .tox-tooltip__body{outline:outset 1px}}.tox .tox-tooltip__arrow{position:absolute}.tox .tox-tooltip--down .tox-tooltip__arrow{border-left:8px solid transparent;border-right:8px solid transparent;border-top:8px solid #324053;bottom:0;left:50%;position:absolute;transform:translateX(-50%)}.tox .tox-tooltip--up .tox-tooltip__arrow{border-bottom:8px solid #324053;border-left:8px solid transparent;border-right:8px solid transparent;left:50%;position:absolute;top:0;transform:translateX(-50%)}.tox .tox-tooltip--right .tox-tooltip__arrow{border-bottom:8px solid transparent;border-left:8px solid #324053;border-top:8px solid transparent;position:absolute;right:0;top:50%;transform:translateY(-50%)}.tox .tox-tooltip--left .tox-tooltip__arrow{border-bottom:8px solid transparent;border-right:8px solid #324053;border-top:8px solid transparent;left:0;position:absolute;top:50%;transform:translateY(-50%)}.tox .tox-tree{display:flex;flex-direction:column}.tox .tox-tree .tox-trbtn{align-items:center;background:0 0;border:0;border-radius:4px;box-shadow:none;color:#fff;display:flex;flex:0 0 auto;font-size:14px;font-style:normal;font-weight:400;height:28px;margin-bottom:4px;margin-top:4px;outline:0;overflow:hidden;padding:0;padding-left:8px;text-transform:none}.tox .tox-tree .tox-trbtn .tox-tree__label{cursor:default;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.tox .tox-tree .tox-trbtn svg{display:block;fill:#fff}.tox .tox-tree .tox-trbtn:focus{background:#2f4055;border:0;box-shadow:none}.tox .tox-tree .tox-trbtn:hover{background:#2f4055;border:0;box-shadow:none;color:#fff}.tox .tox-tree .tox-trbtn:hover svg{fill:#fff}.tox .tox-tree .tox-trbtn:active{background:#599fef;border:0;box-shadow:none;color:#fff}.tox .tox-tree .tox-trbtn:active svg{fill:#fff}.tox .tox-tree .tox-trbtn--disabled,.tox .tox-tree .tox-trbtn--disabled:hover,.tox .tox-tree .tox-trbtn:disabled,.tox .tox-tree .tox-trbtn:disabled:hover{background:0 0;border:0;box-shadow:none;color:rgba(255,255,255,.5);cursor:not-allowed}.tox .tox-tree .tox-trbtn--disabled svg,.tox .tox-tree .tox-trbtn--disabled:hover svg,.tox .tox-tree .tox-trbtn:disabled svg,.tox .tox-tree .tox-trbtn:disabled:hover svg{fill:rgba(255,255,255,.5)}.tox .tox-tree .tox-trbtn--enabled,.tox .tox-tree .tox-trbtn--enabled:hover{background:#599fef;border:0;box-shadow:none;color:#fff}.tox .tox-tree .tox-trbtn--enabled:hover>*,.tox .tox-tree .tox-trbtn--enabled>*{transform:none}.tox .tox-tree .tox-trbtn--enabled svg,.tox .tox-tree .tox-trbtn--enabled:hover svg{fill:#fff}.tox .tox-tree .tox-trbtn:focus:not(.tox-trbtn--disabled){color:#fff}.tox .tox-tree .tox-trbtn:focus:not(.tox-trbtn--disabled) svg{fill:#fff}.tox .tox-tree .tox-trbtn:active>*{transform:none}.tox .tox-tree .tox-trbtn--return{align-self:stretch;height:unset;width:16px}.tox .tox-tree .tox-trbtn--labeled{padding:0 4px;width:unset}.tox .tox-tree .tox-trbtn__vlabel{display:block;font-size:10px;font-weight:400;letter-spacing:-.025em;margin-bottom:4px;white-space:nowrap}.tox .tox-tree .tox-tree--directory{display:flex;flex-direction:column}.tox .tox-tree .tox-tree--directory .tox-tree--directory__label{font-weight:700}.tox .tox-tree .tox-tree--directory .tox-tree--directory__label .tox-mbtn{margin-left:auto}.tox .tox-tree .tox-tree--directory .tox-tree--directory__label .tox-mbtn svg{fill:transparent}.tox .tox-tree .tox-tree--directory .tox-tree--directory__label .tox-mbtn.tox-mbtn--active svg,.tox .tox-tree .tox-tree--directory .tox-tree--directory__label .tox-mbtn:focus svg{fill:#fff}.tox .tox-tree .tox-tree--directory .tox-tree--directory__label:focus .tox-mbtn svg,.tox .tox-tree .tox-tree--directory .tox-tree--directory__label:hover .tox-mbtn svg{fill:#fff}.tox .tox-tree .tox-tree--directory .tox-tree--directory__label:hover:has(.tox-mbtn:hover){background-color:transparent;color:#fff}.tox .tox-tree .tox-tree--directory .tox-tree--directory__label:hover:has(.tox-mbtn:hover) .tox-chevron svg{fill:#fff}.tox .tox-tree .tox-tree--directory .tox-tree--directory__label .tox-chevron{margin-right:6px}.tox .tox-tree .tox-tree--directory .tox-tree--directory__label:has(+.tox-tree--directory__children--growing) .tox-chevron,.tox .tox-tree .tox-tree--directory .tox-tree--directory__label:has(+.tox-tree--directory__children--shrinking) .tox-chevron{transition:transform .5s ease-in-out}.tox .tox-tree .tox-tree--directory .tox-tree--directory__label:has(+.tox-tree--directory__children--growing) .tox-chevron,.tox .tox-tree .tox-tree--directory .tox-tree--directory__label:has(+.tox-tree--directory__children--open) .tox-chevron{transform:rotate(90deg)}.tox .tox-tree .tox-tree--leaf__label{font-weight:400}.tox .tox-tree .tox-tree--leaf__label .tox-mbtn{margin-left:auto}.tox .tox-tree .tox-tree--leaf__label .tox-mbtn svg{fill:transparent}.tox .tox-tree .tox-tree--leaf__label .tox-mbtn.tox-mbtn--active svg,.tox .tox-tree .tox-tree--leaf__label .tox-mbtn:focus svg{fill:#fff}.tox .tox-tree .tox-tree--leaf__label:hover .tox-mbtn svg{fill:#fff}.tox .tox-tree .tox-tree--leaf__label:hover:has(.tox-mbtn:hover){background-color:transparent;color:#fff}.tox .tox-tree .tox-tree--leaf__label:hover:has(.tox-mbtn:hover) .tox-chevron svg{fill:#fff}.tox .tox-tree .tox-tree--directory__children{overflow:hidden;padding-left:16px}.tox .tox-tree .tox-tree--directory__children.tox-tree--directory__children--growing,.tox .tox-tree .tox-tree--directory__children.tox-tree--directory__children--shrinking{transition:height .5s ease-in-out}.tox .tox-tree .tox-trbtn.tox-tree--leaf__label{display:flex;justify-content:space-between}.tox .tox-revisionhistory__pane{padding:0!important}.tox .tox-revisionhistory__container{display:flex;flex-direction:column;height:100%}.tox .tox-revisionhistory{background-color:#2b3b4e;border-radius:4px;border-top:1px solid #161f29;display:flex;flex:1;height:100%;margin-top:8px;overflow-x:auto;overflow-y:hidden;position:relative;width:100%}.tox .tox-revisionhistory--align-right{margin-left:auto}.tox .tox-revisionhistory__iframe{flex:1}.tox .tox-revisionhistory__sidebar{border-left:1px solid #161f29;height:100%;max-width:360px}.tox .tox-revisionhistory__sidebar .tox-revisionhistory__sidebar-title{border-bottom:1px solid #161f29;color:#fff;font-size:20px;font-weight:400;height:60px;min-width:192px;padding:16px}.tox .tox-revisionhistory__sidebar .tox-revisionhistory__revisions{flex-direction:column;max-height:calc(100% - 60px);min-width:192px;overflow-y:auto;padding:8px}.tox .tox-revisionhistory__sidebar .tox-revisionhistory__revisions:focus{height:100%;position:relative;z-index:1}.tox .tox-revisionhistory__sidebar .tox-revisionhistory__revisions:focus::after{border-radius:3px;bottom:0;box-shadow:0 0 0 2px #fff;content:'';left:0;position:absolute;right:0;top:0;border-radius:6px;bottom:1px;left:1px;right:1px;top:1px}@media (forced-colors:active){.tox .tox-revisionhistory__sidebar .tox-revisionhistory__revisions:focus::after{border:2px solid highlight}}.tox .tox-revisionhistory__sidebar .tox-revisionhistory__revisions .tox-revisionhistory__card{border:1px solid #161f29;border-radius:6px;color:#fff;cursor:pointer;font-size:14px;margin-bottom:8px;padding:8px;text-overflow:ellipsis;text-wrap:nowrap;width:100%}.tox .tox-revisionhistory__sidebar .tox-revisionhistory__revisions .tox-revisionhistory__card:hover{background-color:#2f4055;box-shadow:none;color:#fff}.tox .tox-revisionhistory__sidebar .tox-revisionhistory__revisions .tox-revisionhistory__card:focus{position:relative;z-index:1}.tox .tox-revisionhistory__sidebar .tox-revisionhistory__revisions .tox-revisionhistory__card:focus::after{border-radius:6px!important;border-radius:3px;bottom:0;box-shadow:0 0 0 2px #fff;content:'';left:0;position:absolute;right:0;top:0}@media (forced-colors:active){.tox .tox-revisionhistory__sidebar .tox-revisionhistory__revisions .tox-revisionhistory__card:focus::after{border:2px solid highlight}}.tox .tox-revisionhistory__sidebar .tox-revisionhistory__revisions .tox-revisionhistory__card.tox-revisionhistory__card--selected{background-color:#599fef;box-shadow:none;color:#fff}.tox .tox-revisionhistory__sidebar .tox-revisionhistory__revisions .tox-revisionhistory__norevision{color:rgba(255,255,255,.5);font-size:16px;line-height:24px;padding:5px 5.5px}.tox .tox-view-wrap,.tox .tox-view-wrap__slot-container{background-color:#222f3e;display:flex;flex:1;flex-direction:column;height:100%}.tox .tox-view{display:flex;flex:1 1 auto;flex-direction:column;overflow:hidden}.tox .tox-view__header{align-items:center;display:flex;font-size:16px;justify-content:space-between;padding:10px 10px 2px 10px;position:relative}.tox .tox-view__label{color:#fff;font-weight:700;line-height:24px;padding:4px 16px;text-align:center;white-space:nowrap}.tox .tox-view__label--normal{font-size:16px}.tox .tox-view__label--large{font-size:20px}.tox .tox-view--mobile.tox-view__header,.tox .tox-view--mobile.tox-view__toolbar{padding:8px}.tox .tox-view--scrolling{flex-wrap:nowrap;overflow-x:auto}.tox .tox-view__toolbar{display:flex;flex-direction:row;gap:8px;justify-content:space-between;overflow-x:auto;padding:10px 10px 2px 10px}.tox .tox-view__toolbar__group{display:flex;flex-direction:row;gap:12px}.tox .tox-view__header-end,.tox .tox-view__header-start{display:flex}.tox .tox-view__pane{height:100%;padding:8px;position:relative;width:100%}.tox .tox-view__pane_panel{border:1px solid #161f29;border-radius:6px}.tox:not([dir=rtl]) .tox-view__header .tox-view__header-end>*,.tox:not([dir=rtl]) .tox-view__header .tox-view__header-start>*{margin-left:8px}.tox[dir=rtl] .tox-view__header .tox-view__header-end>*,.tox[dir=rtl] .tox-view__header .tox-view__header-start>*{margin-right:8px}.tox .tox-well{border:1px solid #161f29;border-radius:6px;padding:8px;width:100%}.tox .tox-well>:first-child{margin-top:0}.tox .tox-well>:last-child{margin-bottom:0}.tox .tox-well>:only-child{margin:0}.tox .tox-custom-editor{border:1px solid #161f29;border-radius:6px;display:flex;flex:1;overflow:hidden;position:relative}.tox .tox-dialog-loading::before{background-color:rgba(0,0,0,.5);content:\\\"\\\";height:100%;position:absolute;width:100%;z-index:1000}.tox .tox-tab{cursor:pointer}.tox .tox-dialog__content-js{display:flex;flex:1}.tox .tox-dialog__body-content .tox-collection{display:flex;flex:1}.tox.tox-tinymce-aux .tox-toolbar__overflow{box-shadow:0 0 0 1px rgba(255,255,255,.15)}\")\n//# sourceMappingURL=skin.js.map\n"
  },
  {
    "path": "apps/web-antd/public/tinymce/skins/ui/oxide-dark/skin.shadowdom.js",
    "content": "tinymce.Resource.add('ui/dark/skin.shadowdom.css', \"body.tox-dialog__disable-scroll{overflow:hidden}.tox-fullscreen{border:0;height:100%;margin:0;overflow:hidden;overscroll-behavior:none;padding:0;touch-action:pinch-zoom;width:100%}.tox.tox-tinymce.tox-fullscreen .tox-statusbar__resize-handle{display:none}.tox-shadowhost.tox-fullscreen,.tox.tox-tinymce.tox-fullscreen{left:0;position:fixed;top:0;z-index:1200}.tox.tox-tinymce.tox-fullscreen{background-color:transparent}.tox-fullscreen .tox.tox-tinymce-aux,.tox-fullscreen~.tox.tox-tinymce-aux{z-index:1201}\")\n//# sourceMappingURL=skin.shadowdom.js.map\n"
  },
  {
    "path": "apps/web-antd/public/tinymce/skins/ui/tinymce-5/content.inline.js",
    "content": "tinymce.Resource.add('ui/tinymce-5/content.inline.css', \".mce-content-body .mce-item-anchor{background:transparent url(\\\"data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D'8'%20height%3D'12'%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%3E%3Cpath%20d%3D'M0%200L8%200%208%2012%204.09117821%209%200%2012z'%2F%3E%3C%2Fsvg%3E%0A\\\") no-repeat center}.mce-content-body .mce-item-anchor:empty{cursor:default;display:inline-block;height:12px!important;padding:0 2px;-webkit-user-modify:read-only;-moz-user-modify:read-only;-webkit-user-select:all;-moz-user-select:all;user-select:all;width:8px!important}.mce-content-body .mce-item-anchor:not(:empty){background-position-x:2px;display:inline-block;padding-left:12px}.mce-content-body .mce-item-anchor[data-mce-selected]{outline-offset:1px}.tox-comments-visible .tox-comment[contenteditable=false]:not([data-mce-selected]),.tox-comments-visible span.tox-comment img:not([data-mce-selected]),.tox-comments-visible span.tox-comment span.mce-preview-object:not([data-mce-selected]),.tox-comments-visible span.tox-comment>audio:not([data-mce-selected]),.tox-comments-visible span.tox-comment>video:not([data-mce-selected]){outline:3px solid #ffe89d}.tox-comments-visible .tox-comment[contenteditable=false][data-mce-annotation-active=true]:not([data-mce-selected]){outline:3px solid #fed635}.tox-comments-visible span.tox-comment[data-mce-annotation-active=true] img:not([data-mce-selected]),.tox-comments-visible span.tox-comment[data-mce-annotation-active=true] span.mce-preview-object:not([data-mce-selected]),.tox-comments-visible span.tox-comment[data-mce-annotation-active=true]>audio:not([data-mce-selected]),.tox-comments-visible span.tox-comment[data-mce-annotation-active=true]>video:not([data-mce-selected]){outline:3px solid #fed635}.tox-comments-visible span.tox-comment:not([data-mce-selected]){background-color:#ffe89d;outline:0}.tox-comments-visible span.tox-comment[data-mce-annotation-active=true]:not([data-mce-selected=inline-boundary]){background-color:#fed635}.tox-checklist>li:not(.tox-checklist--hidden){list-style:none;margin:.25em 0}.tox-checklist>li:not(.tox-checklist--hidden)::before{content:url(\\\"data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2216%22%20height%3D%2216%22%20viewBox%3D%220%200%2016%2016%22%3E%3Cg%20id%3D%22checklist-unchecked%22%20fill%3D%22none%22%20fill-rule%3D%22evenodd%22%3E%3Crect%20id%3D%22Rectangle%22%20width%3D%2215%22%20height%3D%2215%22%20x%3D%22.5%22%20y%3D%22.5%22%20fill-rule%3D%22nonzero%22%20stroke%3D%22%234C4C4C%22%20rx%3D%222%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E%0A\\\");cursor:pointer;height:1em;margin-left:-1.5em;margin-top:.125em;position:absolute;width:1em}.tox-checklist li:not(.tox-checklist--hidden).tox-checklist--checked::before{content:url(\\\"data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2216%22%20height%3D%2216%22%20viewBox%3D%220%200%2016%2016%22%3E%3Cg%20id%3D%22checklist-checked%22%20fill%3D%22none%22%20fill-rule%3D%22evenodd%22%3E%3Crect%20id%3D%22Rectangle%22%20width%3D%2216%22%20height%3D%2216%22%20fill%3D%22%234099FF%22%20fill-rule%3D%22nonzero%22%20rx%3D%222%22%2F%3E%3Cpath%20id%3D%22Path%22%20fill%3D%22%23FFF%22%20fill-rule%3D%22nonzero%22%20d%3D%22M11.5703186%2C3.14417309%20C11.8516238%2C2.73724603%2012.4164781%2C2.62829933%2012.83558%2C2.89774797%20C13.260121%2C3.17069355%2013.3759736%2C3.72932262%2013.0909105%2C4.14168582%20L7.7580587%2C11.8560195%20C7.43776896%2C12.3193404%206.76483983%2C12.3852142%206.35607322%2C11.9948725%20L3.02491697%2C8.8138662%20C2.66090143%2C8.46625845%202.65798871%2C7.89594698%203.01850234%2C7.54483354%20C3.373942%2C7.19866177%203.94940006%2C7.19592841%204.30829608%2C7.5386474%20L6.85276923%2C9.9684299%20L11.5703186%2C3.14417309%20Z%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E%0A\\\")}[dir=rtl] .tox-checklist>li:not(.tox-checklist--hidden)::before{margin-left:0;margin-right:-1.5em}code[class*=language-],pre[class*=language-]{color:#000;background:0 0;text-shadow:0 1px #fff;font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;tab-size:4;-webkit-hyphens:none;hyphens:none}code[class*=language-] ::-moz-selection,code[class*=language-]::-moz-selection,pre[class*=language-] ::-moz-selection,pre[class*=language-]::-moz-selection{text-shadow:none;background:#b3d4fc}code[class*=language-] ::selection,code[class*=language-]::selection,pre[class*=language-] ::selection,pre[class*=language-]::selection{text-shadow:none;background:#b3d4fc}@media print{code[class*=language-],pre[class*=language-]{text-shadow:none}}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto}:not(pre)>code[class*=language-],pre[class*=language-]{background:#f5f2f0}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#708090}.token.punctuation{color:#999}.token.namespace{opacity:.7}.token.boolean,.token.constant,.token.deleted,.token.number,.token.property,.token.symbol,.token.tag{color:#905}.token.attr-name,.token.builtin,.token.char,.token.inserted,.token.selector,.token.string{color:#690}.language-css .token.string,.style .token.string,.token.entity,.token.operator,.token.url{color:#9a6e3a;background:hsla(0,0%,100%,.5)}.token.atrule,.token.attr-value,.token.keyword{color:#07a}.token.class-name,.token.function{color:#dd4a68}.token.important,.token.regex,.token.variable{color:#e90}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}.mce-content-body{overflow-wrap:break-word;word-wrap:break-word}.mce-content-body .mce-visual-caret{background-color:#000;background-color:currentColor;position:absolute}.mce-content-body .mce-visual-caret-hidden{display:none}.mce-content-body [data-mce-caret]{left:-1000px;margin:0;padding:0;position:absolute;right:auto;top:0}.mce-content-body .mce-offscreen-selection{left:-2000000px;max-width:1000000px;position:absolute}.mce-content-body [contentEditable=false]{cursor:default}.mce-content-body [contentEditable=true]{cursor:text}.tox-cursor-format-painter{cursor:url(\\\"data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%20viewBox%3D%220%200%2024%2024%22%3E%0A%20%20%3Cg%20fill%3D%22none%22%20fill-rule%3D%22evenodd%22%3E%0A%20%20%20%20%3Cpath%20fill%3D%22%23000%22%20fill-rule%3D%22nonzero%22%20d%3D%22M15%2C6%20C15%2C5.45%2014.55%2C5%2014%2C5%20L6%2C5%20C5.45%2C5%205%2C5.45%205%2C6%20L5%2C10%20C5%2C10.55%205.45%2C11%206%2C11%20L14%2C11%20C14.55%2C11%2015%2C10.55%2015%2C10%20L15%2C9%20L16%2C9%20L16%2C12%20L9%2C12%20L9%2C19%20C9%2C19.55%209.45%2C20%2010%2C20%20L11%2C20%20C11.55%2C20%2012%2C19.55%2012%2C19%20L12%2C14%20L18%2C14%20L18%2C7%20L15%2C7%20L15%2C6%20Z%22%2F%3E%0A%20%20%20%20%3Cpath%20fill%3D%22%23000%22%20fill-rule%3D%22nonzero%22%20d%3D%22M1%2C1%20L8.25%2C1%20C8.66421356%2C1%209%2C1.33578644%209%2C1.75%20L9%2C1.75%20C9%2C2.16421356%208.66421356%2C2.5%208.25%2C2.5%20L2.5%2C2.5%20L2.5%2C8.25%20C2.5%2C8.66421356%202.16421356%2C9%201.75%2C9%20L1.75%2C9%20C1.33578644%2C9%201%2C8.66421356%201%2C8.25%20L1%2C1%20Z%22%2F%3E%0A%20%20%3C%2Fg%3E%0A%3C%2Fsvg%3E%0A\\\"),default}div.mce-footnotes hr{margin-inline-end:auto;margin-inline-start:0;width:25%}div.mce-footnotes li>a.mce-footnotes-backlink{text-decoration:none}@media print{sup.mce-footnote a{color:#000;text-decoration:none}div.mce-footnotes{break-inside:avoid;width:100%}div.mce-footnotes li>a.mce-footnotes-backlink{display:none}}tiny-math-block{display:flex;justify-content:center;margin:16px 0 16px 0}tiny-math-inline{display:inline-block}.mce-content-body figure.align-left{float:left}.mce-content-body figure.align-right{float:right}.mce-content-body figure.image.align-center{display:table;margin-left:auto;margin-right:auto}.mce-preview-object{border:1px solid gray;display:inline-block;line-height:0;margin:0 2px 0 2px;position:relative}.mce-preview-object .mce-shim{background:url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7);height:100%;left:0;position:absolute;top:0;width:100%}.mce-preview-object[data-mce-selected=\\\"2\\\"] .mce-shim{display:none}.mce-content-body .mce-mergetag{cursor:default!important;-webkit-user-select:none;-moz-user-select:none;user-select:none}.mce-content-body .mce-mergetag:hover{background-color:rgba(0,108,231,.1)}.mce-content-body .mce-mergetag-affix{background-color:rgba(0,108,231,.1);color:#006ce7}.mce-object{background:transparent url(\\\"data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%3E%3Cpath%20d%3D%22M4%203h16a1%201%200%200%201%201%201v16a1%201%200%200%201-1%201H4a1%201%200%200%201-1-1V4a1%201%200%200%201%201-1zm1%202v14h14V5H5zm4.79%202.565l5.64%204.028a.5.5%200%200%201%200%20.814l-5.64%204.028a.5.5%200%200%201-.79-.407V7.972a.5.5%200%200%201%20.79-.407z%22%2F%3E%3C%2Fsvg%3E%0A\\\") no-repeat center;border:1px dashed #aaa}.mce-pagebreak{border:1px dashed #aaa;cursor:default;display:block;height:5px;margin-top:15px;page-break-before:always;width:100%}@media print{.mce-pagebreak{border:0}}.tiny-pageembed .mce-shim{background:url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7);height:100%;left:0;position:absolute;top:0;width:100%}.tiny-pageembed[data-mce-selected=\\\"2\\\"] .mce-shim{display:none}.tiny-pageembed{display:inline-block;position:relative}.tiny-pageembed--16by9,.tiny-pageembed--1by1,.tiny-pageembed--21by9,.tiny-pageembed--4by3{display:block;overflow:hidden;padding:0;position:relative;width:100%}.tiny-pageembed--21by9{padding-top:42.857143%}.tiny-pageembed--16by9{padding-top:56.25%}.tiny-pageembed--4by3{padding-top:75%}.tiny-pageembed--1by1{padding-top:100%}.tiny-pageembed--16by9 iframe,.tiny-pageembed--1by1 iframe,.tiny-pageembed--21by9 iframe,.tiny-pageembed--4by3 iframe{border:0;height:100%;left:0;position:absolute;top:0;width:100%}.mce-content-body[data-mce-placeholder]{position:relative}.mce-content-body[data-mce-placeholder]:not(.mce-visualblocks)::before{color:rgba(34,47,62,.7);content:attr(data-mce-placeholder);position:absolute}@media (forced-colors:active){.mce-content-body[data-mce-placeholder]:not(.mce-visualblocks)::before{color:highlight;filter:brightness(30%);z-index:-1}}.mce-content-body:not([dir=rtl])[data-mce-placeholder]:not(.mce-visualblocks)::before{left:1px}.mce-content-body[dir=rtl][data-mce-placeholder]:not(.mce-visualblocks)::before{right:1px}.mce-content-body div.mce-resizehandle{background-color:#4099ff;border-color:#4099ff;border-style:solid;border-width:1px;box-sizing:border-box;height:10px;position:absolute;width:10px;z-index:1298}.mce-content-body div.mce-resizehandle:hover{background-color:#4099ff}.mce-content-body div.mce-resizehandle:nth-of-type(1){cursor:nwse-resize}.mce-content-body div.mce-resizehandle:nth-of-type(2){cursor:nesw-resize}.mce-content-body div.mce-resizehandle:nth-of-type(3){cursor:nwse-resize}.mce-content-body div.mce-resizehandle:nth-of-type(4){cursor:nesw-resize}.mce-content-body .mce-resize-backdrop{z-index:10000}.mce-content-body .mce-clonedresizable{cursor:default;opacity:.5;outline:1px dashed #000;position:absolute;z-index:10001}.mce-content-body .mce-clonedresizable.mce-resizetable-columns td,.mce-content-body .mce-clonedresizable.mce-resizetable-columns th{border:0}.mce-content-body .mce-resize-helper{background:#555;background:rgba(0,0,0,.75);border:1px;border-radius:3px;color:#fff;display:none;font-family:sans-serif;font-size:12px;line-height:14px;margin:5px 10px;padding:5px;position:absolute;white-space:nowrap;z-index:10002}.tox-rtc-user-selection{position:relative}.tox-rtc-user-cursor{bottom:0;cursor:default;position:absolute;top:0;width:2px}.tox-rtc-user-cursor::before{background-color:inherit;border-radius:50%;content:'';display:block;height:8px;position:absolute;right:-3px;top:-3px;width:8px}.tox-rtc-user-cursor:hover::after{background-color:inherit;border-radius:100px;box-sizing:border-box;color:#fff;content:attr(data-user);display:block;font-size:12px;font-weight:700;left:-5px;min-height:8px;min-width:8px;padding:0 12px;position:absolute;top:-11px;white-space:nowrap;z-index:1000}.tox-rtc-user-selection--1 .tox-rtc-user-cursor{background-color:#2dc26b}.tox-rtc-user-selection--2 .tox-rtc-user-cursor{background-color:#e03e2d}.tox-rtc-user-selection--3 .tox-rtc-user-cursor{background-color:#f1c40f}.tox-rtc-user-selection--4 .tox-rtc-user-cursor{background-color:#3598db}.tox-rtc-user-selection--5 .tox-rtc-user-cursor{background-color:#b96ad9}.tox-rtc-user-selection--6 .tox-rtc-user-cursor{background-color:#e67e23}.tox-rtc-user-selection--7 .tox-rtc-user-cursor{background-color:#aaa69d}.tox-rtc-user-selection--8 .tox-rtc-user-cursor{background-color:#f368e0}.tox-rtc-remote-image{background:#eaeaea url(\\\"data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%2236%22%20height%3D%2212%22%20viewBox%3D%220%200%2036%2012%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%3Ccircle%20cx%3D%226%22%20cy%3D%226%22%20r%3D%223%22%20fill%3D%22rgba(0%2C%200%2C%200%2C%20.2)%22%3E%0A%20%20%20%20%3Canimate%20attributeName%3D%22r%22%20values%3D%223%3B5%3B3%22%20calcMode%3D%22linear%22%20dur%3D%221s%22%20repeatCount%3D%22indefinite%22%20%2F%3E%0A%20%20%3C%2Fcircle%3E%0A%20%20%3Ccircle%20cx%3D%2218%22%20cy%3D%226%22%20r%3D%223%22%20fill%3D%22rgba(0%2C%200%2C%200%2C%20.2)%22%3E%0A%20%20%20%20%3Canimate%20attributeName%3D%22r%22%20values%3D%223%3B5%3B3%22%20calcMode%3D%22linear%22%20begin%3D%22.33s%22%20dur%3D%221s%22%20repeatCount%3D%22indefinite%22%20%2F%3E%0A%20%20%3C%2Fcircle%3E%0A%20%20%3Ccircle%20cx%3D%2230%22%20cy%3D%226%22%20r%3D%223%22%20fill%3D%22rgba(0%2C%200%2C%200%2C%20.2)%22%3E%0A%20%20%20%20%3Canimate%20attributeName%3D%22r%22%20values%3D%223%3B5%3B3%22%20calcMode%3D%22linear%22%20begin%3D%22.66s%22%20dur%3D%221s%22%20repeatCount%3D%22indefinite%22%20%2F%3E%0A%20%20%3C%2Fcircle%3E%0A%3C%2Fsvg%3E%0A\\\") no-repeat center center;border:1px solid #ccc;min-height:240px;min-width:320px}.mce-match-marker{background:#aaa;color:#fff}.mce-match-marker-selected{background:#39f;color:#fff}.mce-match-marker-selected::-moz-selection{background:#39f;color:#fff}.mce-match-marker-selected::selection{background:#39f;color:#fff}.mce-content-body audio[data-mce-selected],.mce-content-body details[data-mce-selected],.mce-content-body embed[data-mce-selected],.mce-content-body img[data-mce-selected],.mce-content-body object[data-mce-selected],.mce-content-body table[data-mce-selected],.mce-content-body video[data-mce-selected]{outline:3px solid #b4d7ff}.mce-content-body hr[data-mce-selected]{outline:3px solid #b4d7ff;outline-offset:1px}.mce-content-body [contentEditable=false] [contentEditable=true]:focus{outline:3px solid #b4d7ff}.mce-content-body [contentEditable=false] [contentEditable=true]:hover{outline:3px solid #b4d7ff}.mce-content-body [contentEditable=false][data-mce-selected]{cursor:not-allowed;outline:3px solid #b4d7ff}.mce-content-body.mce-content-readonly [contentEditable=true]:focus,.mce-content-body.mce-content-readonly [contentEditable=true]:hover{outline:0}.mce-content-body [data-mce-selected=inline-boundary]{background-color:#b4d7ff}.mce-content-body .mce-edit-focus{outline:3px solid #b4d7ff}.mce-content-body td[data-mce-selected],.mce-content-body th[data-mce-selected]{position:relative}.mce-content-body td[data-mce-selected]::-moz-selection,.mce-content-body th[data-mce-selected]::-moz-selection{background:0 0}.mce-content-body td[data-mce-selected]::selection,.mce-content-body th[data-mce-selected]::selection{background:0 0}.mce-content-body td[data-mce-selected] *,.mce-content-body th[data-mce-selected] *{outline:0;-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}.mce-content-body td[data-mce-selected]::after,.mce-content-body th[data-mce-selected]::after{background-color:rgba(180,215,255,.7);border:1px solid rgba(180,215,255,.7);bottom:-1px;content:'';left:-1px;mix-blend-mode:multiply;position:absolute;right:-1px;top:-1px}@media screen and (-ms-high-contrast:active),(-ms-high-contrast:none){.mce-content-body td[data-mce-selected]::after,.mce-content-body th[data-mce-selected]::after{border-color:rgba(0,84,180,.7)}}.mce-content-body img[data-mce-selected]::-moz-selection{background:0 0}.mce-content-body img[data-mce-selected]::selection{background:0 0}.ephox-snooker-resizer-bar{background-color:#b4d7ff;opacity:0;-webkit-user-select:none;-moz-user-select:none;user-select:none}.ephox-snooker-resizer-cols{cursor:col-resize}.ephox-snooker-resizer-rows{cursor:row-resize}.ephox-snooker-resizer-bar.ephox-snooker-resizer-bar-dragging{opacity:1}.mce-spellchecker-word{background-image:url(\\\"data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D'4'%20height%3D'4'%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%3E%3Cpath%20stroke%3D'%23ff0000'%20fill%3D'none'%20stroke-linecap%3D'round'%20stroke-opacity%3D'.75'%20d%3D'M0%203L2%201%204%203'%2F%3E%3C%2Fsvg%3E%0A\\\");background-position:0 calc(100% + 1px);background-repeat:repeat-x;background-size:auto 6px;cursor:default;height:2rem}.mce-spellchecker-grammar{background-image:url(\\\"data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D'4'%20height%3D'4'%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%3E%3Cpath%20stroke%3D'%2300A835'%20fill%3D'none'%20stroke-linecap%3D'round'%20d%3D'M0%203L2%201%204%203'%2F%3E%3C%2Fsvg%3E%0A\\\");background-position:0 calc(100% + 1px);background-repeat:repeat-x;background-size:auto 6px;cursor:default}.mce-toc{border:1px solid gray}.mce-toc h2{margin:4px}.mce-toc ul>li{list-style-type:none}[data-mce-block]{display:block}.mce-item-table:not([border]),.mce-item-table:not([border]) caption,.mce-item-table:not([border]) td,.mce-item-table:not([border]) th,.mce-item-table[border=\\\"0\\\"],.mce-item-table[border=\\\"0\\\"] caption,.mce-item-table[border=\\\"0\\\"] td,.mce-item-table[border=\\\"0\\\"] th,table[style*=\\\"border-width: 0px\\\"],table[style*=\\\"border-width: 0px\\\"] caption,table[style*=\\\"border-width: 0px\\\"] td,table[style*=\\\"border-width: 0px\\\"] th{border:1px dashed #bbb}.mce-visualblocks address,.mce-visualblocks article,.mce-visualblocks aside,.mce-visualblocks blockquote,.mce-visualblocks div:not([data-mce-bogus]),.mce-visualblocks dl,.mce-visualblocks figcaption,.mce-visualblocks figure,.mce-visualblocks h1,.mce-visualblocks h2,.mce-visualblocks h3,.mce-visualblocks h4,.mce-visualblocks h5,.mce-visualblocks h6,.mce-visualblocks hgroup,.mce-visualblocks ol,.mce-visualblocks p,.mce-visualblocks pre,.mce-visualblocks section,.mce-visualblocks ul{background-repeat:no-repeat;border:1px dashed #bbb;margin-left:3px;padding-top:10px}.mce-visualblocks p{background-image:url(data:image/gif;base64,R0lGODlhCQAJAJEAAAAAAP///7u7u////yH5BAEAAAMALAAAAAAJAAkAAAIQnG+CqCN/mlyvsRUpThG6AgA7)}.mce-visualblocks h1{background-image:url(data:image/gif;base64,R0lGODlhDQAKAIABALu7u////yH5BAEAAAEALAAAAAANAAoAAAIXjI8GybGu1JuxHoAfRNRW3TWXyF2YiRUAOw==)}.mce-visualblocks h2{background-image:url(data:image/gif;base64,R0lGODlhDgAKAIABALu7u////yH5BAEAAAEALAAAAAAOAAoAAAIajI8Hybbx4oOuqgTynJd6bGlWg3DkJzoaUAAAOw==)}.mce-visualblocks h3{background-image:url(data:image/gif;base64,R0lGODlhDgAKAIABALu7u////yH5BAEAAAEALAAAAAAOAAoAAAIZjI8Hybbx4oOuqgTynJf2Ln2NOHpQpmhAAQA7)}.mce-visualblocks h4{background-image:url(data:image/gif;base64,R0lGODlhDgAKAIABALu7u////yH5BAEAAAEALAAAAAAOAAoAAAIajI8HybbxInR0zqeAdhtJlXwV1oCll2HaWgAAOw==)}.mce-visualblocks h5{background-image:url(data:image/gif;base64,R0lGODlhDgAKAIABALu7u////yH5BAEAAAEALAAAAAAOAAoAAAIajI8HybbxIoiuwjane4iq5GlW05GgIkIZUAAAOw==)}.mce-visualblocks h6{background-image:url(data:image/gif;base64,R0lGODlhDgAKAIABALu7u////yH5BAEAAAEALAAAAAAOAAoAAAIajI8HybbxIoiuwjan04jep1iZ1XRlAo5bVgAAOw==)}.mce-visualblocks div:not([data-mce-bogus]){background-image:url(data:image/gif;base64,R0lGODlhEgAKAIABALu7u////yH5BAEAAAEALAAAAAASAAoAAAIfjI9poI0cgDywrhuxfbrzDEbQM2Ei5aRjmoySW4pAAQA7)}.mce-visualblocks section{background-image:url(data:image/gif;base64,R0lGODlhKAAKAIABALu7u////yH5BAEAAAEALAAAAAAoAAoAAAI5jI+pywcNY3sBWHdNrplytD2ellDeSVbp+GmWqaDqDMepc8t17Y4vBsK5hDyJMcI6KkuYU+jpjLoKADs=)}.mce-visualblocks article{background-image:url(data:image/gif;base64,R0lGODlhKgAKAIABALu7u////yH5BAEAAAEALAAAAAAqAAoAAAI6jI+pywkNY3wG0GBvrsd2tXGYSGnfiF7ikpXemTpOiJScasYoDJJrjsG9gkCJ0ag6KhmaIe3pjDYBBQA7)}.mce-visualblocks blockquote{background-image:url(data:image/gif;base64,R0lGODlhPgAKAIABALu7u////yH5BAEAAAEALAAAAAA+AAoAAAJPjI+py+0Knpz0xQDyuUhvfoGgIX5iSKZYgq5uNL5q69asZ8s5rrf0yZmpNkJZzFesBTu8TOlDVAabUyatguVhWduud3EyiUk45xhTTgMBBQA7)}.mce-visualblocks address{background-image:url(data:image/gif;base64,R0lGODlhLQAKAIABALu7u////yH5BAEAAAEALAAAAAAtAAoAAAI/jI+pywwNozSP1gDyyZcjb3UaRpXkWaXmZW4OqKLhBmLs+K263DkJK7OJeifh7FicKD9A1/IpGdKkyFpNmCkAADs=)}.mce-visualblocks pre{background-image:url(data:image/gif;base64,R0lGODlhFQAKAIABALu7uwAAACH5BAEAAAEALAAAAAAVAAoAAAIjjI+ZoN0cgDwSmnpz1NCueYERhnibZVKLNnbOq8IvKpJtVQAAOw==)}.mce-visualblocks figure{background-image:url(data:image/gif;base64,R0lGODlhJAAKAIAAALu7u////yH5BAEAAAEALAAAAAAkAAoAAAI0jI+py+2fwAHUSFvD3RlvG4HIp4nX5JFSpnZUJ6LlrM52OE7uSWosBHScgkSZj7dDKnWAAgA7)}.mce-visualblocks figcaption{border:1px dashed #bbb}.mce-visualblocks hgroup{background-image:url(data:image/gif;base64,R0lGODlhJwAKAIABALu7uwAAACH5BAEAAAEALAAAAAAnAAoAAAI3jI+pywYNI3uB0gpsRtt5fFnfNZaVSYJil4Wo03Hv6Z62uOCgiXH1kZIIJ8NiIxRrAZNMZAtQAAA7)}.mce-visualblocks aside{background-image:url(data:image/gif;base64,R0lGODlhHgAKAIABAKqqqv///yH5BAEAAAEALAAAAAAeAAoAAAItjI+pG8APjZOTzgtqy7I3f1yehmQcFY4WKZbqByutmW4aHUd6vfcVbgudgpYCADs=)}.mce-visualblocks ul{background-image:url(data:image/gif;base64,R0lGODlhDQAKAIAAALu7u////yH5BAEAAAEALAAAAAANAAoAAAIXjI8GybGuYnqUVSjvw26DzzXiqIDlVwAAOw==)}.mce-visualblocks ol{background-image:url(data:image/gif;base64,R0lGODlhDQAKAIABALu7u////yH5BAEAAAEALAAAAAANAAoAAAIXjI8GybH6HHt0qourxC6CvzXieHyeWQAAOw==)}.mce-visualblocks dl{background-image:url(data:image/gif;base64,R0lGODlhDQAKAIABALu7u////yH5BAEAAAEALAAAAAANAAoAAAIXjI8GybEOnmOvUoWznTqeuEjNSCqeGRUAOw==)}.mce-visualblocks:not([dir=rtl]) address,.mce-visualblocks:not([dir=rtl]) article,.mce-visualblocks:not([dir=rtl]) aside,.mce-visualblocks:not([dir=rtl]) blockquote,.mce-visualblocks:not([dir=rtl]) div:not([data-mce-bogus]),.mce-visualblocks:not([dir=rtl]) dl,.mce-visualblocks:not([dir=rtl]) figcaption,.mce-visualblocks:not([dir=rtl]) figure,.mce-visualblocks:not([dir=rtl]) h1,.mce-visualblocks:not([dir=rtl]) h2,.mce-visualblocks:not([dir=rtl]) h3,.mce-visualblocks:not([dir=rtl]) h4,.mce-visualblocks:not([dir=rtl]) h5,.mce-visualblocks:not([dir=rtl]) h6,.mce-visualblocks:not([dir=rtl]) hgroup,.mce-visualblocks:not([dir=rtl]) ol,.mce-visualblocks:not([dir=rtl]) p,.mce-visualblocks:not([dir=rtl]) pre,.mce-visualblocks:not([dir=rtl]) section,.mce-visualblocks:not([dir=rtl]) ul{margin-left:3px}.mce-visualblocks[dir=rtl] address,.mce-visualblocks[dir=rtl] article,.mce-visualblocks[dir=rtl] aside,.mce-visualblocks[dir=rtl] blockquote,.mce-visualblocks[dir=rtl] div:not([data-mce-bogus]),.mce-visualblocks[dir=rtl] dl,.mce-visualblocks[dir=rtl] figcaption,.mce-visualblocks[dir=rtl] figure,.mce-visualblocks[dir=rtl] h1,.mce-visualblocks[dir=rtl] h2,.mce-visualblocks[dir=rtl] h3,.mce-visualblocks[dir=rtl] h4,.mce-visualblocks[dir=rtl] h5,.mce-visualblocks[dir=rtl] h6,.mce-visualblocks[dir=rtl] hgroup,.mce-visualblocks[dir=rtl] ol,.mce-visualblocks[dir=rtl] p,.mce-visualblocks[dir=rtl] pre,.mce-visualblocks[dir=rtl] section,.mce-visualblocks[dir=rtl] ul{background-position-x:right;margin-right:3px}.mce-nbsp,.mce-shy{background:#aaa}.mce-shy::after{content:'-'}\")\n//# sourceMappingURL=content.inline.js.map\n"
  },
  {
    "path": "apps/web-antd/public/tinymce/skins/ui/tinymce-5/content.js",
    "content": "tinymce.Resource.add('ui/tinymce-5/content.css', \".mce-content-body .mce-item-anchor{background:transparent url(\\\"data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D'8'%20height%3D'12'%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%3E%3Cpath%20d%3D'M0%200L8%200%208%2012%204.09117821%209%200%2012z'%2F%3E%3C%2Fsvg%3E%0A\\\") no-repeat center}.mce-content-body .mce-item-anchor:empty{cursor:default;display:inline-block;height:12px!important;padding:0 2px;-webkit-user-modify:read-only;-moz-user-modify:read-only;-webkit-user-select:all;-moz-user-select:all;user-select:all;width:8px!important}.mce-content-body .mce-item-anchor:not(:empty){background-position-x:2px;display:inline-block;padding-left:12px}.mce-content-body .mce-item-anchor[data-mce-selected]{outline-offset:1px}.tox-comments-visible .tox-comment[contenteditable=false]:not([data-mce-selected]),.tox-comments-visible span.tox-comment img:not([data-mce-selected]),.tox-comments-visible span.tox-comment span.mce-preview-object:not([data-mce-selected]),.tox-comments-visible span.tox-comment>audio:not([data-mce-selected]),.tox-comments-visible span.tox-comment>video:not([data-mce-selected]){outline:3px solid #ffe89d}.tox-comments-visible .tox-comment[contenteditable=false][data-mce-annotation-active=true]:not([data-mce-selected]){outline:3px solid #fed635}.tox-comments-visible span.tox-comment[data-mce-annotation-active=true] img:not([data-mce-selected]),.tox-comments-visible span.tox-comment[data-mce-annotation-active=true] span.mce-preview-object:not([data-mce-selected]),.tox-comments-visible span.tox-comment[data-mce-annotation-active=true]>audio:not([data-mce-selected]),.tox-comments-visible span.tox-comment[data-mce-annotation-active=true]>video:not([data-mce-selected]){outline:3px solid #fed635}.tox-comments-visible span.tox-comment:not([data-mce-selected]){background-color:#ffe89d;outline:0}.tox-comments-visible span.tox-comment[data-mce-annotation-active=true]:not([data-mce-selected=inline-boundary]){background-color:#fed635}.tox-checklist>li:not(.tox-checklist--hidden){list-style:none;margin:.25em 0}.tox-checklist>li:not(.tox-checklist--hidden)::before{content:url(\\\"data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2216%22%20height%3D%2216%22%20viewBox%3D%220%200%2016%2016%22%3E%3Cg%20id%3D%22checklist-unchecked%22%20fill%3D%22none%22%20fill-rule%3D%22evenodd%22%3E%3Crect%20id%3D%22Rectangle%22%20width%3D%2215%22%20height%3D%2215%22%20x%3D%22.5%22%20y%3D%22.5%22%20fill-rule%3D%22nonzero%22%20stroke%3D%22%234C4C4C%22%20rx%3D%222%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E%0A\\\");cursor:pointer;height:1em;margin-left:-1.5em;margin-top:.125em;position:absolute;width:1em}.tox-checklist li:not(.tox-checklist--hidden).tox-checklist--checked::before{content:url(\\\"data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2216%22%20height%3D%2216%22%20viewBox%3D%220%200%2016%2016%22%3E%3Cg%20id%3D%22checklist-checked%22%20fill%3D%22none%22%20fill-rule%3D%22evenodd%22%3E%3Crect%20id%3D%22Rectangle%22%20width%3D%2216%22%20height%3D%2216%22%20fill%3D%22%234099FF%22%20fill-rule%3D%22nonzero%22%20rx%3D%222%22%2F%3E%3Cpath%20id%3D%22Path%22%20fill%3D%22%23FFF%22%20fill-rule%3D%22nonzero%22%20d%3D%22M11.5703186%2C3.14417309%20C11.8516238%2C2.73724603%2012.4164781%2C2.62829933%2012.83558%2C2.89774797%20C13.260121%2C3.17069355%2013.3759736%2C3.72932262%2013.0909105%2C4.14168582%20L7.7580587%2C11.8560195%20C7.43776896%2C12.3193404%206.76483983%2C12.3852142%206.35607322%2C11.9948725%20L3.02491697%2C8.8138662%20C2.66090143%2C8.46625845%202.65798871%2C7.89594698%203.01850234%2C7.54483354%20C3.373942%2C7.19866177%203.94940006%2C7.19592841%204.30829608%2C7.5386474%20L6.85276923%2C9.9684299%20L11.5703186%2C3.14417309%20Z%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E%0A\\\")}[dir=rtl] .tox-checklist>li:not(.tox-checklist--hidden)::before{margin-left:0;margin-right:-1.5em}code[class*=language-],pre[class*=language-]{color:#000;background:0 0;text-shadow:0 1px #fff;font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;tab-size:4;-webkit-hyphens:none;hyphens:none}code[class*=language-] ::-moz-selection,code[class*=language-]::-moz-selection,pre[class*=language-] ::-moz-selection,pre[class*=language-]::-moz-selection{text-shadow:none;background:#b3d4fc}code[class*=language-] ::selection,code[class*=language-]::selection,pre[class*=language-] ::selection,pre[class*=language-]::selection{text-shadow:none;background:#b3d4fc}@media print{code[class*=language-],pre[class*=language-]{text-shadow:none}}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto}:not(pre)>code[class*=language-],pre[class*=language-]{background:#f5f2f0}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#708090}.token.punctuation{color:#999}.token.namespace{opacity:.7}.token.boolean,.token.constant,.token.deleted,.token.number,.token.property,.token.symbol,.token.tag{color:#905}.token.attr-name,.token.builtin,.token.char,.token.inserted,.token.selector,.token.string{color:#690}.language-css .token.string,.style .token.string,.token.entity,.token.operator,.token.url{color:#9a6e3a;background:hsla(0,0%,100%,.5)}.token.atrule,.token.attr-value,.token.keyword{color:#07a}.token.class-name,.token.function{color:#dd4a68}.token.important,.token.regex,.token.variable{color:#e90}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}.mce-content-body{overflow-wrap:break-word;word-wrap:break-word}.mce-content-body .mce-visual-caret{background-color:#000;background-color:currentColor;position:absolute}.mce-content-body .mce-visual-caret-hidden{display:none}.mce-content-body [data-mce-caret]{left:-1000px;margin:0;padding:0;position:absolute;right:auto;top:0}.mce-content-body .mce-offscreen-selection{left:-2000000px;max-width:1000000px;position:absolute}.mce-content-body [contentEditable=false]{cursor:default}.mce-content-body [contentEditable=true]{cursor:text}.tox-cursor-format-painter{cursor:url(\\\"data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%20viewBox%3D%220%200%2024%2024%22%3E%0A%20%20%3Cg%20fill%3D%22none%22%20fill-rule%3D%22evenodd%22%3E%0A%20%20%20%20%3Cpath%20fill%3D%22%23000%22%20fill-rule%3D%22nonzero%22%20d%3D%22M15%2C6%20C15%2C5.45%2014.55%2C5%2014%2C5%20L6%2C5%20C5.45%2C5%205%2C5.45%205%2C6%20L5%2C10%20C5%2C10.55%205.45%2C11%206%2C11%20L14%2C11%20C14.55%2C11%2015%2C10.55%2015%2C10%20L15%2C9%20L16%2C9%20L16%2C12%20L9%2C12%20L9%2C19%20C9%2C19.55%209.45%2C20%2010%2C20%20L11%2C20%20C11.55%2C20%2012%2C19.55%2012%2C19%20L12%2C14%20L18%2C14%20L18%2C7%20L15%2C7%20L15%2C6%20Z%22%2F%3E%0A%20%20%20%20%3Cpath%20fill%3D%22%23000%22%20fill-rule%3D%22nonzero%22%20d%3D%22M1%2C1%20L8.25%2C1%20C8.66421356%2C1%209%2C1.33578644%209%2C1.75%20L9%2C1.75%20C9%2C2.16421356%208.66421356%2C2.5%208.25%2C2.5%20L2.5%2C2.5%20L2.5%2C8.25%20C2.5%2C8.66421356%202.16421356%2C9%201.75%2C9%20L1.75%2C9%20C1.33578644%2C9%201%2C8.66421356%201%2C8.25%20L1%2C1%20Z%22%2F%3E%0A%20%20%3C%2Fg%3E%0A%3C%2Fsvg%3E%0A\\\"),default}div.mce-footnotes hr{margin-inline-end:auto;margin-inline-start:0;width:25%}div.mce-footnotes li>a.mce-footnotes-backlink{text-decoration:none}@media print{sup.mce-footnote a{color:#000;text-decoration:none}div.mce-footnotes{break-inside:avoid;width:100%}div.mce-footnotes li>a.mce-footnotes-backlink{display:none}}tiny-math-block{display:flex;justify-content:center;margin:16px 0 16px 0}tiny-math-inline{display:inline-block}.mce-content-body figure.align-left{float:left}.mce-content-body figure.align-right{float:right}.mce-content-body figure.image.align-center{display:table;margin-left:auto;margin-right:auto}.mce-preview-object{border:1px solid gray;display:inline-block;line-height:0;margin:0 2px 0 2px;position:relative}.mce-preview-object .mce-shim{background:url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7);height:100%;left:0;position:absolute;top:0;width:100%}.mce-preview-object[data-mce-selected=\\\"2\\\"] .mce-shim{display:none}.mce-content-body .mce-mergetag{cursor:default!important;-webkit-user-select:none;-moz-user-select:none;user-select:none}.mce-content-body .mce-mergetag:hover{background-color:rgba(0,108,231,.1)}.mce-content-body .mce-mergetag-affix{background-color:rgba(0,108,231,.1);color:#006ce7}.mce-object{background:transparent url(\\\"data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%3E%3Cpath%20d%3D%22M4%203h16a1%201%200%200%201%201%201v16a1%201%200%200%201-1%201H4a1%201%200%200%201-1-1V4a1%201%200%200%201%201-1zm1%202v14h14V5H5zm4.79%202.565l5.64%204.028a.5.5%200%200%201%200%20.814l-5.64%204.028a.5.5%200%200%201-.79-.407V7.972a.5.5%200%200%201%20.79-.407z%22%2F%3E%3C%2Fsvg%3E%0A\\\") no-repeat center;border:1px dashed #aaa}.mce-pagebreak{border:1px dashed #aaa;cursor:default;display:block;height:5px;margin-top:15px;page-break-before:always;width:100%}@media print{.mce-pagebreak{border:0}}.tiny-pageembed .mce-shim{background:url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7);height:100%;left:0;position:absolute;top:0;width:100%}.tiny-pageembed[data-mce-selected=\\\"2\\\"] .mce-shim{display:none}.tiny-pageembed{display:inline-block;position:relative}.tiny-pageembed--16by9,.tiny-pageembed--1by1,.tiny-pageembed--21by9,.tiny-pageembed--4by3{display:block;overflow:hidden;padding:0;position:relative;width:100%}.tiny-pageembed--21by9{padding-top:42.857143%}.tiny-pageembed--16by9{padding-top:56.25%}.tiny-pageembed--4by3{padding-top:75%}.tiny-pageembed--1by1{padding-top:100%}.tiny-pageembed--16by9 iframe,.tiny-pageembed--1by1 iframe,.tiny-pageembed--21by9 iframe,.tiny-pageembed--4by3 iframe{border:0;height:100%;left:0;position:absolute;top:0;width:100%}.mce-content-body[data-mce-placeholder]{position:relative}.mce-content-body[data-mce-placeholder]:not(.mce-visualblocks)::before{color:rgba(34,47,62,.7);content:attr(data-mce-placeholder);position:absolute}@media (forced-colors:active){.mce-content-body[data-mce-placeholder]:not(.mce-visualblocks)::before{color:highlight;filter:brightness(30%);z-index:-1}}.mce-content-body:not([dir=rtl])[data-mce-placeholder]:not(.mce-visualblocks)::before{left:1px}.mce-content-body[dir=rtl][data-mce-placeholder]:not(.mce-visualblocks)::before{right:1px}.mce-content-body div.mce-resizehandle{background-color:#4099ff;border-color:#4099ff;border-style:solid;border-width:1px;box-sizing:border-box;height:10px;position:absolute;width:10px;z-index:1298}.mce-content-body div.mce-resizehandle:hover{background-color:#4099ff}.mce-content-body div.mce-resizehandle:nth-of-type(1){cursor:nwse-resize}.mce-content-body div.mce-resizehandle:nth-of-type(2){cursor:nesw-resize}.mce-content-body div.mce-resizehandle:nth-of-type(3){cursor:nwse-resize}.mce-content-body div.mce-resizehandle:nth-of-type(4){cursor:nesw-resize}.mce-content-body .mce-resize-backdrop{z-index:10000}.mce-content-body .mce-clonedresizable{cursor:default;opacity:.5;outline:1px dashed #000;position:absolute;z-index:10001}.mce-content-body .mce-clonedresizable.mce-resizetable-columns td,.mce-content-body .mce-clonedresizable.mce-resizetable-columns th{border:0}.mce-content-body .mce-resize-helper{background:#555;background:rgba(0,0,0,.75);border:1px;border-radius:3px;color:#fff;display:none;font-family:sans-serif;font-size:12px;line-height:14px;margin:5px 10px;padding:5px;position:absolute;white-space:nowrap;z-index:10002}.tox-rtc-user-selection{position:relative}.tox-rtc-user-cursor{bottom:0;cursor:default;position:absolute;top:0;width:2px}.tox-rtc-user-cursor::before{background-color:inherit;border-radius:50%;content:'';display:block;height:8px;position:absolute;right:-3px;top:-3px;width:8px}.tox-rtc-user-cursor:hover::after{background-color:inherit;border-radius:100px;box-sizing:border-box;color:#fff;content:attr(data-user);display:block;font-size:12px;font-weight:700;left:-5px;min-height:8px;min-width:8px;padding:0 12px;position:absolute;top:-11px;white-space:nowrap;z-index:1000}.tox-rtc-user-selection--1 .tox-rtc-user-cursor{background-color:#2dc26b}.tox-rtc-user-selection--2 .tox-rtc-user-cursor{background-color:#e03e2d}.tox-rtc-user-selection--3 .tox-rtc-user-cursor{background-color:#f1c40f}.tox-rtc-user-selection--4 .tox-rtc-user-cursor{background-color:#3598db}.tox-rtc-user-selection--5 .tox-rtc-user-cursor{background-color:#b96ad9}.tox-rtc-user-selection--6 .tox-rtc-user-cursor{background-color:#e67e23}.tox-rtc-user-selection--7 .tox-rtc-user-cursor{background-color:#aaa69d}.tox-rtc-user-selection--8 .tox-rtc-user-cursor{background-color:#f368e0}.tox-rtc-remote-image{background:#eaeaea url(\\\"data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%2236%22%20height%3D%2212%22%20viewBox%3D%220%200%2036%2012%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%3Ccircle%20cx%3D%226%22%20cy%3D%226%22%20r%3D%223%22%20fill%3D%22rgba(0%2C%200%2C%200%2C%20.2)%22%3E%0A%20%20%20%20%3Canimate%20attributeName%3D%22r%22%20values%3D%223%3B5%3B3%22%20calcMode%3D%22linear%22%20dur%3D%221s%22%20repeatCount%3D%22indefinite%22%20%2F%3E%0A%20%20%3C%2Fcircle%3E%0A%20%20%3Ccircle%20cx%3D%2218%22%20cy%3D%226%22%20r%3D%223%22%20fill%3D%22rgba(0%2C%200%2C%200%2C%20.2)%22%3E%0A%20%20%20%20%3Canimate%20attributeName%3D%22r%22%20values%3D%223%3B5%3B3%22%20calcMode%3D%22linear%22%20begin%3D%22.33s%22%20dur%3D%221s%22%20repeatCount%3D%22indefinite%22%20%2F%3E%0A%20%20%3C%2Fcircle%3E%0A%20%20%3Ccircle%20cx%3D%2230%22%20cy%3D%226%22%20r%3D%223%22%20fill%3D%22rgba(0%2C%200%2C%200%2C%20.2)%22%3E%0A%20%20%20%20%3Canimate%20attributeName%3D%22r%22%20values%3D%223%3B5%3B3%22%20calcMode%3D%22linear%22%20begin%3D%22.66s%22%20dur%3D%221s%22%20repeatCount%3D%22indefinite%22%20%2F%3E%0A%20%20%3C%2Fcircle%3E%0A%3C%2Fsvg%3E%0A\\\") no-repeat center center;border:1px solid #ccc;min-height:240px;min-width:320px}.mce-match-marker{background:#aaa;color:#fff}.mce-match-marker-selected{background:#39f;color:#fff}.mce-match-marker-selected::-moz-selection{background:#39f;color:#fff}.mce-match-marker-selected::selection{background:#39f;color:#fff}.mce-content-body audio[data-mce-selected],.mce-content-body details[data-mce-selected],.mce-content-body embed[data-mce-selected],.mce-content-body img[data-mce-selected],.mce-content-body object[data-mce-selected],.mce-content-body table[data-mce-selected],.mce-content-body video[data-mce-selected]{outline:3px solid #b4d7ff}.mce-content-body hr[data-mce-selected]{outline:3px solid #b4d7ff;outline-offset:1px}.mce-content-body [contentEditable=false] [contentEditable=true]:focus{outline:3px solid #b4d7ff}.mce-content-body [contentEditable=false] [contentEditable=true]:hover{outline:3px solid #b4d7ff}.mce-content-body [contentEditable=false][data-mce-selected]{cursor:not-allowed;outline:3px solid #b4d7ff}.mce-content-body.mce-content-readonly [contentEditable=true]:focus,.mce-content-body.mce-content-readonly [contentEditable=true]:hover{outline:0}.mce-content-body [data-mce-selected=inline-boundary]{background-color:#b4d7ff}.mce-content-body .mce-edit-focus{outline:3px solid #b4d7ff}.mce-content-body td[data-mce-selected],.mce-content-body th[data-mce-selected]{position:relative}.mce-content-body td[data-mce-selected]::-moz-selection,.mce-content-body th[data-mce-selected]::-moz-selection{background:0 0}.mce-content-body td[data-mce-selected]::selection,.mce-content-body th[data-mce-selected]::selection{background:0 0}.mce-content-body td[data-mce-selected] *,.mce-content-body th[data-mce-selected] *{outline:0;-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}.mce-content-body td[data-mce-selected]::after,.mce-content-body th[data-mce-selected]::after{background-color:rgba(180,215,255,.7);border:1px solid rgba(180,215,255,.7);bottom:-1px;content:'';left:-1px;mix-blend-mode:multiply;position:absolute;right:-1px;top:-1px}@media screen and (-ms-high-contrast:active),(-ms-high-contrast:none){.mce-content-body td[data-mce-selected]::after,.mce-content-body th[data-mce-selected]::after{border-color:rgba(0,84,180,.7)}}.mce-content-body img[data-mce-selected]::-moz-selection{background:0 0}.mce-content-body img[data-mce-selected]::selection{background:0 0}.ephox-snooker-resizer-bar{background-color:#b4d7ff;opacity:0;-webkit-user-select:none;-moz-user-select:none;user-select:none}.ephox-snooker-resizer-cols{cursor:col-resize}.ephox-snooker-resizer-rows{cursor:row-resize}.ephox-snooker-resizer-bar.ephox-snooker-resizer-bar-dragging{opacity:1}.mce-spellchecker-word{background-image:url(\\\"data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D'4'%20height%3D'4'%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%3E%3Cpath%20stroke%3D'%23ff0000'%20fill%3D'none'%20stroke-linecap%3D'round'%20stroke-opacity%3D'.75'%20d%3D'M0%203L2%201%204%203'%2F%3E%3C%2Fsvg%3E%0A\\\");background-position:0 calc(100% + 1px);background-repeat:repeat-x;background-size:auto 6px;cursor:default;height:2rem}.mce-spellchecker-grammar{background-image:url(\\\"data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D'4'%20height%3D'4'%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%3E%3Cpath%20stroke%3D'%2300A835'%20fill%3D'none'%20stroke-linecap%3D'round'%20d%3D'M0%203L2%201%204%203'%2F%3E%3C%2Fsvg%3E%0A\\\");background-position:0 calc(100% + 1px);background-repeat:repeat-x;background-size:auto 6px;cursor:default}.mce-toc{border:1px solid gray}.mce-toc h2{margin:4px}.mce-toc ul>li{list-style-type:none}[data-mce-block]{display:block}.mce-item-table:not([border]),.mce-item-table:not([border]) caption,.mce-item-table:not([border]) td,.mce-item-table:not([border]) th,.mce-item-table[border=\\\"0\\\"],.mce-item-table[border=\\\"0\\\"] caption,.mce-item-table[border=\\\"0\\\"] td,.mce-item-table[border=\\\"0\\\"] th,table[style*=\\\"border-width: 0px\\\"],table[style*=\\\"border-width: 0px\\\"] caption,table[style*=\\\"border-width: 0px\\\"] td,table[style*=\\\"border-width: 0px\\\"] th{border:1px dashed #bbb}.mce-visualblocks address,.mce-visualblocks article,.mce-visualblocks aside,.mce-visualblocks blockquote,.mce-visualblocks div:not([data-mce-bogus]),.mce-visualblocks dl,.mce-visualblocks figcaption,.mce-visualblocks figure,.mce-visualblocks h1,.mce-visualblocks h2,.mce-visualblocks h3,.mce-visualblocks h4,.mce-visualblocks h5,.mce-visualblocks h6,.mce-visualblocks hgroup,.mce-visualblocks ol,.mce-visualblocks p,.mce-visualblocks pre,.mce-visualblocks section,.mce-visualblocks ul{background-repeat:no-repeat;border:1px dashed #bbb;margin-left:3px;padding-top:10px}.mce-visualblocks p{background-image:url(data:image/gif;base64,R0lGODlhCQAJAJEAAAAAAP///7u7u////yH5BAEAAAMALAAAAAAJAAkAAAIQnG+CqCN/mlyvsRUpThG6AgA7)}.mce-visualblocks h1{background-image:url(data:image/gif;base64,R0lGODlhDQAKAIABALu7u////yH5BAEAAAEALAAAAAANAAoAAAIXjI8GybGu1JuxHoAfRNRW3TWXyF2YiRUAOw==)}.mce-visualblocks h2{background-image:url(data:image/gif;base64,R0lGODlhDgAKAIABALu7u////yH5BAEAAAEALAAAAAAOAAoAAAIajI8Hybbx4oOuqgTynJd6bGlWg3DkJzoaUAAAOw==)}.mce-visualblocks h3{background-image:url(data:image/gif;base64,R0lGODlhDgAKAIABALu7u////yH5BAEAAAEALAAAAAAOAAoAAAIZjI8Hybbx4oOuqgTynJf2Ln2NOHpQpmhAAQA7)}.mce-visualblocks h4{background-image:url(data:image/gif;base64,R0lGODlhDgAKAIABALu7u////yH5BAEAAAEALAAAAAAOAAoAAAIajI8HybbxInR0zqeAdhtJlXwV1oCll2HaWgAAOw==)}.mce-visualblocks h5{background-image:url(data:image/gif;base64,R0lGODlhDgAKAIABALu7u////yH5BAEAAAEALAAAAAAOAAoAAAIajI8HybbxIoiuwjane4iq5GlW05GgIkIZUAAAOw==)}.mce-visualblocks h6{background-image:url(data:image/gif;base64,R0lGODlhDgAKAIABALu7u////yH5BAEAAAEALAAAAAAOAAoAAAIajI8HybbxIoiuwjan04jep1iZ1XRlAo5bVgAAOw==)}.mce-visualblocks div:not([data-mce-bogus]){background-image:url(data:image/gif;base64,R0lGODlhEgAKAIABALu7u////yH5BAEAAAEALAAAAAASAAoAAAIfjI9poI0cgDywrhuxfbrzDEbQM2Ei5aRjmoySW4pAAQA7)}.mce-visualblocks section{background-image:url(data:image/gif;base64,R0lGODlhKAAKAIABALu7u////yH5BAEAAAEALAAAAAAoAAoAAAI5jI+pywcNY3sBWHdNrplytD2ellDeSVbp+GmWqaDqDMepc8t17Y4vBsK5hDyJMcI6KkuYU+jpjLoKADs=)}.mce-visualblocks article{background-image:url(data:image/gif;base64,R0lGODlhKgAKAIABALu7u////yH5BAEAAAEALAAAAAAqAAoAAAI6jI+pywkNY3wG0GBvrsd2tXGYSGnfiF7ikpXemTpOiJScasYoDJJrjsG9gkCJ0ag6KhmaIe3pjDYBBQA7)}.mce-visualblocks blockquote{background-image:url(data:image/gif;base64,R0lGODlhPgAKAIABALu7u////yH5BAEAAAEALAAAAAA+AAoAAAJPjI+py+0Knpz0xQDyuUhvfoGgIX5iSKZYgq5uNL5q69asZ8s5rrf0yZmpNkJZzFesBTu8TOlDVAabUyatguVhWduud3EyiUk45xhTTgMBBQA7)}.mce-visualblocks address{background-image:url(data:image/gif;base64,R0lGODlhLQAKAIABALu7u////yH5BAEAAAEALAAAAAAtAAoAAAI/jI+pywwNozSP1gDyyZcjb3UaRpXkWaXmZW4OqKLhBmLs+K263DkJK7OJeifh7FicKD9A1/IpGdKkyFpNmCkAADs=)}.mce-visualblocks pre{background-image:url(data:image/gif;base64,R0lGODlhFQAKAIABALu7uwAAACH5BAEAAAEALAAAAAAVAAoAAAIjjI+ZoN0cgDwSmnpz1NCueYERhnibZVKLNnbOq8IvKpJtVQAAOw==)}.mce-visualblocks figure{background-image:url(data:image/gif;base64,R0lGODlhJAAKAIAAALu7u////yH5BAEAAAEALAAAAAAkAAoAAAI0jI+py+2fwAHUSFvD3RlvG4HIp4nX5JFSpnZUJ6LlrM52OE7uSWosBHScgkSZj7dDKnWAAgA7)}.mce-visualblocks figcaption{border:1px dashed #bbb}.mce-visualblocks hgroup{background-image:url(data:image/gif;base64,R0lGODlhJwAKAIABALu7uwAAACH5BAEAAAEALAAAAAAnAAoAAAI3jI+pywYNI3uB0gpsRtt5fFnfNZaVSYJil4Wo03Hv6Z62uOCgiXH1kZIIJ8NiIxRrAZNMZAtQAAA7)}.mce-visualblocks aside{background-image:url(data:image/gif;base64,R0lGODlhHgAKAIABAKqqqv///yH5BAEAAAEALAAAAAAeAAoAAAItjI+pG8APjZOTzgtqy7I3f1yehmQcFY4WKZbqByutmW4aHUd6vfcVbgudgpYCADs=)}.mce-visualblocks ul{background-image:url(data:image/gif;base64,R0lGODlhDQAKAIAAALu7u////yH5BAEAAAEALAAAAAANAAoAAAIXjI8GybGuYnqUVSjvw26DzzXiqIDlVwAAOw==)}.mce-visualblocks ol{background-image:url(data:image/gif;base64,R0lGODlhDQAKAIABALu7u////yH5BAEAAAEALAAAAAANAAoAAAIXjI8GybH6HHt0qourxC6CvzXieHyeWQAAOw==)}.mce-visualblocks dl{background-image:url(data:image/gif;base64,R0lGODlhDQAKAIABALu7u////yH5BAEAAAEALAAAAAANAAoAAAIXjI8GybEOnmOvUoWznTqeuEjNSCqeGRUAOw==)}.mce-visualblocks:not([dir=rtl]) address,.mce-visualblocks:not([dir=rtl]) article,.mce-visualblocks:not([dir=rtl]) aside,.mce-visualblocks:not([dir=rtl]) blockquote,.mce-visualblocks:not([dir=rtl]) div:not([data-mce-bogus]),.mce-visualblocks:not([dir=rtl]) dl,.mce-visualblocks:not([dir=rtl]) figcaption,.mce-visualblocks:not([dir=rtl]) figure,.mce-visualblocks:not([dir=rtl]) h1,.mce-visualblocks:not([dir=rtl]) h2,.mce-visualblocks:not([dir=rtl]) h3,.mce-visualblocks:not([dir=rtl]) h4,.mce-visualblocks:not([dir=rtl]) h5,.mce-visualblocks:not([dir=rtl]) h6,.mce-visualblocks:not([dir=rtl]) hgroup,.mce-visualblocks:not([dir=rtl]) ol,.mce-visualblocks:not([dir=rtl]) p,.mce-visualblocks:not([dir=rtl]) pre,.mce-visualblocks:not([dir=rtl]) section,.mce-visualblocks:not([dir=rtl]) ul{margin-left:3px}.mce-visualblocks[dir=rtl] address,.mce-visualblocks[dir=rtl] article,.mce-visualblocks[dir=rtl] aside,.mce-visualblocks[dir=rtl] blockquote,.mce-visualblocks[dir=rtl] div:not([data-mce-bogus]),.mce-visualblocks[dir=rtl] dl,.mce-visualblocks[dir=rtl] figcaption,.mce-visualblocks[dir=rtl] figure,.mce-visualblocks[dir=rtl] h1,.mce-visualblocks[dir=rtl] h2,.mce-visualblocks[dir=rtl] h3,.mce-visualblocks[dir=rtl] h4,.mce-visualblocks[dir=rtl] h5,.mce-visualblocks[dir=rtl] h6,.mce-visualblocks[dir=rtl] hgroup,.mce-visualblocks[dir=rtl] ol,.mce-visualblocks[dir=rtl] p,.mce-visualblocks[dir=rtl] pre,.mce-visualblocks[dir=rtl] section,.mce-visualblocks[dir=rtl] ul{background-position-x:right;margin-right:3px}.mce-nbsp,.mce-shy{background:#aaa}.mce-shy::after{content:'-'}body{font-family:sans-serif}table{border-collapse:collapse}\")\n//# sourceMappingURL=content.js.map\n"
  },
  {
    "path": "apps/web-antd/public/tinymce/skins/ui/tinymce-5/skin.js",
    "content": "tinymce.Resource.add('ui/tinymce-5/skin.css', \".tox{box-shadow:none;box-sizing:content-box;color:#222f3e;cursor:auto;font-family:-apple-system,BlinkMacSystemFont,\\\"Segoe UI\\\",Roboto,Oxygen-Sans,Ubuntu,Cantarell,\\\"Helvetica Neue\\\",sans-serif;font-size:16px;font-style:normal;font-weight:400;line-height:normal;-webkit-tap-highlight-color:transparent;text-decoration:none;text-shadow:none;text-transform:none;vertical-align:initial;white-space:normal}.tox :not(svg):not(rect){box-sizing:inherit;color:inherit;cursor:inherit;direction:inherit;font-family:inherit;font-size:inherit;font-style:inherit;font-weight:inherit;line-height:inherit;-webkit-tap-highlight-color:inherit;text-align:inherit;text-decoration:inherit;text-shadow:inherit;text-transform:inherit;vertical-align:inherit;white-space:inherit}.tox :not(svg):not(rect){background:0 0;border:0;box-shadow:none;float:none;height:auto;margin:0;max-width:none;outline:0;padding:0;position:static;width:auto}.tox:not([dir=rtl]){direction:ltr;text-align:left}.tox[dir=rtl]{direction:rtl;text-align:right}.tox-tinymce{border:1px solid #ccc;border-radius:0;box-shadow:none;box-sizing:border-box;display:flex;flex-direction:column;font-family:-apple-system,BlinkMacSystemFont,\\\"Segoe UI\\\",Roboto,Oxygen-Sans,Ubuntu,Cantarell,\\\"Helvetica Neue\\\",sans-serif;overflow:hidden;position:relative;visibility:inherit!important}.tox.tox-tinymce-inline{border:none;box-shadow:none;overflow:initial}.tox.tox-tinymce-inline .tox-editor-container{overflow:initial}.tox.tox-tinymce-inline .tox-editor-header{background-color:#fff;border:1px solid #ccc;border-radius:0;box-shadow:none;overflow:hidden}.tox-tinymce-aux{font-family:-apple-system,BlinkMacSystemFont,\\\"Segoe UI\\\",Roboto,Oxygen-Sans,Ubuntu,Cantarell,\\\"Helvetica Neue\\\",sans-serif;z-index:1300}.tox-tinymce :focus,.tox-tinymce-aux :focus{outline:0}button::-moz-focus-inner{border:0}.tox[dir=rtl] .tox-icon--flip svg{transform:rotateY(180deg)}.tox .accessibility-issue__header{align-items:center;display:flex;margin-bottom:4px}.tox .accessibility-issue__description{align-items:stretch;border-radius:3px;display:flex;justify-content:space-between}.tox .accessibility-issue__description>div{padding-bottom:4px}.tox .accessibility-issue__description>div>div{align-items:center;display:flex;margin-bottom:4px}.tox .accessibility-issue__description>div>div .tox-icon svg{display:block}.tox .accessibility-issue__repair{margin-top:16px}.tox .tox-dialog__body-content .accessibility-issue--info .accessibility-issue__description{background-color:rgba(30,113,170,.1);color:#222f3e}.tox .tox-dialog__body-content .accessibility-issue--info .tox-form__group h2{color:#207ab7}.tox .tox-dialog__body-content .accessibility-issue--info .tox-icon svg{fill:#207ab7}.tox .tox-dialog__body-content .accessibility-issue--info a.tox-button--naked.tox-button--icon{background-color:#207ab7;color:#fff}.tox .tox-dialog__body-content .accessibility-issue--info a.tox-button--naked.tox-button--icon:focus,.tox .tox-dialog__body-content .accessibility-issue--info a.tox-button--naked.tox-button--icon:hover{background-color:#1c6ca1}.tox .tox-dialog__body-content .accessibility-issue--info a.tox-button--naked.tox-button--icon:active{background-color:#185d8c}.tox .tox-dialog__body-content .accessibility-issue--warn .accessibility-issue__description{background-color:rgba(255,165,0,.08);color:#222f3e}.tox .tox-dialog__body-content .accessibility-issue--warn .tox-form__group h2{color:#8f5d00}.tox .tox-dialog__body-content .accessibility-issue--warn .tox-icon svg{fill:#8f5d00}.tox .tox-dialog__body-content .accessibility-issue--warn a.tox-button--naked.tox-button--icon{background-color:#ffe89d;color:#222f3e}.tox .tox-dialog__body-content .accessibility-issue--warn a.tox-button--naked.tox-button--icon:focus,.tox .tox-dialog__body-content .accessibility-issue--warn a.tox-button--naked.tox-button--icon:hover{background-color:#f2d574;color:#222f3e}.tox .tox-dialog__body-content .accessibility-issue--warn a.tox-button--naked.tox-button--icon:active{background-color:#e8c657;color:#222f3e}.tox .tox-dialog__body-content .accessibility-issue--error .accessibility-issue__description{background-color:rgba(204,0,0,.1);color:#222f3e}.tox .tox-dialog__body-content .accessibility-issue--error .tox-form__group h2{color:#c00}.tox .tox-dialog__body-content .accessibility-issue--error .tox-icon svg{fill:#c00}.tox .tox-dialog__body-content .accessibility-issue--error a.tox-button--naked.tox-button--icon{background-color:#f2bfbf;color:#222f3e}.tox .tox-dialog__body-content .accessibility-issue--error a.tox-button--naked.tox-button--icon:focus,.tox .tox-dialog__body-content .accessibility-issue--error a.tox-button--naked.tox-button--icon:hover{background-color:#e9a4a4;color:#222f3e}.tox .tox-dialog__body-content .accessibility-issue--error a.tox-button--naked.tox-button--icon:active{background-color:#ee9494;color:#222f3e}.tox .tox-dialog__body-content .accessibility-issue--success .accessibility-issue__description{background-color:rgba(120,171,70,.1);color:#222f3e}.tox .tox-dialog__body-content .accessibility-issue--success .accessibility-issue__description>:last-child{display:none}.tox .tox-dialog__body-content .accessibility-issue--success .tox-form__group h2{color:#527530}.tox .tox-dialog__body-content .accessibility-issue--success .tox-icon svg{fill:#527530}.tox .tox-dialog__body-content .accessibility-issue__header .tox-form__group h1,.tox .tox-dialog__body-content .tox-form__group .accessibility-issue__description h2{font-size:14px;margin-top:0}.tox:not([dir=rtl]) .tox-dialog__body-content .accessibility-issue__header .tox-button{margin-left:4px}.tox:not([dir=rtl]) .tox-dialog__body-content .accessibility-issue__header>:nth-last-child(2){margin-left:auto}.tox:not([dir=rtl]) .tox-dialog__body-content .accessibility-issue__description{padding:4px 4px 4px 8px}.tox[dir=rtl] .tox-dialog__body-content .accessibility-issue__header .tox-button{margin-right:4px}.tox[dir=rtl] .tox-dialog__body-content .accessibility-issue__header>:nth-last-child(2){margin-right:auto}.tox[dir=rtl] .tox-dialog__body-content .accessibility-issue__description{padding:4px 8px 4px 4px}.tox .mce-codemirror{background:#fff;bottom:0;font-size:13px;left:0;position:absolute;right:0;top:0;z-index:1}.tox .mce-codemirror.tox-inline-codemirror{margin:8px;position:absolute}.tox .tox-advtemplate .tox-form__grid{flex:1}.tox .tox-advtemplate .tox-form__grid>div:first-child{display:flex;flex-direction:column;width:30%}.tox .tox-advtemplate .tox-form__grid>div:first-child>div:nth-child(2){flex-basis:0;flex-grow:1;overflow:auto}@media only screen and (max-width:767px){body:not(.tox-force-desktop) .tox .tox-advtemplate .tox-form__grid>div:first-child{width:100%}}.tox .tox-advtemplate iframe{border-color:#ccc;border-radius:0;border-style:solid;border-width:1px;margin:0 10px}.tox .tox-anchorbar{display:flex;flex:0 0 auto}.tox .tox-bottom-anchorbar{display:flex;flex:0 0 auto}.tox .tox-bar{display:flex;flex:0 0 auto}.tox .tox-button{background-color:#207ab7;background-image:none;background-position:0 0;background-repeat:repeat;border-color:#207ab7;border-radius:3px;border-style:solid;border-width:1px;box-shadow:none;box-sizing:border-box;color:#fff;cursor:pointer;display:inline-block;font-family:-apple-system,BlinkMacSystemFont,\\\"Segoe UI\\\",Roboto,Oxygen-Sans,Ubuntu,Cantarell,\\\"Helvetica Neue\\\",sans-serif;font-size:14px;font-style:normal;font-weight:700;letter-spacing:normal;line-height:24px;margin:0;outline:0;padding:4px 16px;position:relative;text-align:center;text-decoration:none;text-transform:none;white-space:nowrap}.tox .tox-button::before{border-radius:3px;bottom:-1px;box-shadow:inset 0 0 0 1px #fff,0 0 0 2px #207ab7;content:'';left:-1px;opacity:0;pointer-events:none;position:absolute;right:-1px;top:-1px}.tox .tox-button[disabled]{background-color:#207ab7;background-image:none;border-color:#207ab7;box-shadow:none;color:rgba(255,255,255,.5);cursor:not-allowed}.tox .tox-button:focus:not(:disabled){background-color:#1c6ca1;background-image:none;border-color:#1c6ca1;box-shadow:none;color:#fff}.tox .tox-button:focus:not(:disabled)::before{opacity:1}.tox .tox-button:hover:not(:disabled){background-color:#1c6ca1;background-image:none;border-color:#1c6ca1;box-shadow:none;color:#fff}.tox .tox-button:active:not(:disabled){background-color:#185d8c;background-image:none;border-color:#185d8c;box-shadow:none;color:#fff}.tox .tox-button.tox-button--enabled{background-color:#185d8c;background-image:none;border-color:#185d8c;box-shadow:none;color:#fff}.tox .tox-button.tox-button--enabled[disabled]{background-color:#185d8c;background-image:none;border-color:#185d8c;box-shadow:none;color:rgba(255,255,255,.5);cursor:not-allowed}.tox .tox-button.tox-button--enabled:focus:not(:disabled){background-color:#154f76;background-image:none;border-color:#154f76;box-shadow:none;color:#fff}.tox .tox-button.tox-button--enabled:hover:not(:disabled){background-color:#154f76;background-image:none;border-color:#154f76;box-shadow:none;color:#fff}.tox .tox-button.tox-button--enabled:active:not(:disabled){background-color:#114060;background-image:none;border-color:#114060;box-shadow:none;color:#fff}.tox .tox-button--icon-and-text,.tox .tox-button.tox-button--icon-and-text,.tox .tox-button.tox-button--secondary.tox-button--icon-and-text{display:flex;padding:5px 4px}.tox .tox-button--icon-and-text .tox-icon svg,.tox .tox-button.tox-button--icon-and-text .tox-icon svg,.tox .tox-button.tox-button--secondary.tox-button--icon-and-text .tox-icon svg{display:block;fill:currentColor}.tox .tox-button--secondary{background-color:#f0f0f0;background-image:none;background-position:0 0;background-repeat:repeat;border-color:#f0f0f0;border-radius:3px;border-style:solid;border-width:1px;box-shadow:none;color:#222f3e;font-size:14px;font-style:normal;font-weight:700;letter-spacing:normal;outline:0;padding:4px 16px;text-decoration:none;text-transform:none}.tox .tox-button--secondary[disabled]{background-color:#f0f0f0;background-image:none;border-color:#f0f0f0;box-shadow:none;color:rgba(34,47,62,.5)}.tox .tox-button--secondary:focus:not(:disabled){background-color:#e3e3e3;background-image:none;border-color:#e3e3e3;box-shadow:none;color:#222f3e}.tox .tox-button--secondary:hover:not(:disabled){background-color:#e3e3e3;background-image:none;border-color:#e3e3e3;box-shadow:none;color:#222f3e}.tox .tox-button--secondary:active:not(:disabled){background-color:#d6d6d6;background-image:none;border-color:#d6d6d6;box-shadow:none;color:#222f3e}.tox .tox-button--secondary.tox-button--enabled{background-color:#b1ccdf;background-image:none;border-color:#b1ccdf;box-shadow:none;color:#222f3e}.tox .tox-button--secondary.tox-button--enabled[disabled]{background-color:#b1ccdf;background-image:none;border-color:#b1ccdf;box-shadow:none;color:rgba(34,47,62,.5)}.tox .tox-button--secondary.tox-button--enabled:focus:not(:disabled){background-color:#9fc1d7;background-image:none;border-color:#9fc1d7;box-shadow:none;color:#222f3e}.tox .tox-button--secondary.tox-button--enabled:hover:not(:disabled){background-color:#9fc1d7;background-image:none;border-color:#9fc1d7;box-shadow:none;color:#222f3e}.tox .tox-button--secondary.tox-button--enabled:active:not(:disabled){background-color:#8db5d0;background-image:none;border-color:#8db5d0;box-shadow:none;color:#222f3e}.tox .tox-button--icon,.tox .tox-button.tox-button--icon,.tox .tox-button.tox-button--secondary.tox-button--icon{padding:4px}.tox .tox-button--icon .tox-icon svg,.tox .tox-button.tox-button--icon .tox-icon svg,.tox .tox-button.tox-button--secondary.tox-button--icon .tox-icon svg{display:block;fill:currentColor}.tox .tox-button-link{background:0;border:none;box-sizing:border-box;cursor:pointer;display:inline-block;font-family:-apple-system,BlinkMacSystemFont,\\\"Segoe UI\\\",Roboto,Oxygen-Sans,Ubuntu,Cantarell,\\\"Helvetica Neue\\\",sans-serif;font-size:16px;font-weight:400;line-height:1.3;margin:0;padding:0;white-space:nowrap}.tox .tox-button-link--sm{font-size:14px}.tox .tox-button--naked{background-color:transparent;border-color:transparent;box-shadow:unset;color:#222f3e}.tox .tox-button--naked[disabled]{background-color:#f0f0f0;border-color:#f0f0f0;box-shadow:none;color:rgba(34,47,62,.5)}.tox .tox-button--naked:hover:not(:disabled){background-color:#e3e3e3;border-color:#e3e3e3;box-shadow:none;color:#222f3e}.tox .tox-button--naked:focus:not(:disabled){background-color:#e3e3e3;border-color:#e3e3e3;box-shadow:none;color:#222f3e}.tox .tox-button--naked:active:not(:disabled){background-color:#d6d6d6;border-color:#d6d6d6;box-shadow:none;color:#222f3e}.tox .tox-button--naked .tox-icon svg{fill:currentColor}.tox .tox-button--naked.tox-button--icon:hover:not(:disabled){color:#222f3e}.tox .tox-checkbox{align-items:center;border-radius:3px;cursor:pointer;display:flex;height:36px;min-width:36px}.tox .tox-checkbox__input{height:1px;overflow:hidden;position:absolute;top:auto;width:1px}.tox .tox-checkbox__icons{align-items:center;border-radius:3px;box-shadow:0 0 0 2px transparent;box-sizing:content-box;display:flex;height:24px;justify-content:center;padding:calc(4px - 1px);width:24px}.tox .tox-checkbox__icons .tox-checkbox-icon__unchecked svg{display:block;fill:rgba(34,47,62,.3)}@media (forced-colors:active){.tox .tox-checkbox__icons .tox-checkbox-icon__unchecked svg{fill:currentColor!important}}.tox .tox-checkbox__icons .tox-checkbox-icon__indeterminate svg{display:none;fill:#207ab7}.tox .tox-checkbox__icons .tox-checkbox-icon__checked svg{display:none;fill:#207ab7}.tox .tox-checkbox--disabled{color:rgba(34,47,62,.5);cursor:not-allowed}.tox .tox-checkbox--disabled .tox-checkbox__icons .tox-checkbox-icon__checked svg{fill:rgba(34,47,62,.5)}.tox .tox-checkbox--disabled .tox-checkbox__icons .tox-checkbox-icon__unchecked svg{fill:rgba(34,47,62,.5)}.tox .tox-checkbox--disabled .tox-checkbox__icons .tox-checkbox-icon__indeterminate svg{fill:rgba(34,47,62,.5)}.tox input.tox-checkbox__input:checked+.tox-checkbox__icons .tox-checkbox-icon__unchecked svg{display:none}.tox input.tox-checkbox__input:checked+.tox-checkbox__icons .tox-checkbox-icon__checked svg{display:block}.tox input.tox-checkbox__input:indeterminate+.tox-checkbox__icons .tox-checkbox-icon__unchecked svg{display:none}.tox input.tox-checkbox__input:indeterminate+.tox-checkbox__icons .tox-checkbox-icon__indeterminate svg{display:block}.tox input.tox-checkbox__input:focus+.tox-checkbox__icons{border-radius:3px;box-shadow:inset 0 0 0 1px #207ab7;padding:calc(4px - 1px)}.tox:not([dir=rtl]) .tox-checkbox__label{margin-left:4px}.tox:not([dir=rtl]) .tox-checkbox__input{left:-10000px}.tox:not([dir=rtl]) .tox-bar .tox-checkbox{margin-left:4px}.tox[dir=rtl] .tox-checkbox__label{margin-right:4px}.tox[dir=rtl] .tox-checkbox__input{right:-10000px}.tox[dir=rtl] .tox-bar .tox-checkbox{margin-right:4px}.tox .tox-collection--toolbar .tox-collection__group{display:flex;padding:0}.tox .tox-collection--grid .tox-collection__group{display:flex;flex-wrap:wrap;max-height:208px;overflow-x:hidden;overflow-y:auto;padding:0}.tox .tox-collection--list .tox-collection__group{border-bottom-width:0;border-color:#ccc;border-left-width:0;border-right-width:0;border-style:solid;border-top-width:1px;padding:4px 0}.tox .tox-collection--list .tox-collection__group:first-child{border-top-width:0}.tox .tox-collection__group-heading{background-color:#e6e6e6;color:rgba(34,47,62,.7);cursor:default;font-size:12px;font-style:normal;font-weight:400;margin-bottom:4px;margin-top:-4px;padding:4px 8px;text-transform:none;-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}.tox .tox-collection__item{align-items:center;border-radius:3px;color:#222f3e;display:flex;-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}.tox .tox-collection--list .tox-collection__item{padding:4px 8px}.tox .tox-collection--toolbar .tox-collection__item{border-radius:3px;padding:4px}.tox .tox-collection--grid .tox-collection__item{border-radius:3px;padding:4px}.tox .tox-collection--list .tox-collection__item--enabled{background-color:#fff;color:#222f3e}.tox .tox-collection--list .tox-collection__item--active{background-color:#dee0e2}.tox .tox-collection--toolbar .tox-collection__item--enabled,.tox .tox-collection--toolbar .tox-collection__item--enabled.tox-collection__item--active,.tox .tox-collection--toolbar .tox-collection__item--enabled.tox-collection__item--active:hover{background-color:#c8cbcf;color:#222f3e}@media (forced-colors:active){.tox .tox-collection--toolbar .tox-collection__item--enabled,.tox .tox-collection--toolbar .tox-collection__item--enabled.tox-collection__item--active,.tox .tox-collection--toolbar .tox-collection__item--enabled.tox-collection__item--active:hover{border-radius:3px;outline:solid 1px}}.tox .tox-collection--toolbar .tox-collection__item--active{background-color:#fff;position:relative}.tox .tox-collection--toolbar .tox-collection__item--active:hover{background-color:#dee0e2;color:#222f3e}.tox .tox-collection--toolbar .tox-collection__item--active:focus{background-color:#dee0e2;color:#222f3e}.tox .tox-collection--toolbar .tox-collection__item--active:focus::after{border-radius:3px;bottom:0;box-shadow:0 0 0 0 transparent;content:'';left:0;position:absolute;right:0;top:0}@media (forced-colors:active){.tox .tox-collection--toolbar .tox-collection__item--active:focus::after{border:2px solid highlight}}.tox .tox-collection--grid .tox-collection__item--enabled{background-color:#c8cbcf;color:#222f3e}.tox .tox-collection--grid .tox-collection__item--active:not(.tox-collection__item--state-disabled){background-color:#dee0e2;color:#222f3e;position:relative;z-index:1}.tox .tox-collection--grid .tox-collection__item--active:not(.tox-collection__item--state-disabled):focus::after{border-radius:3px;bottom:0;box-shadow:0 0 0 0 transparent inset;content:'';left:0;position:absolute;right:0;top:0}@media (forced-colors:active){.tox .tox-collection--grid .tox-collection__item--active:not(.tox-collection__item--state-disabled):focus::after{border:2px solid highlight}}.tox .tox-collection--list .tox-collection__item--active:not(.tox-collection__item--state-disabled){color:#222f3e}@media (forced-colors:active){.tox .tox-collection--list .tox-collection__item--active:not(.tox-collection__item--state-disabled){border:solid 1px}}.tox .tox-collection--toolbar .tox-collection__item--active:not(.tox-collection__item--state-disabled){color:#222f3e}@media (forced-colors:active){.tox .tox-collection--toolbar .tox-collection__item--active:not(.tox-collection__item--state-disabled):hover{border-radius:3px;outline:solid 1px}}.tox .tox-collection__item-checkmark,.tox .tox-collection__item-icon{align-items:center;display:flex;height:24px;justify-content:center;width:24px}.tox .tox-collection__item-checkmark svg,.tox .tox-collection__item-icon svg{fill:currentColor}.tox .tox-collection--toolbar-lg .tox-collection__item-icon{height:48px;width:48px}.tox .tox-collection__item-label{color:currentColor;display:inline-block;flex:1;font-size:14px;font-style:normal;font-weight:400;line-height:24px;max-width:100%;text-transform:none;word-break:break-all}.tox .tox-collection__item-accessory{color:currentColor;display:inline-block;font-size:14px;height:24px;line-height:24px;text-transform:none}.tox .tox-collection__item-caret{align-items:center;display:flex;min-height:24px}.tox .tox-collection__item-caret::after{content:'';font-size:0;min-height:inherit}.tox .tox-collection__item-caret svg{fill:currentColor}.tox .tox-collection__item--state-disabled{background-color:transparent;color:rgba(34,47,62,.5);cursor:not-allowed}.tox .tox-collection__item--state-disabled .tox-collection__item-caret svg{fill:rgba(34,47,62,.5)}.tox .tox-collection--list .tox-collection__item:not(.tox-collection__item--enabled) .tox-collection__item-checkmark svg{display:none}.tox .tox-collection--list .tox-collection__item:not(.tox-collection__item--enabled) .tox-collection__item-accessory+.tox-collection__item-checkmark{display:none}.tox .tox-collection--horizontal{background-color:#fff;border:1px solid #ccc;border-radius:3px;box-shadow:0 0 2px 0 rgba(34,47,62,.2),0 4px 8px 0 rgba(34,47,62,.15);display:flex;flex:0 0 auto;flex-shrink:0;flex-wrap:nowrap;margin-bottom:0;overflow-x:auto;padding:0}.tox .tox-collection--horizontal .tox-collection__group{align-items:center;display:flex;flex-wrap:nowrap;margin:0;padding:0 4px}.tox .tox-collection--horizontal .tox-collection__item{height:34px;margin:3px 0 2px 0;padding:0 4px}.tox .tox-collection--horizontal .tox-collection__item-label{white-space:nowrap}.tox .tox-collection--horizontal .tox-collection__item-caret{margin-left:4px}.tox .tox-collection__item-container{display:flex}.tox .tox-collection__item-container--row{align-items:center;flex:1 1 auto;flex-direction:row}.tox .tox-collection__item-container--row.tox-collection__item-container--align-left{margin-right:auto}.tox .tox-collection__item-container--row.tox-collection__item-container--align-right{justify-content:flex-end;margin-left:auto}.tox .tox-collection__item-container--row.tox-collection__item-container--valign-top{align-items:flex-start;margin-bottom:auto}.tox .tox-collection__item-container--row.tox-collection__item-container--valign-middle{align-items:center}.tox .tox-collection__item-container--row.tox-collection__item-container--valign-bottom{align-items:flex-end;margin-top:auto}.tox .tox-collection__item-container--column{align-self:center;flex:1 1 auto;flex-direction:column}.tox .tox-collection__item-container--column.tox-collection__item-container--align-left{align-items:flex-start}.tox .tox-collection__item-container--column.tox-collection__item-container--align-right{align-items:flex-end}.tox .tox-collection__item-container--column.tox-collection__item-container--valign-top{align-self:flex-start}.tox .tox-collection__item-container--column.tox-collection__item-container--valign-middle{align-self:center}.tox .tox-collection__item-container--column.tox-collection__item-container--valign-bottom{align-self:flex-end}.tox:not([dir=rtl]) .tox-collection--horizontal .tox-collection__group:not(:last-of-type){border-right:1px solid #ccc}.tox:not([dir=rtl]) .tox-collection--list .tox-collection__item>:not(:first-child){margin-left:8px}.tox:not([dir=rtl]) .tox-collection--list .tox-collection__item>.tox-collection__item-label:first-child{margin-left:4px}.tox:not([dir=rtl]) .tox-collection__item-accessory{margin-left:16px;text-align:right}.tox:not([dir=rtl]) .tox-collection .tox-collection__item-caret{margin-left:16px}.tox[dir=rtl] .tox-collection--horizontal .tox-collection__group:not(:last-of-type){border-left:1px solid #ccc}.tox[dir=rtl] .tox-collection--list .tox-collection__item>:not(:first-child){margin-right:8px}.tox[dir=rtl] .tox-collection--list .tox-collection__item>.tox-collection__item-label:first-child{margin-right:4px}.tox[dir=rtl] .tox-collection__item-accessory{margin-right:16px;text-align:left}.tox[dir=rtl] .tox-collection .tox-collection__item-caret{margin-right:16px;transform:rotateY(180deg)}.tox[dir=rtl] .tox-collection--horizontal .tox-collection__item-caret{margin-right:4px}@media (forced-colors:active){.tox .tox-hue-slider,.tox .tox-rgb-form .tox-rgba-preview{background-color:currentColor!important;border:1px solid highlight!important;forced-color-adjust:none}}.tox .tox-color-picker-container{display:flex;flex-direction:row;height:225px;margin:0}.tox .tox-sv-palette{box-sizing:border-box;display:flex;height:100%}.tox .tox-sv-palette-spectrum{height:100%}.tox .tox-sv-palette,.tox .tox-sv-palette-spectrum{width:225px}.tox .tox-sv-palette-thumb{background:0 0;border:1px solid #000;border-radius:50%;box-sizing:content-box;height:12px;position:absolute;width:12px}.tox .tox-sv-palette-inner-thumb{border:1px solid #fff;border-radius:50%;height:10px;position:absolute;width:10px}.tox .tox-hue-slider{box-sizing:border-box;height:100%;width:25px}.tox .tox-hue-slider-spectrum{background:linear-gradient(to bottom,red,#ff0080,#f0f,#8000ff,#00f,#0080ff,#0ff,#00ff80,#0f0,#80ff00,#ff0,#ff8000,red);height:100%;width:100%}.tox .tox-hue-slider,.tox .tox-hue-slider-spectrum{width:20px}.tox .tox-hue-slider-spectrum:focus,.tox .tox-sv-palette-spectrum:focus{outline:#08f solid}.tox .tox-hue-slider-thumb{background:#fff;border:1px solid #000;box-sizing:content-box;height:4px;width:100%}.tox .tox-rgb-form{display:flex;flex-direction:column;justify-content:space-between}.tox .tox-rgb-form div{align-items:center;display:flex;justify-content:space-between;margin-bottom:5px;width:inherit}.tox .tox-rgb-form input{width:6em}.tox .tox-rgb-form input.tox-invalid{border:1px solid red!important}.tox .tox-rgb-form .tox-rgba-preview{border:1px solid #000;flex-grow:2;margin-bottom:0}.tox:not([dir=rtl]) .tox-sv-palette{margin-right:15px}.tox:not([dir=rtl]) .tox-hue-slider{margin-right:15px}.tox:not([dir=rtl]) .tox-hue-slider-thumb{margin-left:-1px}.tox:not([dir=rtl]) .tox-rgb-form label{margin-right:.5em}.tox[dir=rtl] .tox-sv-palette{margin-left:15px}.tox[dir=rtl] .tox-hue-slider{margin-left:15px}.tox[dir=rtl] .tox-hue-slider-thumb{margin-right:-1px}.tox[dir=rtl] .tox-rgb-form label{margin-left:.5em}.tox .tox-toolbar .tox-swatches,.tox .tox-toolbar__overflow .tox-swatches,.tox .tox-toolbar__primary .tox-swatches{margin:2px 0 3px 4px}.tox .tox-collection--list .tox-collection__group .tox-swatches-menu{border:0;margin:-4px 0}.tox .tox-swatches__row{display:flex}@media (forced-colors:active){.tox .tox-swatches__row{forced-color-adjust:none}}.tox .tox-swatch{height:30px;transition:transform .15s,box-shadow .15s;width:30px}.tox .tox-swatch:focus,.tox .tox-swatch:hover{box-shadow:0 0 0 1px rgba(127,127,127,.3) inset;transform:scale(.8)}.tox .tox-swatch--remove{align-items:center;display:flex;justify-content:center}.tox .tox-swatch--remove svg path{stroke:#e74c3c}.tox .tox-swatches__picker-btn{align-items:center;background-color:transparent;border:0;cursor:pointer;display:flex;height:30px;justify-content:center;outline:0;padding:0;width:30px}.tox .tox-swatches__picker-btn svg{fill:#222f3e;height:24px;width:24px}.tox .tox-swatches__picker-btn:hover{background:#dee0e2}.tox div.tox-swatch:not(.tox-swatch--remove) svg{display:none;fill:#222f3e;height:24px;margin:calc((30px - 24px)/ 2) calc((30px - 24px)/ 2);width:24px}.tox div.tox-swatch:not(.tox-swatch--remove) svg path{fill:#fff;paint-order:stroke;stroke:#222f3e;stroke-width:2px}.tox div.tox-swatch:not(.tox-swatch--remove).tox-collection__item--enabled svg{display:block}.tox:not([dir=rtl]) .tox-swatches__picker-btn{margin-left:auto}.tox[dir=rtl] .tox-swatches__picker-btn{margin-right:auto}.tox .tox-comment-thread{background:#fff;position:relative}.tox .tox-comment-thread>:not(:first-child){margin-top:8px}.tox .tox-comment{background:#fff;border:1px solid #ccc;border-radius:3px;box-shadow:0 4px 8px 0 rgba(34,47,62,.1);padding:8px 8px 16px 8px;position:relative}.tox .tox-comment__header{align-items:center;color:#222f3e;display:flex;justify-content:space-between}.tox .tox-comment__date{color:#222f3e;font-size:12px;line-height:18px}.tox .tox-comment__body{color:#222f3e;font-size:14px;font-style:normal;font-weight:400;line-height:1.3;margin-top:8px;position:relative;text-transform:initial}.tox .tox-comment__body textarea{resize:none;white-space:normal;width:100%}.tox .tox-comment__expander{padding-top:8px}.tox .tox-comment__expander p{color:rgba(34,47,62,.7);font-size:14px;font-style:normal}.tox .tox-comment__body p{margin:0}.tox .tox-comment__buttonspacing{padding-top:16px;text-align:center}.tox .tox-comment-thread__overlay::after{background:#fff;bottom:0;content:\\\"\\\";display:flex;left:0;opacity:.9;position:absolute;right:0;top:0;z-index:5}.tox .tox-comment__reply{display:flex;flex-shrink:0;flex-wrap:wrap;justify-content:flex-end;margin-top:8px}.tox .tox-comment__reply>:first-child{margin-bottom:8px;width:100%}.tox .tox-comment__edit{display:flex;flex-wrap:wrap;justify-content:flex-end;margin-top:16px}.tox .tox-comment__gradient::after{background:linear-gradient(rgba(255,255,255,0),#fff);bottom:0;content:\\\"\\\";display:block;height:5em;margin-top:-40px;position:absolute;width:100%}.tox .tox-comment__overlay{background:#fff;bottom:0;display:flex;flex-direction:column;flex-grow:1;left:0;opacity:.9;position:absolute;right:0;text-align:center;top:0;z-index:5}.tox .tox-comment__loading-text{align-items:center;color:#222f3e;display:flex;flex-direction:column;position:relative}.tox .tox-comment__loading-text>div{padding-bottom:16px}.tox .tox-comment__overlaytext{bottom:0;flex-direction:column;font-size:14px;left:0;padding:1em;position:absolute;right:0;top:0;z-index:10}.tox .tox-comment__overlaytext p{background-color:#fff;box-shadow:0 0 8px 8px #fff;color:#222f3e;text-align:center}.tox .tox-comment__overlaytext div:nth-of-type(2){font-size:.8em}.tox .tox-comment__busy-spinner{align-items:center;background-color:#fff;bottom:0;display:flex;justify-content:center;left:0;position:absolute;right:0;top:0;z-index:20}.tox .tox-comment__scroll{display:flex;flex-direction:column;flex-shrink:1;overflow:auto}.tox .tox-conversations{margin:8px}.tox:not([dir=rtl]) .tox-comment__edit{margin-left:8px}.tox:not([dir=rtl]) .tox-comment__buttonspacing>:last-child,.tox:not([dir=rtl]) .tox-comment__edit>:last-child,.tox:not([dir=rtl]) .tox-comment__reply>:last-child{margin-left:8px}.tox[dir=rtl] .tox-comment__edit{margin-right:8px}.tox[dir=rtl] .tox-comment__buttonspacing>:last-child,.tox[dir=rtl] .tox-comment__edit>:last-child,.tox[dir=rtl] .tox-comment__reply>:last-child{margin-right:8px}.tox .tox-user{align-items:center;display:flex}.tox .tox-user__avatar svg{fill:rgba(34,47,62,.7)}.tox .tox-user__avatar img{border-radius:50%;height:36px;object-fit:cover;vertical-align:middle;width:36px}.tox .tox-user__name{color:#222f3e;font-size:14px;font-style:normal;font-weight:700;line-height:18px;text-transform:none}.tox:not([dir=rtl]) .tox-user__avatar img,.tox:not([dir=rtl]) .tox-user__avatar svg{margin-right:8px}.tox:not([dir=rtl]) .tox-user__avatar+.tox-user__name{margin-left:8px}.tox[dir=rtl] .tox-user__avatar img,.tox[dir=rtl] .tox-user__avatar svg{margin-left:8px}.tox[dir=rtl] .tox-user__avatar+.tox-user__name{margin-right:8px}.tox .tox-dialog-wrap{align-items:center;bottom:0;display:flex;justify-content:center;left:0;position:fixed;right:0;top:0;z-index:1100}.tox .tox-dialog-wrap__backdrop{background-color:rgba(255,255,255,.75);bottom:0;left:0;position:absolute;right:0;top:0;z-index:1}.tox .tox-dialog-wrap__backdrop--opaque{background-color:#fff}.tox .tox-dialog{background-color:#fff;border-color:#ccc;border-radius:3px;border-style:solid;border-width:1px;box-shadow:0 16px 16px -10px rgba(34,47,62,.15),0 0 40px 1px rgba(34,47,62,.15);display:flex;flex-direction:column;max-height:100%;max-width:480px;overflow:hidden;position:relative;width:95vw;z-index:2}@media only screen and (max-width:767px){body:not(.tox-force-desktop) .tox .tox-dialog{align-self:flex-start;margin:8px auto;max-height:calc(100vh - 8px * 2);width:calc(100vw - 16px)}}.tox .tox-dialog-inline{z-index:1100}.tox .tox-dialog__header{align-items:center;background-color:#fff;border-bottom:none;color:#222f3e;display:flex;font-size:16px;justify-content:space-between;padding:8px 16px 0 16px;position:relative}.tox .tox-dialog__header .tox-button{z-index:1}.tox .tox-dialog__draghandle{cursor:grab;height:100%;left:0;position:absolute;top:0;width:100%}.tox .tox-dialog__draghandle:active{cursor:grabbing}.tox .tox-dialog__dismiss{margin-left:auto}.tox .tox-dialog__title{font-family:-apple-system,BlinkMacSystemFont,\\\"Segoe UI\\\",Roboto,Oxygen-Sans,Ubuntu,Cantarell,\\\"Helvetica Neue\\\",sans-serif;font-size:20px;font-style:normal;font-weight:400;line-height:1.3;margin:0;text-transform:none}.tox .tox-dialog__body{color:#222f3e;display:flex;flex:1;font-size:16px;font-style:normal;font-weight:400;line-height:1.3;min-width:0;text-align:left;text-transform:none}@media only screen and (max-width:767px){body:not(.tox-force-desktop) .tox .tox-dialog__body{flex-direction:column}}.tox .tox-dialog__body-nav{align-items:flex-start;display:flex;flex-direction:column;flex-shrink:0;padding:16px 16px}@media only screen and (min-width:768px){.tox .tox-dialog__body-nav{max-width:11em}}@media only screen and (max-width:767px){body:not(.tox-force-desktop) .tox .tox-dialog__body-nav{flex-direction:row;-webkit-overflow-scrolling:touch;overflow-x:auto;padding-bottom:0}}.tox .tox-dialog__body-nav-item{border-bottom:2px solid transparent;color:rgba(34,47,62,.7);display:inline-block;flex-shrink:0;font-size:14px;line-height:1.3;margin-bottom:8px;max-width:13em;text-decoration:none}.tox .tox-dialog__body-nav-item:focus{background-color:rgba(32,122,183,.1)}.tox .tox-dialog__body-nav-item--active{border-bottom:2px solid #207ab7;color:#207ab7}@media (forced-colors:active){.tox .tox-dialog__body-nav-item--active{border-bottom:2px solid highlight;color:highlight}}.tox .tox-dialog__body-content{box-sizing:border-box;display:flex;flex:1;flex-direction:column;max-height:min(650px,calc(100vh - 110px));overflow:auto;-webkit-overflow-scrolling:touch;padding:16px 16px}.tox .tox-dialog__body-content>*{margin-bottom:0;margin-top:16px}.tox .tox-dialog__body-content>:first-child{margin-top:0}.tox .tox-dialog__body-content>:last-child{margin-bottom:0}.tox .tox-dialog__body-content>:only-child{margin-bottom:0;margin-top:0}.tox .tox-dialog__body-content a{color:#207ab7;cursor:pointer;text-decoration:underline}.tox .tox-dialog__body-content a:focus,.tox .tox-dialog__body-content a:hover{color:#114060;text-decoration:underline}.tox .tox-dialog__body-content a:focus-visible{border-radius:1px;outline:2px solid #207ab7;outline-offset:2px}.tox .tox-dialog__body-content a:active{color:#092335;text-decoration:underline}.tox .tox-dialog__body-content svg{fill:#222f3e}.tox .tox-dialog__body-content strong{font-weight:700}.tox .tox-dialog__body-content ul{list-style-type:disc}.tox .tox-dialog__body-content dd,.tox .tox-dialog__body-content ol,.tox .tox-dialog__body-content ul{padding-inline-start:2.5rem}.tox .tox-dialog__body-content dl,.tox .tox-dialog__body-content ol,.tox .tox-dialog__body-content ul{margin-bottom:16px}.tox .tox-dialog__body-content dd,.tox .tox-dialog__body-content dl,.tox .tox-dialog__body-content dt,.tox .tox-dialog__body-content ol,.tox .tox-dialog__body-content ul{display:block;margin-inline-end:0;margin-inline-start:0}.tox .tox-dialog__body-content .tox-form__group h1{color:#222f3e;font-size:20px;font-style:normal;font-weight:700;letter-spacing:normal;margin-bottom:16px;margin-top:2rem;text-transform:none}.tox .tox-dialog__body-content .tox-form__group h2{color:#222f3e;font-size:16px;font-style:normal;font-weight:700;letter-spacing:normal;margin-bottom:16px;margin-top:2rem;text-transform:none}.tox .tox-dialog__body-content .tox-form__group p{margin-bottom:16px}.tox .tox-dialog__body-content .tox-form__group h1:first-child,.tox .tox-dialog__body-content .tox-form__group h2:first-child,.tox .tox-dialog__body-content .tox-form__group p:first-child{margin-top:0}.tox .tox-dialog__body-content .tox-form__group h1:last-child,.tox .tox-dialog__body-content .tox-form__group h2:last-child,.tox .tox-dialog__body-content .tox-form__group p:last-child{margin-bottom:0}.tox .tox-dialog__body-content .tox-form__group h1:only-child,.tox .tox-dialog__body-content .tox-form__group h2:only-child,.tox .tox-dialog__body-content .tox-form__group p:only-child{margin-bottom:0;margin-top:0}.tox .tox-dialog__body-content .tox-form__group .tox-label.tox-label--center{text-align:center}.tox .tox-dialog__body-content .tox-form__group .tox-label.tox-label--end{text-align:end}.tox .tox-dialog--width-lg{height:650px;max-width:1200px}.tox .tox-dialog--fullscreen{height:100%;max-width:100%}.tox .tox-dialog--fullscreen .tox-dialog__body-content{max-height:100%}.tox .tox-dialog--width-md{max-width:800px}.tox .tox-dialog--width-md .tox-dialog__body-content{overflow:auto}.tox .tox-dialog__body-content--centered{text-align:center}.tox .tox-dialog__footer{align-items:center;background-color:#fff;border-top:1px solid #ccc;display:flex;justify-content:space-between;padding:8px 16px}.tox .tox-dialog__footer-end,.tox .tox-dialog__footer-start{display:flex}.tox .tox-dialog__busy-spinner{align-items:center;background-color:rgba(255,255,255,.75);bottom:0;display:flex;justify-content:center;left:0;position:absolute;right:0;top:0;z-index:3}.tox .tox-dialog__table{border-collapse:collapse;width:100%}.tox .tox-dialog__table thead th{font-weight:700;padding-bottom:8px}.tox .tox-dialog__table thead th:first-child{padding-right:8px}.tox .tox-dialog__table tbody tr{border-bottom:1px solid #404040}.tox .tox-dialog__table tbody tr:last-child{border-bottom:none}.tox .tox-dialog__table td{padding-bottom:8px;padding-top:8px}.tox .tox-dialog__table td:first-child{padding-right:8px}.tox .tox-dialog__iframe{min-height:200px}.tox .tox-dialog__iframe.tox-dialog__iframe--opaque{background:#fff}.tox .tox-navobj-bordered{position:relative}.tox .tox-navobj-bordered::before{border:1px solid #ccc;border-radius:3px;content:'';inset:0;opacity:1;pointer-events:none;position:absolute;z-index:1}.tox .tox-navobj-bordered iframe{border-radius:3px}.tox .tox-navobj-bordered-focus.tox-navobj-bordered::before{border-color:#207ab7;box-shadow:none;outline:2px solid rgba(32,122,183,.25)}.tox .tox-dialog__popups{position:absolute;width:100%;z-index:1100}.tox .tox-dialog__body-iframe{display:flex;flex:1;flex-direction:column}.tox .tox-dialog__body-iframe .tox-navobj{display:flex;flex:1}.tox .tox-dialog__body-iframe .tox-navobj :nth-child(2){flex:1;height:100%}.tox .tox-dialog-dock-fadeout{opacity:0;visibility:hidden}.tox .tox-dialog-dock-fadein{opacity:1;visibility:visible}.tox .tox-dialog-dock-transition{transition:visibility 0s linear .3s,opacity .3s ease}.tox .tox-dialog-dock-transition.tox-dialog-dock-fadein{transition-delay:0s}@media only screen and (max-width:767px){body:not(.tox-force-desktop) .tox:not([dir=rtl]) .tox-dialog__body-nav{margin-right:0}}@media only screen and (max-width:767px){body:not(.tox-force-desktop) .tox:not([dir=rtl]) .tox-dialog__body-nav-item:not(:first-child){margin-left:8px}}.tox:not([dir=rtl]) .tox-dialog__footer .tox-dialog__footer-end>*,.tox:not([dir=rtl]) .tox-dialog__footer .tox-dialog__footer-start>*{margin-left:8px}.tox[dir=rtl] .tox-dialog__body{text-align:right}@media only screen and (max-width:767px){body:not(.tox-force-desktop) .tox[dir=rtl] .tox-dialog__body-nav{margin-left:0}}@media only screen and (max-width:767px){body:not(.tox-force-desktop) .tox[dir=rtl] .tox-dialog__body-nav-item:not(:first-child){margin-right:8px}}.tox[dir=rtl] .tox-dialog__footer .tox-dialog__footer-end>*,.tox[dir=rtl] .tox-dialog__footer .tox-dialog__footer-start>*{margin-right:8px}body.tox-dialog__disable-scroll{overflow:hidden}.tox .tox-dropzone-container{display:flex;flex:1}.tox .tox-dropzone{align-items:center;background:#fff;border:2px dashed #ccc;box-sizing:border-box;display:flex;flex-direction:column;flex-grow:1;justify-content:center;min-height:100px;padding:10px}.tox .tox-dropzone p{color:rgba(34,47,62,.7);margin:0 0 16px 0}.tox .tox-edit-area{display:flex;flex:1;overflow:hidden;position:relative}.tox .tox-edit-area::before{border:0 solid transparent;border-radius:4px;content:'';inset:0;opacity:0;pointer-events:none;position:absolute;transition:opacity .15s;z-index:1}@media (forced-colors:active){.tox .tox-edit-area::before{border:0 solid highlight}}.tox .tox-edit-area__iframe{background-color:#fff;border:0;box-sizing:border-box;flex:1;height:100%;position:absolute;width:100%}.tox.tox-edit-focus .tox-edit-area::before{opacity:1}.tox.tox-inline-edit-area{border:1px dotted #ccc}.tox .tox-editor-container{display:flex;flex:1 1 auto;flex-direction:column;overflow:hidden}.tox .tox-editor-header{display:grid;grid-template-columns:1fr min-content;z-index:2}.tox:not(.tox-tinymce-inline) .tox-editor-header{background-color:#fff;border-bottom:none;box-shadow:none;padding:4px 0}.tox:not(.tox-tinymce-inline) .tox-editor-header:not(.tox-editor-dock-transition){transition:box-shadow .5s}.tox:not(.tox-tinymce-inline).tox-tinymce--toolbar-bottom .tox-editor-header{border-top:1px solid #ccc;box-shadow:none}.tox:not(.tox-tinymce-inline).tox-tinymce--toolbar-sticky-on .tox-editor-header{background-color:#fff;box-shadow:0 4px 4px -3px rgba(0,0,0,.25);padding:4px 0}.tox:not(.tox-tinymce-inline).tox-tinymce--toolbar-sticky-on.tox-tinymce--toolbar-bottom .tox-editor-header{box-shadow:0 4px 4px -3px rgba(0,0,0,.25)}.tox.tox:not(.tox-tinymce-inline) .tox-editor-header.tox-editor-header--empty{background:0 0;border:none;box-shadow:none;padding:0}.tox-editor-dock-fadeout{opacity:0;visibility:hidden}.tox-editor-dock-fadein{opacity:1;visibility:visible}.tox-editor-dock-transition{transition:visibility 0s linear .25s,opacity .25s ease}.tox-editor-dock-transition.tox-editor-dock-fadein{transition-delay:0s}.tox .tox-control-wrap{flex:1;position:relative}.tox .tox-control-wrap:not(.tox-control-wrap--status-invalid) .tox-control-wrap__status-icon-invalid,.tox .tox-control-wrap:not(.tox-control-wrap--status-unknown) .tox-control-wrap__status-icon-unknown,.tox .tox-control-wrap:not(.tox-control-wrap--status-valid) .tox-control-wrap__status-icon-valid{display:none}.tox .tox-control-wrap svg{display:block}.tox .tox-control-wrap__status-icon-wrap{position:absolute;top:50%;transform:translateY(-50%)}.tox .tox-control-wrap__status-icon-invalid svg{fill:#c00}.tox .tox-control-wrap__status-icon-unknown svg{fill:orange}.tox .tox-control-wrap__status-icon-valid svg{fill:green}.tox:not([dir=rtl]) .tox-control-wrap--status-invalid .tox-textfield,.tox:not([dir=rtl]) .tox-control-wrap--status-unknown .tox-textfield,.tox:not([dir=rtl]) .tox-control-wrap--status-valid .tox-textfield{padding-right:32px}.tox:not([dir=rtl]) .tox-control-wrap__status-icon-wrap{right:4px}.tox[dir=rtl] .tox-control-wrap--status-invalid .tox-textfield,.tox[dir=rtl] .tox-control-wrap--status-unknown .tox-textfield,.tox[dir=rtl] .tox-control-wrap--status-valid .tox-textfield{padding-left:32px}.tox[dir=rtl] .tox-control-wrap__status-icon-wrap{left:4px}.tox .tox-custom-preview{border-color:#ccc;border-radius:3px;border-style:solid;border-width:1px;flex:1;padding:8px}.tox .tox-autocompleter{max-width:25em}.tox .tox-autocompleter .tox-menu{box-sizing:border-box;max-width:25em}.tox .tox-autocompleter .tox-autocompleter-highlight{font-weight:700}.tox .tox-color-input{display:flex;position:relative;z-index:1}.tox .tox-color-input .tox-textfield{z-index:-1}.tox .tox-color-input span{border-color:rgba(34,47,62,.2);border-radius:3px;border-style:solid;border-width:1px;box-shadow:none;box-sizing:border-box;height:24px;position:absolute;top:6px;width:24px}@media (forced-colors:active){.tox .tox-color-input span{border-color:currentColor;border-width:2px!important;forced-color-adjust:none}}.tox .tox-color-input span:focus:not([aria-disabled=true]),.tox .tox-color-input span:hover:not([aria-disabled=true]){border-color:#207ab7;cursor:pointer}.tox .tox-color-input span::before{background-image:linear-gradient(45deg,rgba(0,0,0,.25) 25%,transparent 25%),linear-gradient(-45deg,rgba(0,0,0,.25) 25%,transparent 25%),linear-gradient(45deg,transparent 75%,rgba(0,0,0,.25) 75%),linear-gradient(-45deg,transparent 75%,rgba(0,0,0,.25) 75%);background-position:0 0,0 6px,6px -6px,-6px 0;background-size:12px 12px;border:1px solid #fff;border-radius:3px;box-sizing:border-box;content:'';height:24px;left:-1px;position:absolute;top:-1px;width:24px;z-index:-1}@media (forced-colors:active){.tox .tox-color-input span::before{border:none}}.tox .tox-color-input span[aria-disabled=true]{cursor:not-allowed}.tox:not([dir=rtl]) .tox-color-input .tox-textfield{padding-left:36px}.tox:not([dir=rtl]) .tox-color-input span{left:6px}.tox[dir=rtl] .tox-color-input .tox-textfield{padding-right:36px}.tox[dir=rtl] .tox-color-input span{right:6px}.tox .tox-label,.tox .tox-toolbar-label{color:rgba(34,47,62,.7);display:block;font-size:14px;font-style:normal;font-weight:400;line-height:1.3;padding:0 8px 0 0;text-transform:none;white-space:nowrap}.tox .tox-toolbar-label{padding:0 8px}.tox[dir=rtl] .tox-label{padding:0 0 0 8px}.tox .tox-form{display:flex;flex:1;flex-direction:column}.tox .tox-form__group{box-sizing:border-box;margin-bottom:4px}.tox .tox-form-group--maximize{flex:1}.tox .tox-form__group--error{color:#c00}.tox .tox-form__group--collection{display:flex}.tox .tox-form__grid{display:flex;flex-direction:row;flex-wrap:wrap;justify-content:space-between}.tox .tox-form__grid--2col>.tox-form__group{width:calc(50% - (8px / 2))}.tox .tox-form__grid--3col>.tox-form__group{width:calc(100% / 3 - (8px / 2))}.tox .tox-form__grid--4col>.tox-form__group{width:calc(25% - (8px / 2))}.tox .tox-form__controls-h-stack{align-items:center;display:flex}.tox .tox-form__group--inline{align-items:center;display:flex}.tox .tox-form__group--stretched{display:flex;flex:1;flex-direction:column}.tox .tox-form__group--stretched .tox-textarea{flex:1}.tox .tox-form__group--stretched .tox-navobj{display:flex;flex:1}.tox .tox-form__group--stretched .tox-navobj :nth-child(2){flex:1;height:100%}.tox:not([dir=rtl]) .tox-form__controls-h-stack>:not(:first-child){margin-left:4px}.tox[dir=rtl] .tox-form__controls-h-stack>:not(:first-child){margin-right:4px}.tox .tox-lock.tox-locked .tox-lock-icon__unlock,.tox .tox-lock:not(.tox-locked) .tox-lock-icon__lock{display:none}.tox .tox-listboxfield .tox-listbox--select,.tox .tox-textarea,.tox .tox-textarea-wrap .tox-textarea:focus,.tox .tox-textfield,.tox .tox-toolbar-textfield{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#fff;border-color:#ccc;border-radius:3px;border-style:solid;border-width:1px;box-shadow:none;box-sizing:border-box;color:#222f3e;font-family:-apple-system,BlinkMacSystemFont,\\\"Segoe UI\\\",Roboto,Oxygen-Sans,Ubuntu,Cantarell,\\\"Helvetica Neue\\\",sans-serif;font-size:16px;line-height:24px;margin:0;min-height:34px;outline:0;padding:5px 4.75px;resize:none;width:100%}.tox .tox-textarea[disabled],.tox .tox-textfield[disabled]{background-color:#f2f2f2;color:rgba(34,47,62,.85);cursor:not-allowed}.tox .tox-custom-editor:focus-within,.tox .tox-listboxfield .tox-listbox--select:focus,.tox .tox-textarea-wrap:focus-within,.tox .tox-textarea:focus,.tox .tox-textfield:focus{background-color:#fff;border-color:#207ab7;box-shadow:none;outline:2px solid rgba(32,122,183,.25)}.tox .tox-toolbar-textfield{border-width:0;margin-bottom:3px;margin-top:2px;max-width:250px}.tox .tox-naked-btn{background-color:transparent;border:0;border-color:transparent;box-shadow:unset;color:#207ab7;cursor:pointer;display:block;margin:0;padding:0}.tox .tox-naked-btn svg{display:block;fill:#222f3e}.tox:not([dir=rtl]) .tox-toolbar-textfield+*{margin-left:4px}.tox[dir=rtl] .tox-toolbar-textfield+*{margin-right:4px}.tox .tox-listboxfield{cursor:pointer;position:relative}.tox .tox-listboxfield .tox-listbox--select[disabled]{background-color:#f2f2f2;color:rgba(34,47,62,.85);cursor:not-allowed}.tox .tox-listbox__select-label{cursor:default;flex:1;margin:0 4px}.tox .tox-listbox__select-chevron{align-items:center;display:flex;justify-content:center;width:16px}.tox .tox-listbox__select-chevron svg{fill:#222f3e}@media (forced-colors:active){.tox .tox-listbox__select-chevron svg{fill:currentColor!important}}.tox .tox-listboxfield .tox-listbox--select{align-items:center;display:flex}.tox:not([dir=rtl]) .tox-listboxfield svg{right:8px}.tox[dir=rtl] .tox-listboxfield svg{left:8px}.tox .tox-selectfield{cursor:pointer;position:relative}.tox .tox-selectfield select{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#fff;border-color:#ccc;border-radius:3px;border-style:solid;border-width:1px;box-shadow:none;box-sizing:border-box;color:#222f3e;font-family:-apple-system,BlinkMacSystemFont,\\\"Segoe UI\\\",Roboto,Oxygen-Sans,Ubuntu,Cantarell,\\\"Helvetica Neue\\\",sans-serif;font-size:16px;line-height:24px;margin:0;min-height:34px;outline:0;padding:5px 4.75px;resize:none;width:100%}.tox .tox-selectfield select[disabled]{background-color:#f2f2f2;color:rgba(34,47,62,.85);cursor:not-allowed}.tox .tox-selectfield select::-ms-expand{display:none}.tox .tox-selectfield select:focus{background-color:#fff;border-color:#207ab7;box-shadow:none;outline:2px solid rgba(32,122,183,.25)}.tox .tox-selectfield svg{pointer-events:none;position:absolute;top:50%;transform:translateY(-50%)}.tox:not([dir=rtl]) .tox-selectfield select[size=\\\"0\\\"],.tox:not([dir=rtl]) .tox-selectfield select[size=\\\"1\\\"]{padding-right:24px}.tox:not([dir=rtl]) .tox-selectfield svg{right:8px}.tox[dir=rtl] .tox-selectfield select[size=\\\"0\\\"],.tox[dir=rtl] .tox-selectfield select[size=\\\"1\\\"]{padding-left:24px}.tox[dir=rtl] .tox-selectfield svg{left:8px}.tox .tox-textarea-wrap{border-color:#ccc;border-radius:3px;border-style:solid;border-width:1px;display:flex;flex:1;overflow:hidden}.tox .tox-textarea{-webkit-appearance:textarea;-moz-appearance:textarea;appearance:textarea;white-space:pre-wrap}.tox .tox-textarea-wrap .tox-textarea{border:none}.tox .tox-textarea-wrap .tox-textarea:focus{border:none}.tox-fullscreen{border:0;height:100%;margin:0;overflow:hidden;overscroll-behavior:none;padding:0;touch-action:pinch-zoom;width:100%}.tox.tox-tinymce.tox-fullscreen .tox-statusbar__resize-handle{display:none}.tox-shadowhost.tox-fullscreen,.tox.tox-tinymce.tox-fullscreen{left:0;position:fixed;top:0;z-index:1200}.tox.tox-tinymce.tox-fullscreen{background-color:transparent}.tox-fullscreen .tox.tox-tinymce-aux,.tox-fullscreen~.tox.tox-tinymce-aux{z-index:1201}.tox .tox-help__more-link{list-style:none;margin-top:1em}.tox .tox-imagepreview{background-color:#666;height:380px;overflow:hidden;position:relative;width:100%}.tox .tox-imagepreview.tox-imagepreview__loaded{overflow:auto}.tox .tox-imagepreview__container{display:flex;left:100vw;position:absolute;top:100vw}.tox .tox-imagepreview__image{background:url(data:image/gif;base64,R0lGODdhDAAMAIABAMzMzP///ywAAAAADAAMAAACFoQfqYeabNyDMkBQb81Uat85nxguUAEAOw==)}.tox .tox-image-tools .tox-spacer{flex:1}.tox .tox-image-tools .tox-bar{align-items:center;display:flex;height:60px;justify-content:center}.tox .tox-image-tools .tox-imagepreview,.tox .tox-image-tools .tox-imagepreview+.tox-bar{margin-top:8px}.tox .tox-image-tools .tox-croprect-block{background:#000;opacity:.5;position:absolute;zoom:1}.tox .tox-image-tools .tox-croprect-handle{border:2px solid #fff;height:20px;left:0;position:absolute;top:0;width:20px}.tox .tox-image-tools .tox-croprect-handle-move{border:0;cursor:move;position:absolute}.tox .tox-image-tools .tox-croprect-handle-nw{border-width:2px 0 0 2px;cursor:nw-resize;left:100px;margin:-2px 0 0 -2px;top:100px}.tox .tox-image-tools .tox-croprect-handle-ne{border-width:2px 2px 0 0;cursor:ne-resize;left:200px;margin:-2px 0 0 -20px;top:100px}.tox .tox-image-tools .tox-croprect-handle-sw{border-width:0 0 2px 2px;cursor:sw-resize;left:100px;margin:-20px 2px 0 -2px;top:200px}.tox .tox-image-tools .tox-croprect-handle-se{border-width:0 2px 2px 0;cursor:se-resize;left:200px;margin:-20px 0 0 -20px;top:200px}.tox .tox-insert-table-picker{background-color:#fff;display:flex;flex-wrap:wrap;width:170px}.tox .tox-insert-table-picker>div{border-color:#ccc;border-style:solid;border-width:0 1px 1px 0;box-sizing:border-box;height:17px;width:17px}.tox .tox-collection--list .tox-collection__group .tox-insert-table-picker{margin:0 -4px}.tox .tox-insert-table-picker .tox-insert-table-picker__selected{background-color:rgba(32,122,183,.5);border-color:rgba(32,122,183,.5)}@media (forced-colors:active){.tox .tox-insert-table-picker .tox-insert-table-picker__selected{border-color:Highlight;filter:contrast(50%)}}.tox .tox-insert-table-picker__label{color:rgba(34,47,62,.7);display:block;font-size:14px;padding:4px;text-align:center;width:100%}.tox:not([dir=rtl]) .tox-insert-table-picker>div:nth-child(10n){border-right:0}.tox[dir=rtl] .tox-insert-table-picker>div:nth-child(10n+1){border-right:0}.tox .tox-menu{background-color:#fff;border:1px solid #ccc;border-radius:3px;box-shadow:0 4px 8px 0 rgba(34,47,62,.1);display:inline-block;overflow:hidden;vertical-align:top;z-index:1150}.tox .tox-menu.tox-collection.tox-collection--list{padding:0 0}.tox .tox-menu.tox-collection.tox-collection--toolbar{padding:4px}.tox .tox-menu.tox-collection.tox-collection--grid{padding:4px}@media only screen and (min-width:768px){.tox .tox-menu .tox-collection__item-label{overflow-wrap:break-word;word-break:normal}.tox .tox-dialog__popups .tox-menu .tox-collection__item-label{word-break:break-all}}.tox .tox-menu__label blockquote,.tox .tox-menu__label code,.tox .tox-menu__label h1,.tox .tox-menu__label h2,.tox .tox-menu__label h3,.tox .tox-menu__label h4,.tox .tox-menu__label h5,.tox .tox-menu__label h6,.tox .tox-menu__label p{margin:0}.tox .tox-menubar{background:url(\\\"data:image/svg+xml;charset=utf8,%3Csvg height='39px' viewBox='0 0 40 39px' width='40' xmlns='http://www.w3.org/2000/svg'%3E%3Crect x='0' y='38px' width='100' height='1' fill='%23cccccc'/%3E%3C/svg%3E\\\") left 0 top 0 #fff;background-color:#fff;display:flex;flex:0 0 auto;flex-shrink:0;flex-wrap:wrap;grid-column:1/-1;grid-row:1;padding:0 4px 0 4px}.tox .tox-promotion+.tox-menubar{grid-column:1}.tox .tox-promotion{background:url(\\\"data:image/svg+xml;charset=utf8,%3Csvg height='39px' viewBox='0 0 40 39px' width='40' xmlns='http://www.w3.org/2000/svg'%3E%3Crect x='0' y='38px' width='100' height='1' fill='%23cccccc'/%3E%3C/svg%3E\\\") left 0 top 0 #fff;background-color:#fff;grid-column:2;grid-row:1;padding-inline-end:8px;padding-inline-start:4px;padding-top:5px}.tox .tox-promotion-link{align-items:unsafe center;background-color:#e8f1f8;border-radius:5px;color:#086be6;cursor:pointer;display:flex;font-size:14px;height:26.6px;padding:4px 8px;white-space:nowrap}.tox .tox-promotion-link:hover{background-color:#b4d7ff}.tox .tox-promotion-link:focus{background-color:#d9edf7}.tox .tox-mbtn{align-items:center;background:#fff;border:0;border-radius:3px;box-shadow:none;color:#222f3e;display:flex;flex:0 0 auto;font-size:14px;font-style:normal;font-weight:400;height:34px;justify-content:center;margin:2px 0 3px 0;outline:0;padding:0 4px;text-transform:none;width:auto}.tox .tox-mbtn[disabled]{background-color:#fff;border:0;box-shadow:none;color:rgba(34,47,62,.5);cursor:not-allowed}.tox .tox-mbtn:focus:not(:disabled){background:#dee0e2;border:0;box-shadow:none;color:#222f3e;position:relative;z-index:1}.tox .tox-mbtn:focus:not(:disabled)::after{border-radius:3px;bottom:0;box-shadow:0 0 0 0 transparent;content:'';left:0;position:absolute;right:0;top:0}@media (forced-colors:active){.tox .tox-mbtn:focus:not(:disabled)::after{border:2px solid highlight}}.tox .tox-mbtn--active,.tox .tox-mbtn:not(:disabled).tox-mbtn--active:focus{background:#c8cbcf;border:0;box-shadow:none;color:#222f3e}.tox .tox-mbtn:hover:not(:disabled):not(.tox-mbtn--active){background:#dee0e2;border:0;box-shadow:none;color:#222f3e}.tox .tox-mbtn__select-label{cursor:default;font-weight:400;margin:0 4px}.tox .tox-mbtn[disabled] .tox-mbtn__select-label{cursor:not-allowed}.tox .tox-mbtn__select-chevron{align-items:center;display:flex;justify-content:center;width:16px;display:none}.tox .tox-notification{border-radius:3px;border-style:solid;border-width:1px;box-shadow:none;box-sizing:border-box;display:grid;font-size:14px;font-weight:400;grid-template-columns:minmax(40px,1fr) auto minmax(40px,1fr);margin-left:auto;margin-right:auto;margin-top:4px;opacity:0;padding:4px;transition:transform .1s ease-in,opacity 150ms ease-in;width:-moz-max-content;width:max-content}.tox .tox-notification a{cursor:pointer;text-decoration:underline}.tox .tox-notification p{font-size:14px;font-weight:400}.tox .tox-notification:focus{border-color:#207ab7;box-shadow:none}.tox .tox-notification--in{opacity:1}.tox .tox-notification--success{background-color:#e4eeda;border-color:#d7e6c8;color:#222f3e}.tox .tox-notification--success p{color:#222f3e}.tox .tox-notification--success a{color:#517342}.tox .tox-notification--success a:focus,.tox .tox-notification--success a:hover{color:#24321d;text-decoration:underline}.tox .tox-notification--success a:focus-visible{border-radius:1px;outline:2px solid #517342;outline-offset:2px}.tox .tox-notification--success a:active{color:#0d120a;text-decoration:underline}.tox .tox-notification--success svg{fill:#222f3e}.tox .tox-notification--error{background-color:#f5cccc;border-color:#f0b3b3;color:#222f3e}.tox .tox-notification--error p{color:#222f3e}.tox .tox-notification--error a{color:#77181f}.tox .tox-notification--error a:focus,.tox .tox-notification--error a:hover{color:#220709;text-decoration:underline}.tox .tox-notification--error a:focus-visible{border-radius:1px;outline:2px solid #77181f;outline-offset:2px}.tox .tox-notification--error a:active{color:#000;text-decoration:underline}.tox .tox-notification--error svg{fill:#222f3e}.tox .tox-notification--warn,.tox .tox-notification--warning{background-color:#fff5cc;border-color:#fff0b3;color:#222f3e}.tox .tox-notification--warn p,.tox .tox-notification--warning p{color:#222f3e}.tox .tox-notification--warn a,.tox .tox-notification--warning a{color:#7a6e25}.tox .tox-notification--warn a:focus,.tox .tox-notification--warn a:hover,.tox .tox-notification--warning a:focus,.tox .tox-notification--warning a:hover{color:#2c280d;text-decoration:underline}.tox .tox-notification--warn a:focus-visible,.tox .tox-notification--warning a:focus-visible{border-radius:1px;outline:2px solid #7a6e25;outline-offset:2px}.tox .tox-notification--warn a:active,.tox .tox-notification--warning a:active{color:#050502;text-decoration:underline}.tox .tox-notification--warn svg,.tox .tox-notification--warning svg{fill:#222f3e}.tox .tox-notification--info{background-color:#d6e7fb;border-color:#c1dbf9;color:#222f3e}.tox .tox-notification--info p{color:#222f3e}.tox .tox-notification--info a{color:#2a64a6}.tox .tox-notification--info a:focus,.tox .tox-notification--info a:hover{color:#163355;text-decoration:underline}.tox .tox-notification--info a:focus-visible{border-radius:1px;outline:2px solid #2a64a6;outline-offset:2px}.tox .tox-notification--info a:active{color:#0b1a2c;text-decoration:underline}.tox .tox-notification--info svg{fill:#222f3e}.tox .tox-notification__body{align-self:center;color:#222f3e;font-size:14px;grid-column-end:3;grid-column-start:2;grid-row-end:2;grid-row-start:1;text-align:center;white-space:normal;word-break:break-all;word-break:break-word}.tox .tox-notification__body>*{margin:0}.tox .tox-notification__body>*+*{margin-top:1rem}.tox .tox-notification__icon{align-self:center;grid-column-end:2;grid-column-start:1;grid-row-end:2;grid-row-start:1;justify-self:end}.tox .tox-notification__icon svg{display:block}.tox .tox-notification__dismiss{align-self:start;grid-column-end:4;grid-column-start:3;grid-row-end:2;grid-row-start:1;justify-self:end}.tox .tox-notification .tox-progress-bar{grid-column-end:4;grid-column-start:1;grid-row-end:3;grid-row-start:2;justify-self:center}.tox .tox-notification-container-dock-fadeout{opacity:0;visibility:hidden}.tox .tox-notification-container-dock-fadein{opacity:1;visibility:visible}.tox .tox-notification-container-dock-transition{transition:visibility 0s linear .3s,opacity .3s ease}.tox .tox-notification-container-dock-transition.tox-notification-container-dock-fadein{transition-delay:0s}.tox .tox-pop{display:inline-block;position:relative}.tox .tox-pop--resizing{transition:width .1s ease}.tox .tox-pop--resizing .tox-toolbar,.tox .tox-pop--resizing .tox-toolbar__group{flex-wrap:nowrap}.tox .tox-pop--transition{transition:.15s ease;transition-property:left,right,top,bottom}.tox .tox-pop--transition::after,.tox .tox-pop--transition::before{transition:all .15s,visibility 0s,opacity 75ms ease 75ms}.tox .tox-pop__dialog{background-color:#fff;border:1px solid #ccc;border-radius:3px;box-shadow:0 0 2px 0 rgba(34,47,62,.2),0 4px 8px 0 rgba(34,47,62,.15);min-width:0;overflow:hidden}.tox .tox-pop__dialog>:not(.tox-toolbar){margin:4px 4px 4px 8px}.tox .tox-pop__dialog .tox-toolbar{background-color:transparent;margin-bottom:-1px}.tox .tox-pop::after,.tox .tox-pop::before{border-style:solid;content:'';display:block;height:0;opacity:1;position:absolute;width:0}@media (forced-colors:active){.tox .tox-pop::after,.tox .tox-pop::before{content:none}}.tox .tox-pop.tox-pop--inset::after,.tox .tox-pop.tox-pop--inset::before{opacity:0;transition:all 0s .15s,visibility 0s,opacity 75ms ease}.tox .tox-pop.tox-pop--bottom::after,.tox .tox-pop.tox-pop--bottom::before{left:50%;top:100%}.tox .tox-pop.tox-pop--bottom::after{border-color:#fff transparent transparent transparent;border-width:8px;margin-left:-8px;margin-top:-1px}.tox .tox-pop.tox-pop--bottom::before{border-color:#ccc transparent transparent transparent;border-width:9px;margin-left:-9px}.tox .tox-pop.tox-pop--top::after,.tox .tox-pop.tox-pop--top::before{left:50%;top:0;transform:translateY(-100%)}.tox .tox-pop.tox-pop--top::after{border-color:transparent transparent #fff transparent;border-width:8px;margin-left:-8px;margin-top:1px}.tox .tox-pop.tox-pop--top::before{border-color:transparent transparent #ccc transparent;border-width:9px;margin-left:-9px}.tox .tox-pop.tox-pop--left::after,.tox .tox-pop.tox-pop--left::before{left:0;top:calc(50% - 1px);transform:translateY(-50%)}.tox .tox-pop.tox-pop--left::after{border-color:transparent #fff transparent transparent;border-width:8px;margin-left:-15px}.tox .tox-pop.tox-pop--left::before{border-color:transparent #ccc transparent transparent;border-width:10px;margin-left:-19px}.tox .tox-pop.tox-pop--right::after,.tox .tox-pop.tox-pop--right::before{left:100%;top:calc(50% + 1px);transform:translateY(-50%)}.tox .tox-pop.tox-pop--right::after{border-color:transparent transparent transparent #fff;border-width:8px;margin-left:-1px}.tox .tox-pop.tox-pop--right::before{border-color:transparent transparent transparent #ccc;border-width:10px;margin-left:-1px}.tox .tox-pop.tox-pop--align-left::after,.tox .tox-pop.tox-pop--align-left::before{left:20px}.tox .tox-pop.tox-pop--align-right::after,.tox .tox-pop.tox-pop--align-right::before{left:calc(100% - 20px)}.tox .tox-sidebar-wrap{display:flex;flex-direction:row;flex-grow:1;min-height:0}.tox .tox-sidebar{background-color:#fff;display:flex;flex-direction:row;justify-content:flex-end}.tox .tox-sidebar__slider{display:flex;overflow:hidden}.tox .tox-sidebar__pane-container{display:flex}.tox .tox-sidebar__pane{display:flex}.tox .tox-sidebar--sliding-closed{opacity:0}.tox .tox-sidebar--sliding-open{opacity:1}.tox .tox-sidebar--sliding-growing,.tox .tox-sidebar--sliding-shrinking{transition:width .5s ease,opacity .5s ease}.tox .tox-selector{background-color:#4099ff;border-color:#4099ff;border-style:solid;border-width:1px;box-sizing:border-box;display:inline-block;height:10px;position:absolute;width:10px}.tox.tox-platform-touch .tox-selector{height:12px;width:12px}.tox .tox-slider{align-items:center;display:flex;flex:1;height:24px;justify-content:center;position:relative}.tox .tox-slider__rail{background-color:transparent;border:1px solid #ccc;border-radius:3px;height:10px;min-width:120px;width:100%}.tox .tox-slider__handle{background-color:#207ab7;border:2px solid #185d8c;border-radius:3px;box-shadow:none;height:24px;left:50%;position:absolute;top:50%;transform:translateX(-50%) translateY(-50%);width:14px}.tox .tox-form__controls-h-stack>.tox-slider:not(:first-of-type){margin-inline-start:8px}.tox .tox-form__controls-h-stack>.tox-form__group+.tox-slider{margin-inline-start:32px}.tox .tox-form__controls-h-stack>.tox-slider+.tox-form__group{margin-inline-start:32px}.tox .tox-source-code{overflow:auto}.tox .tox-spinner{display:flex}.tox .tox-spinner>div{animation:tam-bouncing-dots 1.5s ease-in-out 0s infinite both;background-color:rgba(34,47,62,.7);border-radius:100%;height:8px;width:8px}.tox .tox-spinner>div:nth-child(1){animation-delay:-.32s}.tox .tox-spinner>div:nth-child(2){animation-delay:-.16s}@keyframes tam-bouncing-dots{0%,100%,80%{transform:scale(0)}40%{transform:scale(1)}}.tox:not([dir=rtl]) .tox-spinner>div:not(:first-child){margin-left:4px}.tox[dir=rtl] .tox-spinner>div:not(:first-child){margin-right:4px}.tox .tox-statusbar{align-items:center;background-color:#fff;border-top:1px solid #ccc;color:rgba(34,47,62,.7);display:flex;flex:0 0 auto;font-size:12px;font-weight:400;height:18px;overflow:hidden;padding:0 8px;position:relative;text-transform:uppercase}.tox .tox-statusbar__path{display:flex;flex:1 1 auto;text-overflow:ellipsis;white-space:nowrap}.tox .tox-statusbar__right-container{display:flex;justify-content:flex-end;white-space:nowrap}.tox .tox-statusbar__help-text{text-align:center}.tox .tox-statusbar__text-container{align-items:flex-start;display:flex;flex:1 1 auto;height:16px;justify-content:space-between;overflow:hidden}@media only screen and (min-width:768px){.tox .tox-statusbar__text-container.tox-statusbar__text-container-3-cols>.tox-statusbar__help-text,.tox .tox-statusbar__text-container.tox-statusbar__text-container-3-cols>.tox-statusbar__path,.tox .tox-statusbar__text-container.tox-statusbar__text-container-3-cols>.tox-statusbar__right-container{flex:0 0 calc(100% / 3)}}.tox .tox-statusbar__text-container.tox-statusbar__text-container--flex-end{justify-content:flex-end}.tox .tox-statusbar__text-container.tox-statusbar__text-container--flex-start{justify-content:flex-start}.tox .tox-statusbar__text-container.tox-statusbar__text-container--space-around{justify-content:space-around}.tox .tox-statusbar__path>*{display:inline;white-space:nowrap}.tox .tox-statusbar__wordcount{flex:0 0 auto;margin-left:1ch}@media only screen and (max-width:767px){.tox .tox-statusbar__text-container .tox-statusbar__help-text{display:none}.tox .tox-statusbar__text-container .tox-statusbar__help-text:only-child{display:block}}.tox .tox-statusbar a,.tox .tox-statusbar__path-item,.tox .tox-statusbar__wordcount{color:rgba(34,47,62,.7);position:relative;text-decoration:none}.tox .tox-statusbar a:focus:not(:disabled):not([aria-disabled=true]),.tox .tox-statusbar a:hover:not(:disabled):not([aria-disabled=true]),.tox .tox-statusbar__path-item:focus:not(:disabled):not([aria-disabled=true]),.tox .tox-statusbar__path-item:hover:not(:disabled):not([aria-disabled=true]),.tox .tox-statusbar__wordcount:focus:not(:disabled):not([aria-disabled=true]),.tox .tox-statusbar__wordcount:hover:not(:disabled):not([aria-disabled=true]){color:#222f3e;cursor:pointer}.tox .tox-statusbar a:focus-visible::after,.tox .tox-statusbar__path-item:focus-visible::after,.tox .tox-statusbar__wordcount:focus-visible::after{border-radius:3px;bottom:0;box-shadow:0 0 0 0 transparent;content:'';left:0;position:absolute;right:0;top:0}@media (forced-colors:active){.tox .tox-statusbar a:focus-visible::after,.tox .tox-statusbar__path-item:focus-visible::after,.tox .tox-statusbar__wordcount:focus-visible::after{border:2px solid highlight}}.tox .tox-statusbar__branding svg{fill:rgba(34,47,62,.8);height:1em;margin-left:.3em;width:auto}@media (forced-colors:active){.tox .tox-statusbar__branding svg{fill:currentColor}}.tox .tox-statusbar__branding a{align-items:center;display:inline-flex}.tox .tox-statusbar__branding a:focus:not(:disabled):not([aria-disabled=true]) svg,.tox .tox-statusbar__branding a:hover:not(:disabled):not([aria-disabled=true]) svg{fill:#222f3e}.tox .tox-statusbar__resize-handle{align-items:flex-end;align-self:stretch;cursor:nwse-resize;display:flex;flex:0 0 auto;justify-content:flex-end;margin-bottom:3px;margin-left:4px;margin-right:calc(3px - 8px);margin-top:3px;padding-bottom:0;padding-left:0;padding-right:0;position:relative}.tox .tox-statusbar__resize-handle svg{display:block;fill:rgba(34,47,62,.5)}.tox .tox-statusbar__resize-handle:focus svg,.tox .tox-statusbar__resize-handle:hover svg{fill:#222f3e}.tox .tox-statusbar__resize-handle:focus-visible{background-color:transparent;border-radius:1px 1px -4px 1px;box-shadow:0 0 0 2px transparent}.tox .tox-statusbar__resize-handle:focus-visible::after{border-radius:3px;bottom:0;box-shadow:0 0 0 0 transparent;content:'';left:0;position:absolute;right:0;top:0}@media (forced-colors:active){.tox .tox-statusbar__resize-handle:focus-visible::after{border:2px solid highlight}}.tox:not([dir=rtl]) .tox-statusbar__path>*{margin-right:4px}.tox:not([dir=rtl]) .tox-statusbar__branding{margin-left:2ch}.tox[dir=rtl] .tox-statusbar{flex-direction:row-reverse}.tox[dir=rtl] .tox-statusbar__path>*{margin-left:4px}.tox[dir=rtl] .tox-statusbar__branding svg{margin-left:0;margin-right:.3em}.tox .tox-throbber{z-index:1299}.tox .tox-throbber__busy-spinner{align-items:center;background-color:rgba(255,255,255,.6);bottom:0;display:flex;justify-content:center;left:0;position:absolute;right:0;top:0}.tox .tox-tbtn{align-items:center;background:#fff;border:0;border-radius:3px;box-shadow:none;color:#222f3e;display:flex;flex:0 0 auto;font-size:14px;font-style:normal;font-weight:400;height:34px;justify-content:center;margin:3px 0 2px 0;outline:0;padding:0;text-transform:none;width:34px}@media (forced-colors:active){.tox .tox-tbtn.tox-tbtn:hover,.tox .tox-tbtn:hover{outline:1px dashed currentColor}.tox .tox-tbtn.tox-tbtn--active,.tox .tox-tbtn.tox-tbtn--enabled,.tox .tox-tbtn.tox-tbtn--enabled:focus,.tox .tox-tbtn.tox-tbtn--enabled:hover,.tox .tox-tbtn:focus:not(.tox-tbtn--disabled){outline:1px solid currentColor;position:relative}}.tox .tox-tbtn svg{display:block;fill:#222f3e}@media (forced-colors:active){.tox .tox-tbtn svg{fill:currentColor!important}.tox .tox-tbtn svg.tox-tbtn--enabled,.tox .tox-tbtn svg:focus:not(.tox-tbtn--disabled){fill:currentColor!important}.tox .tox-tbtn svg .tox-tbtn:disabled,.tox .tox-tbtn svg .tox-tbtn:disabled:hover,.tox .tox-tbtn svg.tox-tbtn--disabled,.tox .tox-tbtn svg.tox-tbtn--disabled:hover{filter:contrast(0)}}.tox .tox-tbtn.tox-tbtn-more{padding-left:5px;padding-right:5px;width:inherit}.tox .tox-tbtn:focus{background:#dee0e2;border:0;box-shadow:none;position:relative;z-index:1}.tox .tox-tbtn:focus::after{border-radius:3px;bottom:0;box-shadow:0 0 0 0 transparent;content:'';left:0;position:absolute;right:0;top:0}@media (forced-colors:active){.tox .tox-tbtn:focus::after{border:2px solid highlight}}.tox .tox-tbtn:hover{background:#dee0e2;border:0;box-shadow:none;color:#222f3e}.tox .tox-tbtn:hover svg{fill:#222f3e}.tox .tox-tbtn:active{background:#c8cbcf;border:0;box-shadow:none;color:#222f3e}.tox .tox-tbtn:active svg{fill:#222f3e}.tox .tox-tbtn--disabled .tox-tbtn--enabled svg{fill:rgba(34,47,62,.5)}.tox .tox-tbtn--disabled,.tox .tox-tbtn--disabled:hover,.tox .tox-tbtn:disabled,.tox .tox-tbtn:disabled:hover{background:#fff;border:0;box-shadow:none;color:rgba(34,47,62,.5);cursor:not-allowed}.tox .tox-tbtn--disabled svg,.tox .tox-tbtn--disabled:hover svg,.tox .tox-tbtn:disabled svg,.tox .tox-tbtn:disabled:hover svg{fill:rgba(34,47,62,.5)}.tox .tox-tbtn--active,.tox .tox-tbtn--enabled,.tox .tox-tbtn--enabled:focus,.tox .tox-tbtn--enabled:hover{background:#c8cbcf;border:0;box-shadow:none;color:#222f3e;position:relative}.tox .tox-tbtn--active>*,.tox .tox-tbtn--enabled:focus>*,.tox .tox-tbtn--enabled:hover>*,.tox .tox-tbtn--enabled>*{transform:none}.tox .tox-tbtn--active svg,.tox .tox-tbtn--enabled svg,.tox .tox-tbtn--enabled:focus svg,.tox .tox-tbtn--enabled:hover svg{fill:#222f3e}.tox .tox-tbtn--active.tox-tbtn--disabled svg,.tox .tox-tbtn--enabled.tox-tbtn--disabled svg,.tox .tox-tbtn--enabled:focus.tox-tbtn--disabled svg,.tox .tox-tbtn--enabled:hover.tox-tbtn--disabled svg{fill:rgba(34,47,62,.5)}.tox .tox-tbtn--enabled:focus::after{border-radius:3px;bottom:0;box-shadow:0 0 0 0 transparent;content:'';left:0;position:absolute;right:0;top:0}@media (forced-colors:active){.tox .tox-tbtn--enabled:focus::after{border:2px solid highlight}}.tox .tox-tbtn:focus:not(.tox-tbtn--disabled){color:#222f3e}.tox .tox-tbtn:focus:not(.tox-tbtn--disabled) svg{fill:#222f3e}.tox .tox-tbtn:active>*{transform:none}.tox .tox-tbtn--md{height:51px;width:51px}.tox .tox-tbtn--lg{flex-direction:column;height:68px;width:68px}.tox .tox-tbtn--return{align-self:stretch;height:unset;width:16px}.tox .tox-tbtn--labeled{padding:0 4px;width:unset}.tox .tox-tbtn__vlabel{display:block;font-size:10px;font-weight:400;letter-spacing:-.025em;margin-bottom:4px;white-space:nowrap}.tox .tox-number-input{background:0 0;border-radius:3px;display:flex;margin:3px 0 2px 0;position:relative;width:auto}.tox .tox-number-input:focus{background:#dee0e2}.tox .tox-number-input:focus::after{border-radius:3px;bottom:0;box-shadow:0 0 0 0 transparent;content:'';left:0;position:absolute;right:0;top:0}@media (forced-colors:active){.tox .tox-number-input:focus::after{border:2px solid highlight}}.tox .tox-number-input .tox-input-wrapper{display:flex;pointer-events:none;position:relative;text-align:center}.tox .tox-number-input .tox-input-wrapper:focus{background-color:#dee0e2;z-index:1}.tox .tox-number-input .tox-input-wrapper:focus::after{border-radius:3px;bottom:0;box-shadow:0 0 0 0 transparent;content:'';left:0;position:absolute;right:0;top:0}@media (forced-colors:active){.tox .tox-number-input .tox-input-wrapper:focus::after{border:2px solid highlight}}.tox .tox-number-input .tox-input-wrapper:has(input:focus)::after{border-radius:3px;bottom:0;box-shadow:0 0 0 0 transparent;content:'';left:0;position:absolute;right:0;top:0}@media (forced-colors:active){.tox .tox-number-input .tox-input-wrapper:has(input:focus)::after{border:2px solid highlight}}.tox .tox-number-input input{border-radius:3px;color:#222f3e;font-size:14px;margin:2px 0;pointer-events:all;position:relative;width:60px}.tox .tox-number-input input:hover{background:#dee0e2;color:#222f3e}.tox .tox-number-input input:focus{background-color:#dee0e2}.tox .tox-number-input input:disabled{background:#fff;border:0;box-shadow:none;color:rgba(34,47,62,.5);cursor:not-allowed}.tox .tox-number-input button{color:#222f3e;height:34px;position:relative;text-align:center;width:24px}@media (forced-colors:active){.tox .tox-number-input button:active,.tox .tox-number-input button:focus,.tox .tox-number-input button:hover{outline:1px solid currentColor!important}}.tox .tox-number-input button svg{display:block;fill:#222f3e;margin:0 auto;transform:scale(.67)}@media (forced-colors:active){.tox .tox-number-input button svg,.tox .tox-number-input button svg:active,.tox .tox-number-input button svg:hover{fill:currentColor!important}.tox .tox-number-input button svg:disabled{filter:contrast(0)}}.tox .tox-number-input button:focus{background:#dee0e2;z-index:1}.tox .tox-number-input button:focus::after{border-radius:3px;bottom:0;box-shadow:0 0 0 0 transparent;content:'';left:0;position:absolute;right:0;top:0}@media (forced-colors:active){.tox .tox-number-input button:focus::after{border:2px solid highlight}}.tox .tox-number-input button:hover{background:#dee0e2;border:0;box-shadow:none;color:#222f3e}.tox .tox-number-input button:hover svg{fill:#222f3e}.tox .tox-number-input button:active{background:#c8cbcf;border:0;box-shadow:none;color:#222f3e}.tox .tox-number-input button:active svg{fill:#222f3e}.tox .tox-number-input button:disabled{background:#fff;border:0;box-shadow:none;color:rgba(34,47,62,.5);cursor:not-allowed}.tox .tox-number-input button:disabled svg{fill:rgba(34,47,62,.5)}.tox .tox-number-input button.minus{border-radius:3px 0 0 3px}.tox .tox-number-input button.plus{border-radius:0 3px 3px 0}.tox .tox-number-input:focus:not(:active)>.tox-input-wrapper,.tox .tox-number-input:focus:not(:active)>button{background:#dee0e2}.tox .tox-tbtn--select{margin:3px 0 2px 0;padding:0 4px;width:auto}.tox .tox-tbtn__select-label{cursor:default;font-weight:400;height:initial;margin:0 4px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.tox .tox-tbtn__select-chevron{align-items:center;display:flex;justify-content:center;width:16px}.tox .tox-tbtn__select-chevron svg{fill:rgba(34,47,62,.5)}@media (forced-colors:active){.tox .tox-tbtn__select-chevron svg{fill:currentColor}}.tox .tox-tbtn--bespoke{background:0 0}.tox .tox-tbtn--bespoke:focus{background:#dee0e2}.tox .tox-tbtn--bespoke+.tox-tbtn--bespoke{margin-inline-start:0}.tox .tox-tbtn--bespoke .tox-tbtn__select-label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;width:7em}.tox .tox-tbtn--disabled .tox-tbtn__select-label,.tox .tox-tbtn--select:disabled .tox-tbtn__select-label{cursor:not-allowed}.tox .tox-split-button{border:0;border-radius:3px;box-sizing:border-box;display:flex;margin:3px 0 2px 0}.tox .tox-split-button:hover{box-shadow:0 0 0 1px #dee0e2 inset}.tox .tox-split-button:focus{background:#dee0e2;box-shadow:none;color:#222f3e;position:relative;z-index:1}.tox .tox-split-button:focus::after{pointer-events:none;border-radius:3px;bottom:0;box-shadow:0 0 0 0 transparent;content:'';left:0;position:absolute;right:0;top:0}@media (forced-colors:active){.tox .tox-split-button:focus::after{border:2px solid highlight}}.tox .tox-split-button>*{border-radius:0}.tox .tox-split-button>:nth-child(1){border-bottom-left-radius:3px;border-top-left-radius:3px}.tox .tox-split-button>:nth-child(2){border-bottom-right-radius:3px;border-top-right-radius:3px}.tox .tox-split-button__chevron{width:16px}.tox .tox-split-button__chevron svg{fill:rgba(34,47,62,.5)}@media (forced-colors:active){.tox .tox-split-button__chevron svg{fill:currentColor}}.tox .tox-split-button .tox-tbtn{margin:0}.tox .tox-split-button:focus .tox-tbtn{background-color:transparent}.tox .tox-split-button.tox-tbtn--disabled .tox-tbtn:focus,.tox .tox-split-button.tox-tbtn--disabled .tox-tbtn:hover,.tox .tox-split-button.tox-tbtn--disabled:focus,.tox .tox-split-button.tox-tbtn--disabled:hover{background:#fff;box-shadow:none;color:rgba(34,47,62,.5)}.tox.tox-platform-touch .tox-split-button .tox-tbtn--select{padding:0 0}.tox.tox-platform-touch .tox-split-button .tox-tbtn:not(.tox-tbtn--select):first-child{width:30px}.tox.tox-platform-touch .tox-split-button__chevron{width:20px}.tox .tox-split-button.tox-tbtn--disabled svg #tox-icon-highlight-bg-color__color,.tox .tox-split-button.tox-tbtn--disabled svg #tox-icon-text-color__color{opacity:.6}.tox .tox-toolbar-overlord{background-color:#fff}.tox .tox-toolbar,.tox .tox-toolbar__overflow,.tox .tox-toolbar__primary{background-attachment:local;background-color:#fff;background-image:repeating-linear-gradient(#ccc 0 1px,transparent 1px 39px);background-position:center top 39px;background-repeat:no-repeat;background-size:calc(100% - 4px * 2) calc(100% - 39px);display:flex;flex:0 0 auto;flex-shrink:0;flex-wrap:wrap;padding:0 0;transform:perspective(1px)}.tox .tox-toolbar-overlord>.tox-toolbar,.tox .tox-toolbar-overlord>.tox-toolbar__overflow,.tox .tox-toolbar-overlord>.tox-toolbar__primary{background-position:center top 0;background-size:calc(100% - 4px * 2) calc(100% - 0px)}.tox .tox-toolbar__overflow.tox-toolbar__overflow--closed{height:0;opacity:0;padding-bottom:0;padding-top:0;visibility:hidden}.tox .tox-toolbar__overflow--growing{transition:height .3s ease,opacity .2s linear .1s}.tox .tox-toolbar__overflow--shrinking{transition:opacity .3s ease,height .2s linear .1s,visibility 0s linear .3s}.tox .tox-anchorbar,.tox .tox-toolbar-overlord{grid-column:1/-1}.tox .tox-menubar+.tox-toolbar,.tox .tox-menubar+.tox-toolbar-overlord{border-top:1px solid #ccc;margin-top:-1px;padding-bottom:0;padding-top:0}@media (forced-colors:active){.tox .tox-menubar+.tox-toolbar,.tox .tox-menubar+.tox-toolbar-overlord{outline:1px solid currentColor}}.tox .tox-toolbar--scrolling{flex-wrap:nowrap;overflow-x:auto}.tox .tox-pop .tox-toolbar{border-width:0}.tox .tox-toolbar--no-divider{background-image:none}.tox .tox-toolbar-overlord .tox-toolbar:not(.tox-toolbar--scrolling):first-child,.tox .tox-toolbar-overlord .tox-toolbar__primary{background-position:center top 39px}.tox .tox-editor-header>.tox-toolbar--scrolling,.tox .tox-toolbar-overlord .tox-toolbar--scrolling:first-child{background-image:none}.tox.tox-tinymce-aux .tox-toolbar__overflow{background-color:#fff;background-position:center top 43px;background-size:calc(100% - 8px * 2) calc(100% - 51px);border:none;border-radius:3px;box-shadow:0 0 2px 0 rgba(34,47,62,.2),0 4px 8px 0 rgba(34,47,62,.15);overscroll-behavior:none;padding:4px 0}@media (forced-colors:active){.tox.tox-tinymce-aux .tox-toolbar__overflow{border:solid}}.tox-pop .tox-pop__dialog .tox-toolbar{background-position:center top 43px;background-size:calc(100% - 4px * 2) calc(100% - 51px);padding:4px 0}.tox .tox-toolbar__group{align-items:center;display:flex;flex-wrap:wrap;margin:0 0;padding:0 4px 0 4px}.tox .tox-toolbar__group--pull-right{margin-left:auto}.tox .tox-toolbar--scrolling .tox-toolbar__group{flex-shrink:0;flex-wrap:nowrap}.tox:not([dir=rtl]) .tox-toolbar__group:not(:last-of-type){border-right:1px solid #ccc}.tox[dir=rtl] .tox-toolbar__group:not(:last-of-type){border-left:1px solid #ccc}.tox .tox-tooltip{display:inline-block;max-width:15em;padding:8px;pointer-events:none;position:relative;width:-moz-max-content;width:max-content;z-index:1150}.tox .tox-tooltip__body{background-color:#222f3e;border-radius:3px;box-shadow:none;color:#fff;font-size:12px;font-style:normal;font-weight:600;overflow-wrap:break-word;padding:4px 6px;text-transform:none}@media (forced-colors:active){.tox .tox-tooltip__body{outline:outset 1px}}.tox .tox-tooltip__arrow{position:absolute}.tox .tox-tooltip--down .tox-tooltip__arrow{border-left:8px solid transparent;border-right:8px solid transparent;border-top:8px solid #222f3e;bottom:0;left:50%;position:absolute;transform:translateX(-50%)}.tox .tox-tooltip--up .tox-tooltip__arrow{border-bottom:8px solid #222f3e;border-left:8px solid transparent;border-right:8px solid transparent;left:50%;position:absolute;top:0;transform:translateX(-50%)}.tox .tox-tooltip--right .tox-tooltip__arrow{border-bottom:8px solid transparent;border-left:8px solid #222f3e;border-top:8px solid transparent;position:absolute;right:0;top:50%;transform:translateY(-50%)}.tox .tox-tooltip--left .tox-tooltip__arrow{border-bottom:8px solid transparent;border-right:8px solid #222f3e;border-top:8px solid transparent;left:0;position:absolute;top:50%;transform:translateY(-50%)}.tox .tox-tree{display:flex;flex-direction:column}.tox .tox-tree .tox-trbtn{align-items:center;background:0 0;border:0;border-radius:4px;box-shadow:none;color:#222f3e;display:flex;flex:0 0 auto;font-size:14px;font-style:normal;font-weight:400;height:28px;margin-bottom:4px;margin-top:4px;outline:0;overflow:hidden;padding:0;padding-left:8px;text-transform:none}.tox .tox-tree .tox-trbtn .tox-tree__label{cursor:default;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.tox .tox-tree .tox-trbtn svg{display:block;fill:#222f3e}.tox .tox-tree .tox-trbtn:focus{background:#dee0e2;border:0;box-shadow:none}.tox .tox-tree .tox-trbtn:hover{background:#dee0e2;border:0;box-shadow:none;color:#222f3e}.tox .tox-tree .tox-trbtn:hover svg{fill:#222f3e}.tox .tox-tree .tox-trbtn:active{background:#b1d0e6;border:0;box-shadow:none;color:#222f3e}.tox .tox-tree .tox-trbtn:active svg{fill:#222f3e}.tox .tox-tree .tox-trbtn--disabled,.tox .tox-tree .tox-trbtn--disabled:hover,.tox .tox-tree .tox-trbtn:disabled,.tox .tox-tree .tox-trbtn:disabled:hover{background:0 0;border:0;box-shadow:none;color:rgba(34,47,62,.5);cursor:not-allowed}.tox .tox-tree .tox-trbtn--disabled svg,.tox .tox-tree .tox-trbtn--disabled:hover svg,.tox .tox-tree .tox-trbtn:disabled svg,.tox .tox-tree .tox-trbtn:disabled:hover svg{fill:rgba(34,47,62,.5)}.tox .tox-tree .tox-trbtn--enabled,.tox .tox-tree .tox-trbtn--enabled:hover{background:#b1d0e6;border:0;box-shadow:none;color:#222f3e}.tox .tox-tree .tox-trbtn--enabled:hover>*,.tox .tox-tree .tox-trbtn--enabled>*{transform:none}.tox .tox-tree .tox-trbtn--enabled svg,.tox .tox-tree .tox-trbtn--enabled:hover svg{fill:#222f3e}.tox .tox-tree .tox-trbtn:focus:not(.tox-trbtn--disabled){color:#222f3e}.tox .tox-tree .tox-trbtn:focus:not(.tox-trbtn--disabled) svg{fill:#222f3e}.tox .tox-tree .tox-trbtn:active>*{transform:none}.tox .tox-tree .tox-trbtn--return{align-self:stretch;height:unset;width:16px}.tox .tox-tree .tox-trbtn--labeled{padding:0 4px;width:unset}.tox .tox-tree .tox-trbtn__vlabel{display:block;font-size:10px;font-weight:400;letter-spacing:-.025em;margin-bottom:4px;white-space:nowrap}.tox .tox-tree .tox-tree--directory{display:flex;flex-direction:column}.tox .tox-tree .tox-tree--directory .tox-tree--directory__label{font-weight:700}.tox .tox-tree .tox-tree--directory .tox-tree--directory__label .tox-mbtn{margin-left:auto}.tox .tox-tree .tox-tree--directory .tox-tree--directory__label .tox-mbtn svg{fill:transparent}.tox .tox-tree .tox-tree--directory .tox-tree--directory__label .tox-mbtn.tox-mbtn--active svg,.tox .tox-tree .tox-tree--directory .tox-tree--directory__label .tox-mbtn:focus svg{fill:#222f3e}.tox .tox-tree .tox-tree--directory .tox-tree--directory__label:focus .tox-mbtn svg,.tox .tox-tree .tox-tree--directory .tox-tree--directory__label:hover .tox-mbtn svg{fill:#222f3e}.tox .tox-tree .tox-tree--directory .tox-tree--directory__label:hover:has(.tox-mbtn:hover){background-color:transparent;color:#222f3e}.tox .tox-tree .tox-tree--directory .tox-tree--directory__label:hover:has(.tox-mbtn:hover) .tox-chevron svg{fill:#222f3e}.tox .tox-tree .tox-tree--directory .tox-tree--directory__label .tox-chevron{margin-right:6px}.tox .tox-tree .tox-tree--directory .tox-tree--directory__label:has(+.tox-tree--directory__children--growing) .tox-chevron,.tox .tox-tree .tox-tree--directory .tox-tree--directory__label:has(+.tox-tree--directory__children--shrinking) .tox-chevron{transition:transform .5s ease-in-out}.tox .tox-tree .tox-tree--directory .tox-tree--directory__label:has(+.tox-tree--directory__children--growing) .tox-chevron,.tox .tox-tree .tox-tree--directory .tox-tree--directory__label:has(+.tox-tree--directory__children--open) .tox-chevron{transform:rotate(90deg)}.tox .tox-tree .tox-tree--leaf__label{font-weight:400}.tox .tox-tree .tox-tree--leaf__label .tox-mbtn{margin-left:auto}.tox .tox-tree .tox-tree--leaf__label .tox-mbtn svg{fill:transparent}.tox .tox-tree .tox-tree--leaf__label .tox-mbtn.tox-mbtn--active svg,.tox .tox-tree .tox-tree--leaf__label .tox-mbtn:focus svg{fill:#222f3e}.tox .tox-tree .tox-tree--leaf__label:hover .tox-mbtn svg{fill:#222f3e}.tox .tox-tree .tox-tree--leaf__label:hover:has(.tox-mbtn:hover){background-color:transparent;color:#222f3e}.tox .tox-tree .tox-tree--leaf__label:hover:has(.tox-mbtn:hover) .tox-chevron svg{fill:#222f3e}.tox .tox-tree .tox-tree--directory__children{overflow:hidden;padding-left:16px}.tox .tox-tree .tox-tree--directory__children.tox-tree--directory__children--growing,.tox .tox-tree .tox-tree--directory__children.tox-tree--directory__children--shrinking{transition:height .5s ease-in-out}.tox .tox-tree .tox-trbtn.tox-tree--leaf__label{display:flex;justify-content:space-between}.tox .tox-revisionhistory__pane{padding:0!important}.tox .tox-revisionhistory__container{display:flex;flex-direction:column;height:100%}.tox .tox-revisionhistory{background-color:#fff;border-radius:4px;border-top:1px solid #ccc;display:flex;flex:1;height:100%;margin-top:8px;overflow-x:auto;overflow-y:hidden;position:relative;width:100%}.tox .tox-revisionhistory--align-right{margin-left:auto}.tox .tox-revisionhistory__iframe{flex:1}.tox .tox-revisionhistory__sidebar{border-left:1px solid #ccc;height:100%;max-width:360px}.tox .tox-revisionhistory__sidebar .tox-revisionhistory__sidebar-title{border-bottom:1px solid #ccc;color:#222f3e;font-size:20px;font-weight:400;height:60px;min-width:192px;padding:16px}.tox .tox-revisionhistory__sidebar .tox-revisionhistory__revisions{flex-direction:column;max-height:calc(100% - 60px);min-width:192px;overflow-y:auto;padding:8px}.tox .tox-revisionhistory__sidebar .tox-revisionhistory__revisions:focus{height:100%;position:relative;z-index:1}.tox .tox-revisionhistory__sidebar .tox-revisionhistory__revisions:focus::after{bottom:0;box-shadow:0 0 0 0 transparent;content:'';left:0;position:absolute;right:0;top:0;border-radius:3px;bottom:1px;left:1px;right:1px;top:1px}@media (forced-colors:active){.tox .tox-revisionhistory__sidebar .tox-revisionhistory__revisions:focus::after{border:2px solid highlight}}.tox .tox-revisionhistory__sidebar .tox-revisionhistory__revisions .tox-revisionhistory__card{border:1px solid #ccc;border-radius:3px;color:#222f3e;cursor:pointer;font-size:14px;margin-bottom:8px;padding:8px;text-overflow:ellipsis;text-wrap:nowrap;width:100%}.tox .tox-revisionhistory__sidebar .tox-revisionhistory__revisions .tox-revisionhistory__card:hover{background-color:#dee0e2;box-shadow:none;color:#222f3e}.tox .tox-revisionhistory__sidebar .tox-revisionhistory__revisions .tox-revisionhistory__card:focus{position:relative;z-index:1}.tox .tox-revisionhistory__sidebar .tox-revisionhistory__revisions .tox-revisionhistory__card:focus::after{border-radius:3px!important;border-radius:3px;bottom:0;box-shadow:0 0 0 0 transparent;content:'';left:0;position:absolute;right:0;top:0}@media (forced-colors:active){.tox .tox-revisionhistory__sidebar .tox-revisionhistory__revisions .tox-revisionhistory__card:focus::after{border:2px solid highlight}}.tox .tox-revisionhistory__sidebar .tox-revisionhistory__revisions .tox-revisionhistory__card.tox-revisionhistory__card--selected{background-color:#b1d0e6;box-shadow:none;color:#222f3e}.tox .tox-revisionhistory__sidebar .tox-revisionhistory__revisions .tox-revisionhistory__norevision{color:rgba(34,47,62,.7);font-size:16px;line-height:24px;padding:5px 5.5px}.tox .tox-view-wrap,.tox .tox-view-wrap__slot-container{background-color:#fff;display:flex;flex:1;flex-direction:column;height:100%}.tox .tox-view{display:flex;flex:1 1 auto;flex-direction:column;overflow:hidden}.tox .tox-view__header{align-items:center;display:flex;font-size:16px;justify-content:space-between;padding:8px 8px 0 8px;position:relative}.tox .tox-view__label{color:#222f3e;font-weight:700;line-height:24px;padding:4px 16px;text-align:center;white-space:nowrap}.tox .tox-view__label--normal{font-size:16px}.tox .tox-view__label--large{font-size:20px}.tox .tox-view--mobile.tox-view__header,.tox .tox-view--mobile.tox-view__toolbar{padding:8px}.tox .tox-view--scrolling{flex-wrap:nowrap;overflow-x:auto}.tox .tox-view__toolbar{display:flex;flex-direction:row;gap:8px;justify-content:space-between;overflow-x:auto;padding:8px 8px 0 8px}.tox .tox-view__toolbar__group{display:flex;flex-direction:row;gap:12px}.tox .tox-view__header-end,.tox .tox-view__header-start{display:flex}.tox .tox-view__pane{height:100%;padding:8px;position:relative;width:100%}.tox .tox-view__pane_panel{border:1px solid #ccc;border-radius:3px}.tox:not([dir=rtl]) .tox-view__header .tox-view__header-end>*,.tox:not([dir=rtl]) .tox-view__header .tox-view__header-start>*{margin-left:8px}.tox[dir=rtl] .tox-view__header .tox-view__header-end>*,.tox[dir=rtl] .tox-view__header .tox-view__header-start>*{margin-right:8px}.tox .tox-well{border:1px solid #ccc;border-radius:3px;padding:8px;width:100%}.tox .tox-well>:first-child{margin-top:0}.tox .tox-well>:last-child{margin-bottom:0}.tox .tox-well>:only-child{margin:0}.tox .tox-custom-editor{border:1px solid #ccc;border-radius:3px;display:flex;flex:1;overflow:hidden;position:relative}.tox .tox-dialog-loading::before{background-color:rgba(0,0,0,.5);content:\\\"\\\";height:100%;position:absolute;width:100%;z-index:1000}.tox .tox-tab{cursor:pointer}.tox .tox-dialog__content-js{display:flex;flex:1}.tox .tox-dialog__body-content .tox-collection{display:flex;flex:1}.tox:not(.tox-tinymce-inline) .tox-editor-header{background-color:none;padding:0}.tox.tox-tinymce--toolbar-bottom .tox-editor-header,.tox.tox-tinymce-inline .tox-editor-header{margin-bottom:-1px}.tox.tox-tinymce-inline .tox-editor-container{overflow:hidden}.tox:not(.tox-tinymce-inline).tox-tinymce--toolbar-bottom .tox-editor-header{border-top:none;box-shadow:none}.tox.tox.tox-tinymce--toolbar-sticky-on .tox-editor-header{background-color:transparent;box-shadow:0 4px 4px -3px rgba(0,0,0,.25);padding:0}.tox.tox.tox-tinymce--toolbar-sticky-on.tox-tinymce--toolbar-bottom .tox-editor-header{box-shadow:0 4px 4px -3px rgba(0,0,0,.25)}.tox .tox-collection--list .tox-collection__group .tox-insert-table-picker{margin:-4px 0}.tox .tox-menu.tox-collection.tox-collection--list{padding:0}.tox .tox-pop{box-shadow:none}.tox .tox-number-input,.tox .tox-split-button,.tox .tox-tbtn,.tox .tox-tbtn--select{margin:2px 0 3px 0}.tox .tox-toolbar,.tox .tox-toolbar__overflow,.tox .tox-toolbar__primary{background:url(\\\"data:image/svg+xml;charset=utf8,%3Csvg height='39px' viewBox='0 0 40 39px' width='40' xmlns='http://www.w3.org/2000/svg'%3E%3Crect x='0' y='38px' width='100' height='1' fill='%23cccccc'/%3E%3C/svg%3E\\\") left 0 top 0 #fff!important}.tox .tox-menubar+.tox-toolbar-overlord{border-top:none}.tox .tox-menubar+.tox-toolbar,.tox .tox-menubar+.tox-toolbar-overlord .tox-toolbar__primary{border-top:1px solid #ccc;margin-top:-1px}.tox.tox-tinymce-aux .tox-toolbar__overflow{border:1px solid #ccc;padding:0}.tox .tox-pop .tox-pop__dialog .tox-toolbar{padding:0}.tox:not(.tox-tinymce-inline) .tox-editor-header:not(:first-child) .tox-menubar{border-top:1px solid #ccc}.tox:not(.tox-tinymce-inline) .tox-editor-header:not(:first-child) .tox-toolbar-overlord:first-child .tox-toolbar__primary,.tox:not(.tox-tinymce-inline) .tox-editor-header:not(:first-child) .tox-toolbar:first-child{border-top:1px solid #ccc}.tox .tox-toolbar__group{padding:0 4px 0 4px}.tox .tox-collection__item{border-radius:0;cursor:pointer}.tox .tox-statusbar a:focus:not(:disabled):not([aria-disabled=true]),.tox .tox-statusbar a:hover:not(:disabled):not([aria-disabled=true]),.tox .tox-statusbar__path-item:focus:not(:disabled):not([aria-disabled=true]),.tox .tox-statusbar__path-item:hover:not(:disabled):not([aria-disabled=true]),.tox .tox-statusbar__wordcount:focus:not(:disabled):not([aria-disabled=true]),.tox .tox-statusbar__wordcount:hover:not(:disabled):not([aria-disabled=true]){color:#222f3e}.tox .tox-statusbar__branding svg{fill:rgba(34,47,62,.8);height:1em;margin-left:.3em;width:auto}@media (forced-colors:active){.tox .tox-statusbar__branding svg{fill:currentColor}}.tox .tox-statusbar__branding a{align-items:center;display:inline-flex}.tox .tox-statusbar__branding a:focus:not(:disabled):not([aria-disabled=true]) svg,.tox .tox-statusbar__branding a:hover:not(:disabled):not([aria-disabled=true]) svg{fill:#222f3e}.tox:not([dir=rtl]) .tox-statusbar__branding{margin-left:1ch}.tox[dir=rtl] .tox-statusbar__branding svg{margin-left:0;margin-right:.3em}.tox .tox-statusbar__resize-handle{padding-bottom:0;padding-right:0}.tox .tox-button::before{display:none}\")\n//# sourceMappingURL=skin.js.map\n"
  },
  {
    "path": "apps/web-antd/public/tinymce/skins/ui/tinymce-5/skin.shadowdom.js",
    "content": "tinymce.Resource.add('ui/tinymce-5/skin.shadowdom.css', \"body.tox-dialog__disable-scroll{overflow:hidden}.tox-fullscreen{border:0;height:100%;margin:0;overflow:hidden;overscroll-behavior:none;padding:0;touch-action:pinch-zoom;width:100%}.tox.tox-tinymce.tox-fullscreen .tox-statusbar__resize-handle{display:none}.tox-shadowhost.tox-fullscreen,.tox.tox-tinymce.tox-fullscreen{left:0;position:fixed;top:0;z-index:1200}.tox.tox-tinymce.tox-fullscreen{background-color:transparent}.tox-fullscreen .tox.tox-tinymce-aux,.tox-fullscreen~.tox.tox-tinymce-aux{z-index:1201}\")\n//# sourceMappingURL=skin.shadowdom.js.map\n"
  },
  {
    "path": "apps/web-antd/public/tinymce/skins/ui/tinymce-5-dark/content.inline.js",
    "content": "tinymce.Resource.add('ui/tinymce-5-dark/content.inline.css', \".mce-content-body .mce-item-anchor{background:transparent url(\\\"data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D'8'%20height%3D'12'%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%3E%3Cpath%20d%3D'M0%200L8%200%208%2012%204.09117821%209%200%2012z'%2F%3E%3C%2Fsvg%3E%0A\\\") no-repeat center}.mce-content-body .mce-item-anchor:empty{cursor:default;display:inline-block;height:12px!important;padding:0 2px;-webkit-user-modify:read-only;-moz-user-modify:read-only;-webkit-user-select:all;-moz-user-select:all;user-select:all;width:8px!important}.mce-content-body .mce-item-anchor:not(:empty){background-position-x:2px;display:inline-block;padding-left:12px}.mce-content-body .mce-item-anchor[data-mce-selected]{outline-offset:1px}.tox-comments-visible .tox-comment[contenteditable=false]:not([data-mce-selected]),.tox-comments-visible span.tox-comment img:not([data-mce-selected]),.tox-comments-visible span.tox-comment span.mce-preview-object:not([data-mce-selected]),.tox-comments-visible span.tox-comment>audio:not([data-mce-selected]),.tox-comments-visible span.tox-comment>video:not([data-mce-selected]){outline:3px solid #ffe89d}.tox-comments-visible .tox-comment[contenteditable=false][data-mce-annotation-active=true]:not([data-mce-selected]){outline:3px solid #fed635}.tox-comments-visible span.tox-comment[data-mce-annotation-active=true] img:not([data-mce-selected]),.tox-comments-visible span.tox-comment[data-mce-annotation-active=true] span.mce-preview-object:not([data-mce-selected]),.tox-comments-visible span.tox-comment[data-mce-annotation-active=true]>audio:not([data-mce-selected]),.tox-comments-visible span.tox-comment[data-mce-annotation-active=true]>video:not([data-mce-selected]){outline:3px solid #fed635}.tox-comments-visible span.tox-comment:not([data-mce-selected]){background-color:#ffe89d;outline:0}.tox-comments-visible span.tox-comment[data-mce-annotation-active=true]:not([data-mce-selected=inline-boundary]){background-color:#fed635}.tox-checklist>li:not(.tox-checklist--hidden){list-style:none;margin:.25em 0}.tox-checklist>li:not(.tox-checklist--hidden)::before{content:url(\\\"data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2216%22%20height%3D%2216%22%20viewBox%3D%220%200%2016%2016%22%3E%3Cg%20id%3D%22checklist-unchecked%22%20fill%3D%22none%22%20fill-rule%3D%22evenodd%22%3E%3Crect%20id%3D%22Rectangle%22%20width%3D%2215%22%20height%3D%2215%22%20x%3D%22.5%22%20y%3D%22.5%22%20fill-rule%3D%22nonzero%22%20stroke%3D%22%234C4C4C%22%20rx%3D%222%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E%0A\\\");cursor:pointer;height:1em;margin-left:-1.5em;margin-top:.125em;position:absolute;width:1em}.tox-checklist li:not(.tox-checklist--hidden).tox-checklist--checked::before{content:url(\\\"data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2216%22%20height%3D%2216%22%20viewBox%3D%220%200%2016%2016%22%3E%3Cg%20id%3D%22checklist-checked%22%20fill%3D%22none%22%20fill-rule%3D%22evenodd%22%3E%3Crect%20id%3D%22Rectangle%22%20width%3D%2216%22%20height%3D%2216%22%20fill%3D%22%234099FF%22%20fill-rule%3D%22nonzero%22%20rx%3D%222%22%2F%3E%3Cpath%20id%3D%22Path%22%20fill%3D%22%23FFF%22%20fill-rule%3D%22nonzero%22%20d%3D%22M11.5703186%2C3.14417309%20C11.8516238%2C2.73724603%2012.4164781%2C2.62829933%2012.83558%2C2.89774797%20C13.260121%2C3.17069355%2013.3759736%2C3.72932262%2013.0909105%2C4.14168582%20L7.7580587%2C11.8560195%20C7.43776896%2C12.3193404%206.76483983%2C12.3852142%206.35607322%2C11.9948725%20L3.02491697%2C8.8138662%20C2.66090143%2C8.46625845%202.65798871%2C7.89594698%203.01850234%2C7.54483354%20C3.373942%2C7.19866177%203.94940006%2C7.19592841%204.30829608%2C7.5386474%20L6.85276923%2C9.9684299%20L11.5703186%2C3.14417309%20Z%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E%0A\\\")}[dir=rtl] .tox-checklist>li:not(.tox-checklist--hidden)::before{margin-left:0;margin-right:-1.5em}code[class*=language-],pre[class*=language-]{color:#000;background:0 0;text-shadow:0 1px #fff;font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;tab-size:4;-webkit-hyphens:none;hyphens:none}code[class*=language-] ::-moz-selection,code[class*=language-]::-moz-selection,pre[class*=language-] ::-moz-selection,pre[class*=language-]::-moz-selection{text-shadow:none;background:#b3d4fc}code[class*=language-] ::selection,code[class*=language-]::selection,pre[class*=language-] ::selection,pre[class*=language-]::selection{text-shadow:none;background:#b3d4fc}@media print{code[class*=language-],pre[class*=language-]{text-shadow:none}}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto}:not(pre)>code[class*=language-],pre[class*=language-]{background:#f5f2f0}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#708090}.token.punctuation{color:#999}.token.namespace{opacity:.7}.token.boolean,.token.constant,.token.deleted,.token.number,.token.property,.token.symbol,.token.tag{color:#905}.token.attr-name,.token.builtin,.token.char,.token.inserted,.token.selector,.token.string{color:#690}.language-css .token.string,.style .token.string,.token.entity,.token.operator,.token.url{color:#9a6e3a;background:hsla(0,0%,100%,.5)}.token.atrule,.token.attr-value,.token.keyword{color:#07a}.token.class-name,.token.function{color:#dd4a68}.token.important,.token.regex,.token.variable{color:#e90}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}.mce-content-body{overflow-wrap:break-word;word-wrap:break-word}.mce-content-body .mce-visual-caret{background-color:#000;background-color:currentColor;position:absolute}.mce-content-body .mce-visual-caret-hidden{display:none}.mce-content-body [data-mce-caret]{left:-1000px;margin:0;padding:0;position:absolute;right:auto;top:0}.mce-content-body .mce-offscreen-selection{left:-2000000px;max-width:1000000px;position:absolute}.mce-content-body [contentEditable=false]{cursor:default}.mce-content-body [contentEditable=true]{cursor:text}.tox-cursor-format-painter{cursor:url(\\\"data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%20viewBox%3D%220%200%2024%2024%22%3E%0A%20%20%3Cg%20fill%3D%22none%22%20fill-rule%3D%22evenodd%22%3E%0A%20%20%20%20%3Cpath%20fill%3D%22%23000%22%20fill-rule%3D%22nonzero%22%20d%3D%22M15%2C6%20C15%2C5.45%2014.55%2C5%2014%2C5%20L6%2C5%20C5.45%2C5%205%2C5.45%205%2C6%20L5%2C10%20C5%2C10.55%205.45%2C11%206%2C11%20L14%2C11%20C14.55%2C11%2015%2C10.55%2015%2C10%20L15%2C9%20L16%2C9%20L16%2C12%20L9%2C12%20L9%2C19%20C9%2C19.55%209.45%2C20%2010%2C20%20L11%2C20%20C11.55%2C20%2012%2C19.55%2012%2C19%20L12%2C14%20L18%2C14%20L18%2C7%20L15%2C7%20L15%2C6%20Z%22%2F%3E%0A%20%20%20%20%3Cpath%20fill%3D%22%23000%22%20fill-rule%3D%22nonzero%22%20d%3D%22M1%2C1%20L8.25%2C1%20C8.66421356%2C1%209%2C1.33578644%209%2C1.75%20L9%2C1.75%20C9%2C2.16421356%208.66421356%2C2.5%208.25%2C2.5%20L2.5%2C2.5%20L2.5%2C8.25%20C2.5%2C8.66421356%202.16421356%2C9%201.75%2C9%20L1.75%2C9%20C1.33578644%2C9%201%2C8.66421356%201%2C8.25%20L1%2C1%20Z%22%2F%3E%0A%20%20%3C%2Fg%3E%0A%3C%2Fsvg%3E%0A\\\"),default}div.mce-footnotes hr{margin-inline-end:auto;margin-inline-start:0;width:25%}div.mce-footnotes li>a.mce-footnotes-backlink{text-decoration:none}@media print{sup.mce-footnote a{color:#000;text-decoration:none}div.mce-footnotes{break-inside:avoid;width:100%}div.mce-footnotes li>a.mce-footnotes-backlink{display:none}}tiny-math-block{display:flex;justify-content:center;margin:16px 0 16px 0}tiny-math-inline{display:inline-block}.mce-content-body figure.align-left{float:left}.mce-content-body figure.align-right{float:right}.mce-content-body figure.image.align-center{display:table;margin-left:auto;margin-right:auto}.mce-preview-object{border:1px solid gray;display:inline-block;line-height:0;margin:0 2px 0 2px;position:relative}.mce-preview-object .mce-shim{background:url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7);height:100%;left:0;position:absolute;top:0;width:100%}.mce-preview-object[data-mce-selected=\\\"2\\\"] .mce-shim{display:none}.mce-content-body .mce-mergetag{cursor:default!important;-webkit-user-select:none;-moz-user-select:none;user-select:none}.mce-content-body .mce-mergetag:hover{background-color:rgba(0,108,231,.1)}.mce-content-body .mce-mergetag-affix{background-color:rgba(0,108,231,.1);color:#006ce7}.mce-object{background:transparent url(\\\"data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%3E%3Cpath%20d%3D%22M4%203h16a1%201%200%200%201%201%201v16a1%201%200%200%201-1%201H4a1%201%200%200%201-1-1V4a1%201%200%200%201%201-1zm1%202v14h14V5H5zm4.79%202.565l5.64%204.028a.5.5%200%200%201%200%20.814l-5.64%204.028a.5.5%200%200%201-.79-.407V7.972a.5.5%200%200%201%20.79-.407z%22%2F%3E%3C%2Fsvg%3E%0A\\\") no-repeat center;border:1px dashed #aaa}.mce-pagebreak{border:1px dashed #aaa;cursor:default;display:block;height:5px;margin-top:15px;page-break-before:always;width:100%}@media print{.mce-pagebreak{border:0}}.tiny-pageembed .mce-shim{background:url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7);height:100%;left:0;position:absolute;top:0;width:100%}.tiny-pageembed[data-mce-selected=\\\"2\\\"] .mce-shim{display:none}.tiny-pageembed{display:inline-block;position:relative}.tiny-pageembed--16by9,.tiny-pageembed--1by1,.tiny-pageembed--21by9,.tiny-pageembed--4by3{display:block;overflow:hidden;padding:0;position:relative;width:100%}.tiny-pageembed--21by9{padding-top:42.857143%}.tiny-pageembed--16by9{padding-top:56.25%}.tiny-pageembed--4by3{padding-top:75%}.tiny-pageembed--1by1{padding-top:100%}.tiny-pageembed--16by9 iframe,.tiny-pageembed--1by1 iframe,.tiny-pageembed--21by9 iframe,.tiny-pageembed--4by3 iframe{border:0;height:100%;left:0;position:absolute;top:0;width:100%}.mce-content-body[data-mce-placeholder]{position:relative}.mce-content-body[data-mce-placeholder]:not(.mce-visualblocks)::before{color:rgba(34,47,62,.7);content:attr(data-mce-placeholder);position:absolute}@media (forced-colors:active){.mce-content-body[data-mce-placeholder]:not(.mce-visualblocks)::before{color:highlight;filter:brightness(30%);z-index:-1}}.mce-content-body:not([dir=rtl])[data-mce-placeholder]:not(.mce-visualblocks)::before{left:1px}.mce-content-body[dir=rtl][data-mce-placeholder]:not(.mce-visualblocks)::before{right:1px}.mce-content-body div.mce-resizehandle{background-color:#4099ff;border-color:#4099ff;border-style:solid;border-width:1px;box-sizing:border-box;height:10px;position:absolute;width:10px;z-index:1298}.mce-content-body div.mce-resizehandle:hover{background-color:#4099ff}.mce-content-body div.mce-resizehandle:nth-of-type(1){cursor:nwse-resize}.mce-content-body div.mce-resizehandle:nth-of-type(2){cursor:nesw-resize}.mce-content-body div.mce-resizehandle:nth-of-type(3){cursor:nwse-resize}.mce-content-body div.mce-resizehandle:nth-of-type(4){cursor:nesw-resize}.mce-content-body .mce-resize-backdrop{z-index:10000}.mce-content-body .mce-clonedresizable{cursor:default;opacity:.5;outline:1px dashed #000;position:absolute;z-index:10001}.mce-content-body .mce-clonedresizable.mce-resizetable-columns td,.mce-content-body .mce-clonedresizable.mce-resizetable-columns th{border:0}.mce-content-body .mce-resize-helper{background:#555;background:rgba(0,0,0,.75);border:1px;border-radius:3px;color:#fff;display:none;font-family:sans-serif;font-size:12px;line-height:14px;margin:5px 10px;padding:5px;position:absolute;white-space:nowrap;z-index:10002}.tox-rtc-user-selection{position:relative}.tox-rtc-user-cursor{bottom:0;cursor:default;position:absolute;top:0;width:2px}.tox-rtc-user-cursor::before{background-color:inherit;border-radius:50%;content:'';display:block;height:8px;position:absolute;right:-3px;top:-3px;width:8px}.tox-rtc-user-cursor:hover::after{background-color:inherit;border-radius:100px;box-sizing:border-box;color:#fff;content:attr(data-user);display:block;font-size:12px;font-weight:700;left:-5px;min-height:8px;min-width:8px;padding:0 12px;position:absolute;top:-11px;white-space:nowrap;z-index:1000}.tox-rtc-user-selection--1 .tox-rtc-user-cursor{background-color:#2dc26b}.tox-rtc-user-selection--2 .tox-rtc-user-cursor{background-color:#e03e2d}.tox-rtc-user-selection--3 .tox-rtc-user-cursor{background-color:#f1c40f}.tox-rtc-user-selection--4 .tox-rtc-user-cursor{background-color:#3598db}.tox-rtc-user-selection--5 .tox-rtc-user-cursor{background-color:#b96ad9}.tox-rtc-user-selection--6 .tox-rtc-user-cursor{background-color:#e67e23}.tox-rtc-user-selection--7 .tox-rtc-user-cursor{background-color:#aaa69d}.tox-rtc-user-selection--8 .tox-rtc-user-cursor{background-color:#f368e0}.tox-rtc-remote-image{background:#eaeaea url(\\\"data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%2236%22%20height%3D%2212%22%20viewBox%3D%220%200%2036%2012%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%3Ccircle%20cx%3D%226%22%20cy%3D%226%22%20r%3D%223%22%20fill%3D%22rgba(0%2C%200%2C%200%2C%20.2)%22%3E%0A%20%20%20%20%3Canimate%20attributeName%3D%22r%22%20values%3D%223%3B5%3B3%22%20calcMode%3D%22linear%22%20dur%3D%221s%22%20repeatCount%3D%22indefinite%22%20%2F%3E%0A%20%20%3C%2Fcircle%3E%0A%20%20%3Ccircle%20cx%3D%2218%22%20cy%3D%226%22%20r%3D%223%22%20fill%3D%22rgba(0%2C%200%2C%200%2C%20.2)%22%3E%0A%20%20%20%20%3Canimate%20attributeName%3D%22r%22%20values%3D%223%3B5%3B3%22%20calcMode%3D%22linear%22%20begin%3D%22.33s%22%20dur%3D%221s%22%20repeatCount%3D%22indefinite%22%20%2F%3E%0A%20%20%3C%2Fcircle%3E%0A%20%20%3Ccircle%20cx%3D%2230%22%20cy%3D%226%22%20r%3D%223%22%20fill%3D%22rgba(0%2C%200%2C%200%2C%20.2)%22%3E%0A%20%20%20%20%3Canimate%20attributeName%3D%22r%22%20values%3D%223%3B5%3B3%22%20calcMode%3D%22linear%22%20begin%3D%22.66s%22%20dur%3D%221s%22%20repeatCount%3D%22indefinite%22%20%2F%3E%0A%20%20%3C%2Fcircle%3E%0A%3C%2Fsvg%3E%0A\\\") no-repeat center center;border:1px solid #ccc;min-height:240px;min-width:320px}.mce-match-marker{background:#aaa;color:#fff}.mce-match-marker-selected{background:#39f;color:#fff}.mce-match-marker-selected::-moz-selection{background:#39f;color:#fff}.mce-match-marker-selected::selection{background:#39f;color:#fff}.mce-content-body audio[data-mce-selected],.mce-content-body details[data-mce-selected],.mce-content-body embed[data-mce-selected],.mce-content-body img[data-mce-selected],.mce-content-body object[data-mce-selected],.mce-content-body table[data-mce-selected],.mce-content-body video[data-mce-selected]{outline:3px solid #b4d7ff}.mce-content-body hr[data-mce-selected]{outline:3px solid #b4d7ff;outline-offset:1px}.mce-content-body [contentEditable=false] [contentEditable=true]:focus{outline:3px solid #b4d7ff}.mce-content-body [contentEditable=false] [contentEditable=true]:hover{outline:3px solid #b4d7ff}.mce-content-body [contentEditable=false][data-mce-selected]{cursor:not-allowed;outline:3px solid #b4d7ff}.mce-content-body.mce-content-readonly [contentEditable=true]:focus,.mce-content-body.mce-content-readonly [contentEditable=true]:hover{outline:0}.mce-content-body [data-mce-selected=inline-boundary]{background-color:#b4d7ff}.mce-content-body .mce-edit-focus{outline:3px solid #b4d7ff}.mce-content-body td[data-mce-selected],.mce-content-body th[data-mce-selected]{position:relative}.mce-content-body td[data-mce-selected]::-moz-selection,.mce-content-body th[data-mce-selected]::-moz-selection{background:0 0}.mce-content-body td[data-mce-selected]::selection,.mce-content-body th[data-mce-selected]::selection{background:0 0}.mce-content-body td[data-mce-selected] *,.mce-content-body th[data-mce-selected] *{outline:0;-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}.mce-content-body td[data-mce-selected]::after,.mce-content-body th[data-mce-selected]::after{background-color:rgba(180,215,255,.7);border:1px solid rgba(180,215,255,.7);bottom:-1px;content:'';left:-1px;mix-blend-mode:multiply;position:absolute;right:-1px;top:-1px}@media screen and (-ms-high-contrast:active),(-ms-high-contrast:none){.mce-content-body td[data-mce-selected]::after,.mce-content-body th[data-mce-selected]::after{border-color:rgba(0,84,180,.7)}}.mce-content-body img[data-mce-selected]::-moz-selection{background:0 0}.mce-content-body img[data-mce-selected]::selection{background:0 0}.ephox-snooker-resizer-bar{background-color:#b4d7ff;opacity:0;-webkit-user-select:none;-moz-user-select:none;user-select:none}.ephox-snooker-resizer-cols{cursor:col-resize}.ephox-snooker-resizer-rows{cursor:row-resize}.ephox-snooker-resizer-bar.ephox-snooker-resizer-bar-dragging{opacity:1}.mce-spellchecker-word{background-image:url(\\\"data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D'4'%20height%3D'4'%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%3E%3Cpath%20stroke%3D'%23ff0000'%20fill%3D'none'%20stroke-linecap%3D'round'%20stroke-opacity%3D'.75'%20d%3D'M0%203L2%201%204%203'%2F%3E%3C%2Fsvg%3E%0A\\\");background-position:0 calc(100% + 1px);background-repeat:repeat-x;background-size:auto 6px;cursor:default;height:2rem}.mce-spellchecker-grammar{background-image:url(\\\"data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D'4'%20height%3D'4'%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%3E%3Cpath%20stroke%3D'%2300A835'%20fill%3D'none'%20stroke-linecap%3D'round'%20d%3D'M0%203L2%201%204%203'%2F%3E%3C%2Fsvg%3E%0A\\\");background-position:0 calc(100% + 1px);background-repeat:repeat-x;background-size:auto 6px;cursor:default}.mce-toc{border:1px solid gray}.mce-toc h2{margin:4px}.mce-toc ul>li{list-style-type:none}[data-mce-block]{display:block}.mce-item-table:not([border]),.mce-item-table:not([border]) caption,.mce-item-table:not([border]) td,.mce-item-table:not([border]) th,.mce-item-table[border=\\\"0\\\"],.mce-item-table[border=\\\"0\\\"] caption,.mce-item-table[border=\\\"0\\\"] td,.mce-item-table[border=\\\"0\\\"] th,table[style*=\\\"border-width: 0px\\\"],table[style*=\\\"border-width: 0px\\\"] caption,table[style*=\\\"border-width: 0px\\\"] td,table[style*=\\\"border-width: 0px\\\"] th{border:1px dashed #bbb}.mce-visualblocks address,.mce-visualblocks article,.mce-visualblocks aside,.mce-visualblocks blockquote,.mce-visualblocks div:not([data-mce-bogus]),.mce-visualblocks dl,.mce-visualblocks figcaption,.mce-visualblocks figure,.mce-visualblocks h1,.mce-visualblocks h2,.mce-visualblocks h3,.mce-visualblocks h4,.mce-visualblocks h5,.mce-visualblocks h6,.mce-visualblocks hgroup,.mce-visualblocks ol,.mce-visualblocks p,.mce-visualblocks pre,.mce-visualblocks section,.mce-visualblocks ul{background-repeat:no-repeat;border:1px dashed #bbb;margin-left:3px;padding-top:10px}.mce-visualblocks p{background-image:url(data:image/gif;base64,R0lGODlhCQAJAJEAAAAAAP///7u7u////yH5BAEAAAMALAAAAAAJAAkAAAIQnG+CqCN/mlyvsRUpThG6AgA7)}.mce-visualblocks h1{background-image:url(data:image/gif;base64,R0lGODlhDQAKAIABALu7u////yH5BAEAAAEALAAAAAANAAoAAAIXjI8GybGu1JuxHoAfRNRW3TWXyF2YiRUAOw==)}.mce-visualblocks h2{background-image:url(data:image/gif;base64,R0lGODlhDgAKAIABALu7u////yH5BAEAAAEALAAAAAAOAAoAAAIajI8Hybbx4oOuqgTynJd6bGlWg3DkJzoaUAAAOw==)}.mce-visualblocks h3{background-image:url(data:image/gif;base64,R0lGODlhDgAKAIABALu7u////yH5BAEAAAEALAAAAAAOAAoAAAIZjI8Hybbx4oOuqgTynJf2Ln2NOHpQpmhAAQA7)}.mce-visualblocks h4{background-image:url(data:image/gif;base64,R0lGODlhDgAKAIABALu7u////yH5BAEAAAEALAAAAAAOAAoAAAIajI8HybbxInR0zqeAdhtJlXwV1oCll2HaWgAAOw==)}.mce-visualblocks h5{background-image:url(data:image/gif;base64,R0lGODlhDgAKAIABALu7u////yH5BAEAAAEALAAAAAAOAAoAAAIajI8HybbxIoiuwjane4iq5GlW05GgIkIZUAAAOw==)}.mce-visualblocks h6{background-image:url(data:image/gif;base64,R0lGODlhDgAKAIABALu7u////yH5BAEAAAEALAAAAAAOAAoAAAIajI8HybbxIoiuwjan04jep1iZ1XRlAo5bVgAAOw==)}.mce-visualblocks div:not([data-mce-bogus]){background-image:url(data:image/gif;base64,R0lGODlhEgAKAIABALu7u////yH5BAEAAAEALAAAAAASAAoAAAIfjI9poI0cgDywrhuxfbrzDEbQM2Ei5aRjmoySW4pAAQA7)}.mce-visualblocks section{background-image:url(data:image/gif;base64,R0lGODlhKAAKAIABALu7u////yH5BAEAAAEALAAAAAAoAAoAAAI5jI+pywcNY3sBWHdNrplytD2ellDeSVbp+GmWqaDqDMepc8t17Y4vBsK5hDyJMcI6KkuYU+jpjLoKADs=)}.mce-visualblocks article{background-image:url(data:image/gif;base64,R0lGODlhKgAKAIABALu7u////yH5BAEAAAEALAAAAAAqAAoAAAI6jI+pywkNY3wG0GBvrsd2tXGYSGnfiF7ikpXemTpOiJScasYoDJJrjsG9gkCJ0ag6KhmaIe3pjDYBBQA7)}.mce-visualblocks blockquote{background-image:url(data:image/gif;base64,R0lGODlhPgAKAIABALu7u////yH5BAEAAAEALAAAAAA+AAoAAAJPjI+py+0Knpz0xQDyuUhvfoGgIX5iSKZYgq5uNL5q69asZ8s5rrf0yZmpNkJZzFesBTu8TOlDVAabUyatguVhWduud3EyiUk45xhTTgMBBQA7)}.mce-visualblocks address{background-image:url(data:image/gif;base64,R0lGODlhLQAKAIABALu7u////yH5BAEAAAEALAAAAAAtAAoAAAI/jI+pywwNozSP1gDyyZcjb3UaRpXkWaXmZW4OqKLhBmLs+K263DkJK7OJeifh7FicKD9A1/IpGdKkyFpNmCkAADs=)}.mce-visualblocks pre{background-image:url(data:image/gif;base64,R0lGODlhFQAKAIABALu7uwAAACH5BAEAAAEALAAAAAAVAAoAAAIjjI+ZoN0cgDwSmnpz1NCueYERhnibZVKLNnbOq8IvKpJtVQAAOw==)}.mce-visualblocks figure{background-image:url(data:image/gif;base64,R0lGODlhJAAKAIAAALu7u////yH5BAEAAAEALAAAAAAkAAoAAAI0jI+py+2fwAHUSFvD3RlvG4HIp4nX5JFSpnZUJ6LlrM52OE7uSWosBHScgkSZj7dDKnWAAgA7)}.mce-visualblocks figcaption{border:1px dashed #bbb}.mce-visualblocks hgroup{background-image:url(data:image/gif;base64,R0lGODlhJwAKAIABALu7uwAAACH5BAEAAAEALAAAAAAnAAoAAAI3jI+pywYNI3uB0gpsRtt5fFnfNZaVSYJil4Wo03Hv6Z62uOCgiXH1kZIIJ8NiIxRrAZNMZAtQAAA7)}.mce-visualblocks aside{background-image:url(data:image/gif;base64,R0lGODlhHgAKAIABAKqqqv///yH5BAEAAAEALAAAAAAeAAoAAAItjI+pG8APjZOTzgtqy7I3f1yehmQcFY4WKZbqByutmW4aHUd6vfcVbgudgpYCADs=)}.mce-visualblocks ul{background-image:url(data:image/gif;base64,R0lGODlhDQAKAIAAALu7u////yH5BAEAAAEALAAAAAANAAoAAAIXjI8GybGuYnqUVSjvw26DzzXiqIDlVwAAOw==)}.mce-visualblocks ol{background-image:url(data:image/gif;base64,R0lGODlhDQAKAIABALu7u////yH5BAEAAAEALAAAAAANAAoAAAIXjI8GybH6HHt0qourxC6CvzXieHyeWQAAOw==)}.mce-visualblocks dl{background-image:url(data:image/gif;base64,R0lGODlhDQAKAIABALu7u////yH5BAEAAAEALAAAAAANAAoAAAIXjI8GybEOnmOvUoWznTqeuEjNSCqeGRUAOw==)}.mce-visualblocks:not([dir=rtl]) address,.mce-visualblocks:not([dir=rtl]) article,.mce-visualblocks:not([dir=rtl]) aside,.mce-visualblocks:not([dir=rtl]) blockquote,.mce-visualblocks:not([dir=rtl]) div:not([data-mce-bogus]),.mce-visualblocks:not([dir=rtl]) dl,.mce-visualblocks:not([dir=rtl]) figcaption,.mce-visualblocks:not([dir=rtl]) figure,.mce-visualblocks:not([dir=rtl]) h1,.mce-visualblocks:not([dir=rtl]) h2,.mce-visualblocks:not([dir=rtl]) h3,.mce-visualblocks:not([dir=rtl]) h4,.mce-visualblocks:not([dir=rtl]) h5,.mce-visualblocks:not([dir=rtl]) h6,.mce-visualblocks:not([dir=rtl]) hgroup,.mce-visualblocks:not([dir=rtl]) ol,.mce-visualblocks:not([dir=rtl]) p,.mce-visualblocks:not([dir=rtl]) pre,.mce-visualblocks:not([dir=rtl]) section,.mce-visualblocks:not([dir=rtl]) ul{margin-left:3px}.mce-visualblocks[dir=rtl] address,.mce-visualblocks[dir=rtl] article,.mce-visualblocks[dir=rtl] aside,.mce-visualblocks[dir=rtl] blockquote,.mce-visualblocks[dir=rtl] div:not([data-mce-bogus]),.mce-visualblocks[dir=rtl] dl,.mce-visualblocks[dir=rtl] figcaption,.mce-visualblocks[dir=rtl] figure,.mce-visualblocks[dir=rtl] h1,.mce-visualblocks[dir=rtl] h2,.mce-visualblocks[dir=rtl] h3,.mce-visualblocks[dir=rtl] h4,.mce-visualblocks[dir=rtl] h5,.mce-visualblocks[dir=rtl] h6,.mce-visualblocks[dir=rtl] hgroup,.mce-visualblocks[dir=rtl] ol,.mce-visualblocks[dir=rtl] p,.mce-visualblocks[dir=rtl] pre,.mce-visualblocks[dir=rtl] section,.mce-visualblocks[dir=rtl] ul{background-position-x:right;margin-right:3px}.mce-nbsp,.mce-shy{background:#aaa}.mce-shy::after{content:'-'}\")\n//# sourceMappingURL=content.inline.js.map\n"
  },
  {
    "path": "apps/web-antd/public/tinymce/skins/ui/tinymce-5-dark/content.js",
    "content": "tinymce.Resource.add('ui/tinymce-5-dark/content.css', \".mce-content-body .mce-item-anchor{background:transparent url(\\\"data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D'8'%20height%3D'12'%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%3E%3Cpath%20d%3D'M0%200L8%200%208%2012%204.09117821%209%200%2012z'%20fill%3D%22%23cccccc%22%2F%3E%3C%2Fsvg%3E%0A\\\") no-repeat center}.mce-content-body .mce-item-anchor:empty{cursor:default;display:inline-block;height:12px!important;padding:0 2px;-webkit-user-modify:read-only;-moz-user-modify:read-only;-webkit-user-select:all;-moz-user-select:all;user-select:all;width:8px!important}.mce-content-body .mce-item-anchor:not(:empty){background-position-x:2px;display:inline-block;padding-left:12px}.mce-content-body .mce-item-anchor[data-mce-selected]{outline-offset:1px}.tox-comments-visible .tox-comment[contenteditable=false]:not([data-mce-selected]),.tox-comments-visible span.tox-comment img:not([data-mce-selected]),.tox-comments-visible span.tox-comment span.mce-preview-object:not([data-mce-selected]),.tox-comments-visible span.tox-comment>audio:not([data-mce-selected]),.tox-comments-visible span.tox-comment>video:not([data-mce-selected]){outline:3px solid #ffe89d}.tox-comments-visible .tox-comment[contenteditable=false][data-mce-annotation-active=true]:not([data-mce-selected]){outline:3px solid #fed635}.tox-comments-visible span.tox-comment[data-mce-annotation-active=true] img:not([data-mce-selected]),.tox-comments-visible span.tox-comment[data-mce-annotation-active=true] span.mce-preview-object:not([data-mce-selected]),.tox-comments-visible span.tox-comment[data-mce-annotation-active=true]>audio:not([data-mce-selected]),.tox-comments-visible span.tox-comment[data-mce-annotation-active=true]>video:not([data-mce-selected]){outline:3px solid #fed635}.tox-comments-visible span.tox-comment:not([data-mce-selected]){background-color:#ffe89d;outline:0}.tox-comments-visible span.tox-comment[data-mce-annotation-active=true]:not([data-mce-selected=inline-boundary]){background-color:#fed635}.tox-checklist>li:not(.tox-checklist--hidden){list-style:none;margin:.25em 0}.tox-checklist>li:not(.tox-checklist--hidden)::before{content:url(\\\"data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2216%22%20height%3D%2216%22%20viewBox%3D%220%200%2016%2016%22%3E%3Cg%20id%3D%22checklist-unchecked%22%20fill%3D%22none%22%20fill-rule%3D%22evenodd%22%3E%3Crect%20id%3D%22Rectangle%22%20width%3D%2215%22%20height%3D%2215%22%20x%3D%22.5%22%20y%3D%22.5%22%20fill-rule%3D%22nonzero%22%20stroke%3D%22%236d737b%22%20rx%3D%222%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E%0A\\\");cursor:pointer;height:1em;margin-left:-1.5em;margin-top:.125em;position:absolute;width:1em}.tox-checklist li:not(.tox-checklist--hidden).tox-checklist--checked::before{content:url(\\\"data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2216%22%20height%3D%2216%22%20viewBox%3D%220%200%2016%2016%22%3E%3Cg%20id%3D%22checklist-checked%22%20fill%3D%22none%22%20fill-rule%3D%22evenodd%22%3E%3Crect%20id%3D%22Rectangle%22%20width%3D%2216%22%20height%3D%2216%22%20fill%3D%22%234099FF%22%20fill-rule%3D%22nonzero%22%20rx%3D%222%22%2F%3E%3Cpath%20id%3D%22Path%22%20fill%3D%22%23FFF%22%20fill-rule%3D%22nonzero%22%20d%3D%22M11.5703186%2C3.14417309%20C11.8516238%2C2.73724603%2012.4164781%2C2.62829933%2012.83558%2C2.89774797%20C13.260121%2C3.17069355%2013.3759736%2C3.72932262%2013.0909105%2C4.14168582%20L7.7580587%2C11.8560195%20C7.43776896%2C12.3193404%206.76483983%2C12.3852142%206.35607322%2C11.9948725%20L3.02491697%2C8.8138662%20C2.66090143%2C8.46625845%202.65798871%2C7.89594698%203.01850234%2C7.54483354%20C3.373942%2C7.19866177%203.94940006%2C7.19592841%204.30829608%2C7.5386474%20L6.85276923%2C9.9684299%20L11.5703186%2C3.14417309%20Z%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E%0A\\\")}[dir=rtl] .tox-checklist>li:not(.tox-checklist--hidden)::before{margin-left:0;margin-right:-1.5em}code[class*=language-],pre[class*=language-]{color:#f8f8f2;background:0 0;text-shadow:0 1px rgba(0,0,0,.3);font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;tab-size:4;-webkit-hyphens:none;hyphens:none}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto;border-radius:.3em}:not(pre)>code[class*=language-],pre[class*=language-]{background:#282a36}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#6272a4}.token.punctuation{color:#f8f8f2}.namespace{opacity:.7}.token.constant,.token.deleted,.token.property,.token.symbol,.token.tag{color:#ff79c6}.token.boolean,.token.number{color:#bd93f9}.token.attr-name,.token.builtin,.token.char,.token.inserted,.token.selector,.token.string{color:#50fa7b}.language-css .token.string,.style .token.string,.token.entity,.token.operator,.token.url,.token.variable{color:#f8f8f2}.token.atrule,.token.attr-value,.token.class-name,.token.function{color:#f1fa8c}.token.keyword{color:#8be9fd}.token.important,.token.regex{color:#ffb86c}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}.mce-content-body{overflow-wrap:break-word;word-wrap:break-word}.mce-content-body .mce-visual-caret{background-color:#000;background-color:currentColor;position:absolute}.mce-content-body .mce-visual-caret-hidden{display:none}.mce-content-body [data-mce-caret]{left:-1000px;margin:0;padding:0;position:absolute;right:auto;top:0}.mce-content-body .mce-offscreen-selection{left:-2000000px;max-width:1000000px;position:absolute}.mce-content-body [contentEditable=false]{cursor:default}.mce-content-body [contentEditable=true]{cursor:text}.tox-cursor-format-painter{cursor:url(\\\"data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%20viewBox%3D%220%200%2024%2024%22%3E%0A%20%20%3Cg%20fill%3D%22none%22%20fill-rule%3D%22evenodd%22%3E%0A%20%20%20%20%3Cpath%20fill%3D%22%23000%22%20fill-rule%3D%22nonzero%22%20d%3D%22M15%2C6%20C15%2C5.45%2014.55%2C5%2014%2C5%20L6%2C5%20C5.45%2C5%205%2C5.45%205%2C6%20L5%2C10%20C5%2C10.55%205.45%2C11%206%2C11%20L14%2C11%20C14.55%2C11%2015%2C10.55%2015%2C10%20L15%2C9%20L16%2C9%20L16%2C12%20L9%2C12%20L9%2C19%20C9%2C19.55%209.45%2C20%2010%2C20%20L11%2C20%20C11.55%2C20%2012%2C19.55%2012%2C19%20L12%2C14%20L18%2C14%20L18%2C7%20L15%2C7%20L15%2C6%20Z%22%2F%3E%0A%20%20%20%20%3Cpath%20fill%3D%22%23000%22%20fill-rule%3D%22nonzero%22%20d%3D%22M1%2C1%20L8.25%2C1%20C8.66421356%2C1%209%2C1.33578644%209%2C1.75%20L9%2C1.75%20C9%2C2.16421356%208.66421356%2C2.5%208.25%2C2.5%20L2.5%2C2.5%20L2.5%2C8.25%20C2.5%2C8.66421356%202.16421356%2C9%201.75%2C9%20L1.75%2C9%20C1.33578644%2C9%201%2C8.66421356%201%2C8.25%20L1%2C1%20Z%22%2F%3E%0A%20%20%3C%2Fg%3E%0A%3C%2Fsvg%3E%0A\\\"),default}div.mce-footnotes hr{margin-inline-end:auto;margin-inline-start:0;width:25%}div.mce-footnotes li>a.mce-footnotes-backlink{text-decoration:none}@media print{sup.mce-footnote a{color:#000;text-decoration:none}div.mce-footnotes{break-inside:avoid;width:100%}div.mce-footnotes li>a.mce-footnotes-backlink{display:none}}tiny-math-block{display:flex;justify-content:center;margin:16px 0 16px 0}tiny-math-inline{display:inline-block}.mce-content-body figure.align-left{float:left}.mce-content-body figure.align-right{float:right}.mce-content-body figure.image.align-center{display:table;margin-left:auto;margin-right:auto}.mce-preview-object{border:1px solid gray;display:inline-block;line-height:0;margin:0 2px 0 2px;position:relative}.mce-preview-object .mce-shim{background:url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7);height:100%;left:0;position:absolute;top:0;width:100%}.mce-preview-object[data-mce-selected=\\\"2\\\"] .mce-shim{display:none}.mce-content-body .mce-mergetag{cursor:default!important;-webkit-user-select:none;-moz-user-select:none;user-select:none}.mce-content-body .mce-mergetag:hover{background-color:rgba(0,108,231,.3)}.mce-content-body .mce-mergetag-affix{background-color:rgba(0,108,231,.3);color:#006ce7}.mce-object{background:transparent url(\\\"data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%3E%3Cpath%20d%3D%22M4%203h16a1%201%200%200%201%201%201v16a1%201%200%200%201-1%201H4a1%201%200%200%201-1-1V4a1%201%200%200%201%201-1zm1%202v14h14V5H5zm4.79%202.565l5.64%204.028a.5.5%200%200%201%200%20.814l-5.64%204.028a.5.5%200%200%201-.79-.407V7.972a.5.5%200%200%201%20.79-.407z%22%20fill%3D%22%23cccccc%22%2F%3E%3C%2Fsvg%3E%0A\\\") no-repeat center;border:1px dashed #aaa}.mce-pagebreak{border:1px dashed #aaa;cursor:default;display:block;height:5px;margin-top:15px;page-break-before:always;width:100%}@media print{.mce-pagebreak{border:0}}.tiny-pageembed .mce-shim{background:url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7);height:100%;left:0;position:absolute;top:0;width:100%}.tiny-pageembed[data-mce-selected=\\\"2\\\"] .mce-shim{display:none}.tiny-pageembed{display:inline-block;position:relative}.tiny-pageembed--16by9,.tiny-pageembed--1by1,.tiny-pageembed--21by9,.tiny-pageembed--4by3{display:block;overflow:hidden;padding:0;position:relative;width:100%}.tiny-pageembed--21by9{padding-top:42.857143%}.tiny-pageembed--16by9{padding-top:56.25%}.tiny-pageembed--4by3{padding-top:75%}.tiny-pageembed--1by1{padding-top:100%}.tiny-pageembed--16by9 iframe,.tiny-pageembed--1by1 iframe,.tiny-pageembed--21by9 iframe,.tiny-pageembed--4by3 iframe{border:0;height:100%;left:0;position:absolute;top:0;width:100%}.mce-content-body[data-mce-placeholder]{position:relative}.mce-content-body[data-mce-placeholder]:not(.mce-visualblocks)::before{color:rgba(34,47,62,.7);content:attr(data-mce-placeholder);position:absolute}@media (forced-colors:active){.mce-content-body[data-mce-placeholder]:not(.mce-visualblocks)::before{color:highlight;filter:brightness(30%);z-index:-1}}.mce-content-body:not([dir=rtl])[data-mce-placeholder]:not(.mce-visualblocks)::before{left:1px}.mce-content-body[dir=rtl][data-mce-placeholder]:not(.mce-visualblocks)::before{right:1px}.mce-content-body div.mce-resizehandle{background-color:#4099ff;border-color:#4099ff;border-style:solid;border-width:1px;box-sizing:border-box;height:10px;position:absolute;width:10px;z-index:1298}.mce-content-body div.mce-resizehandle:hover{background-color:#4099ff}.mce-content-body div.mce-resizehandle:nth-of-type(1){cursor:nwse-resize}.mce-content-body div.mce-resizehandle:nth-of-type(2){cursor:nesw-resize}.mce-content-body div.mce-resizehandle:nth-of-type(3){cursor:nwse-resize}.mce-content-body div.mce-resizehandle:nth-of-type(4){cursor:nesw-resize}.mce-content-body .mce-resize-backdrop{z-index:10000}.mce-content-body .mce-clonedresizable{cursor:default;opacity:.5;outline:1px dashed #000;position:absolute;z-index:10001}.mce-content-body .mce-clonedresizable.mce-resizetable-columns td,.mce-content-body .mce-clonedresizable.mce-resizetable-columns th{border:0}.mce-content-body .mce-resize-helper{background:#555;background:rgba(0,0,0,.75);border:1px;border-radius:3px;color:#fff;display:none;font-family:sans-serif;font-size:12px;line-height:14px;margin:5px 10px;padding:5px;position:absolute;white-space:nowrap;z-index:10002}.tox-rtc-user-selection{position:relative}.tox-rtc-user-cursor{bottom:0;cursor:default;position:absolute;top:0;width:2px}.tox-rtc-user-cursor::before{background-color:inherit;border-radius:50%;content:'';display:block;height:8px;position:absolute;right:-3px;top:-3px;width:8px}.tox-rtc-user-cursor:hover::after{background-color:inherit;border-radius:100px;box-sizing:border-box;color:#fff;content:attr(data-user);display:block;font-size:12px;font-weight:700;left:-5px;min-height:8px;min-width:8px;padding:0 12px;position:absolute;top:-11px;white-space:nowrap;z-index:1000}.tox-rtc-user-selection--1 .tox-rtc-user-cursor{background-color:#2dc26b}.tox-rtc-user-selection--2 .tox-rtc-user-cursor{background-color:#e03e2d}.tox-rtc-user-selection--3 .tox-rtc-user-cursor{background-color:#f1c40f}.tox-rtc-user-selection--4 .tox-rtc-user-cursor{background-color:#3598db}.tox-rtc-user-selection--5 .tox-rtc-user-cursor{background-color:#b96ad9}.tox-rtc-user-selection--6 .tox-rtc-user-cursor{background-color:#e67e23}.tox-rtc-user-selection--7 .tox-rtc-user-cursor{background-color:#aaa69d}.tox-rtc-user-selection--8 .tox-rtc-user-cursor{background-color:#f368e0}.tox-rtc-remote-image{background:#eaeaea url(\\\"data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%2236%22%20height%3D%2212%22%20viewBox%3D%220%200%2036%2012%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%3Ccircle%20cx%3D%226%22%20cy%3D%226%22%20r%3D%223%22%20fill%3D%22rgba(0%2C%200%2C%200%2C%20.2)%22%3E%0A%20%20%20%20%3Canimate%20attributeName%3D%22r%22%20values%3D%223%3B5%3B3%22%20calcMode%3D%22linear%22%20dur%3D%221s%22%20repeatCount%3D%22indefinite%22%20%2F%3E%0A%20%20%3C%2Fcircle%3E%0A%20%20%3Ccircle%20cx%3D%2218%22%20cy%3D%226%22%20r%3D%223%22%20fill%3D%22rgba(0%2C%200%2C%200%2C%20.2)%22%3E%0A%20%20%20%20%3Canimate%20attributeName%3D%22r%22%20values%3D%223%3B5%3B3%22%20calcMode%3D%22linear%22%20begin%3D%22.33s%22%20dur%3D%221s%22%20repeatCount%3D%22indefinite%22%20%2F%3E%0A%20%20%3C%2Fcircle%3E%0A%20%20%3Ccircle%20cx%3D%2230%22%20cy%3D%226%22%20r%3D%223%22%20fill%3D%22rgba(0%2C%200%2C%200%2C%20.2)%22%3E%0A%20%20%20%20%3Canimate%20attributeName%3D%22r%22%20values%3D%223%3B5%3B3%22%20calcMode%3D%22linear%22%20begin%3D%22.66s%22%20dur%3D%221s%22%20repeatCount%3D%22indefinite%22%20%2F%3E%0A%20%20%3C%2Fcircle%3E%0A%3C%2Fsvg%3E%0A\\\") no-repeat center center;border:1px solid #ccc;min-height:240px;min-width:320px}.mce-match-marker{background:#aaa;color:#fff}.mce-match-marker-selected{background:#39f;color:#fff}.mce-match-marker-selected::-moz-selection{background:#39f;color:#fff}.mce-match-marker-selected::selection{background:#39f;color:#fff}.mce-content-body audio[data-mce-selected],.mce-content-body details[data-mce-selected],.mce-content-body embed[data-mce-selected],.mce-content-body img[data-mce-selected],.mce-content-body object[data-mce-selected],.mce-content-body table[data-mce-selected],.mce-content-body video[data-mce-selected]{outline:3px solid #4099ff}.mce-content-body hr[data-mce-selected]{outline:3px solid #4099ff;outline-offset:1px}.mce-content-body [contentEditable=false] [contentEditable=true]:focus{outline:3px solid #4099ff}.mce-content-body [contentEditable=false] [contentEditable=true]:hover{outline:3px solid #4099ff}.mce-content-body [contentEditable=false][data-mce-selected]{cursor:not-allowed;outline:3px solid #4099ff}.mce-content-body.mce-content-readonly [contentEditable=true]:focus,.mce-content-body.mce-content-readonly [contentEditable=true]:hover{outline:0}.mce-content-body [data-mce-selected=inline-boundary]{background-color:#4099ff}.mce-content-body .mce-edit-focus{outline:3px solid #4099ff}.mce-content-body td[data-mce-selected],.mce-content-body th[data-mce-selected]{position:relative}.mce-content-body td[data-mce-selected]::-moz-selection,.mce-content-body th[data-mce-selected]::-moz-selection{background:0 0}.mce-content-body td[data-mce-selected]::selection,.mce-content-body th[data-mce-selected]::selection{background:0 0}.mce-content-body td[data-mce-selected] *,.mce-content-body th[data-mce-selected] *{outline:0;-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}.mce-content-body td[data-mce-selected]::after,.mce-content-body th[data-mce-selected]::after{background-color:rgba(180,215,255,.7);border:1px solid transparent;bottom:-1px;content:'';left:-1px;mix-blend-mode:lighten;position:absolute;right:-1px;top:-1px}@media screen and (-ms-high-contrast:active),(-ms-high-contrast:none){.mce-content-body td[data-mce-selected]::after,.mce-content-body th[data-mce-selected]::after{border-color:rgba(0,84,180,.7)}}.mce-content-body img[data-mce-selected]::-moz-selection{background:0 0}.mce-content-body img[data-mce-selected]::selection{background:0 0}.ephox-snooker-resizer-bar{background-color:#4099ff;opacity:0;-webkit-user-select:none;-moz-user-select:none;user-select:none}.ephox-snooker-resizer-cols{cursor:col-resize}.ephox-snooker-resizer-rows{cursor:row-resize}.ephox-snooker-resizer-bar.ephox-snooker-resizer-bar-dragging{opacity:1}.mce-spellchecker-word{background-image:url(\\\"data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D'4'%20height%3D'4'%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%3E%3Cpath%20stroke%3D'%23ff0000'%20fill%3D'none'%20stroke-linecap%3D'round'%20stroke-opacity%3D'.75'%20d%3D'M0%203L2%201%204%203'%2F%3E%3C%2Fsvg%3E%0A\\\");background-position:0 calc(100% + 1px);background-repeat:repeat-x;background-size:auto 6px;cursor:default;height:2rem}.mce-spellchecker-grammar{background-image:url(\\\"data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D'4'%20height%3D'4'%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%3E%3Cpath%20stroke%3D'%2300A835'%20fill%3D'none'%20stroke-linecap%3D'round'%20d%3D'M0%203L2%201%204%203'%2F%3E%3C%2Fsvg%3E%0A\\\");background-position:0 calc(100% + 1px);background-repeat:repeat-x;background-size:auto 6px;cursor:default}.mce-toc{border:1px solid gray}.mce-toc h2{margin:4px}.mce-toc ul>li{list-style-type:none}[data-mce-block]{display:block}.mce-item-table:not([border]),.mce-item-table:not([border]) caption,.mce-item-table:not([border]) td,.mce-item-table:not([border]) th,.mce-item-table[border=\\\"0\\\"],.mce-item-table[border=\\\"0\\\"] caption,.mce-item-table[border=\\\"0\\\"] td,.mce-item-table[border=\\\"0\\\"] th,table[style*=\\\"border-width: 0px\\\"],table[style*=\\\"border-width: 0px\\\"] caption,table[style*=\\\"border-width: 0px\\\"] td,table[style*=\\\"border-width: 0px\\\"] th{border:1px dashed #bbb}.mce-visualblocks address,.mce-visualblocks article,.mce-visualblocks aside,.mce-visualblocks blockquote,.mce-visualblocks div:not([data-mce-bogus]),.mce-visualblocks dl,.mce-visualblocks figcaption,.mce-visualblocks figure,.mce-visualblocks h1,.mce-visualblocks h2,.mce-visualblocks h3,.mce-visualblocks h4,.mce-visualblocks h5,.mce-visualblocks h6,.mce-visualblocks hgroup,.mce-visualblocks ol,.mce-visualblocks p,.mce-visualblocks pre,.mce-visualblocks section,.mce-visualblocks ul{background-repeat:no-repeat;border:1px dashed #bbb;margin-left:3px;padding-top:10px}.mce-visualblocks p{background-image:url(data:image/gif;base64,R0lGODlhCQAJAJEAAAAAAP///7u7u////yH5BAEAAAMALAAAAAAJAAkAAAIQnG+CqCN/mlyvsRUpThG6AgA7)}.mce-visualblocks h1{background-image:url(data:image/gif;base64,R0lGODlhDQAKAIABALu7u////yH5BAEAAAEALAAAAAANAAoAAAIXjI8GybGu1JuxHoAfRNRW3TWXyF2YiRUAOw==)}.mce-visualblocks h2{background-image:url(data:image/gif;base64,R0lGODlhDgAKAIABALu7u////yH5BAEAAAEALAAAAAAOAAoAAAIajI8Hybbx4oOuqgTynJd6bGlWg3DkJzoaUAAAOw==)}.mce-visualblocks h3{background-image:url(data:image/gif;base64,R0lGODlhDgAKAIABALu7u////yH5BAEAAAEALAAAAAAOAAoAAAIZjI8Hybbx4oOuqgTynJf2Ln2NOHpQpmhAAQA7)}.mce-visualblocks h4{background-image:url(data:image/gif;base64,R0lGODlhDgAKAIABALu7u////yH5BAEAAAEALAAAAAAOAAoAAAIajI8HybbxInR0zqeAdhtJlXwV1oCll2HaWgAAOw==)}.mce-visualblocks h5{background-image:url(data:image/gif;base64,R0lGODlhDgAKAIABALu7u////yH5BAEAAAEALAAAAAAOAAoAAAIajI8HybbxIoiuwjane4iq5GlW05GgIkIZUAAAOw==)}.mce-visualblocks h6{background-image:url(data:image/gif;base64,R0lGODlhDgAKAIABALu7u////yH5BAEAAAEALAAAAAAOAAoAAAIajI8HybbxIoiuwjan04jep1iZ1XRlAo5bVgAAOw==)}.mce-visualblocks div:not([data-mce-bogus]){background-image:url(data:image/gif;base64,R0lGODlhEgAKAIABALu7u////yH5BAEAAAEALAAAAAASAAoAAAIfjI9poI0cgDywrhuxfbrzDEbQM2Ei5aRjmoySW4pAAQA7)}.mce-visualblocks section{background-image:url(data:image/gif;base64,R0lGODlhKAAKAIABALu7u////yH5BAEAAAEALAAAAAAoAAoAAAI5jI+pywcNY3sBWHdNrplytD2ellDeSVbp+GmWqaDqDMepc8t17Y4vBsK5hDyJMcI6KkuYU+jpjLoKADs=)}.mce-visualblocks article{background-image:url(data:image/gif;base64,R0lGODlhKgAKAIABALu7u////yH5BAEAAAEALAAAAAAqAAoAAAI6jI+pywkNY3wG0GBvrsd2tXGYSGnfiF7ikpXemTpOiJScasYoDJJrjsG9gkCJ0ag6KhmaIe3pjDYBBQA7)}.mce-visualblocks blockquote{background-image:url(data:image/gif;base64,R0lGODlhPgAKAIABALu7u////yH5BAEAAAEALAAAAAA+AAoAAAJPjI+py+0Knpz0xQDyuUhvfoGgIX5iSKZYgq5uNL5q69asZ8s5rrf0yZmpNkJZzFesBTu8TOlDVAabUyatguVhWduud3EyiUk45xhTTgMBBQA7)}.mce-visualblocks address{background-image:url(data:image/gif;base64,R0lGODlhLQAKAIABALu7u////yH5BAEAAAEALAAAAAAtAAoAAAI/jI+pywwNozSP1gDyyZcjb3UaRpXkWaXmZW4OqKLhBmLs+K263DkJK7OJeifh7FicKD9A1/IpGdKkyFpNmCkAADs=)}.mce-visualblocks pre{background-image:url(data:image/gif;base64,R0lGODlhFQAKAIABALu7uwAAACH5BAEAAAEALAAAAAAVAAoAAAIjjI+ZoN0cgDwSmnpz1NCueYERhnibZVKLNnbOq8IvKpJtVQAAOw==)}.mce-visualblocks figure{background-image:url(data:image/gif;base64,R0lGODlhJAAKAIAAALu7u////yH5BAEAAAEALAAAAAAkAAoAAAI0jI+py+2fwAHUSFvD3RlvG4HIp4nX5JFSpnZUJ6LlrM52OE7uSWosBHScgkSZj7dDKnWAAgA7)}.mce-visualblocks figcaption{border:1px dashed #bbb}.mce-visualblocks hgroup{background-image:url(data:image/gif;base64,R0lGODlhJwAKAIABALu7uwAAACH5BAEAAAEALAAAAAAnAAoAAAI3jI+pywYNI3uB0gpsRtt5fFnfNZaVSYJil4Wo03Hv6Z62uOCgiXH1kZIIJ8NiIxRrAZNMZAtQAAA7)}.mce-visualblocks aside{background-image:url(data:image/gif;base64,R0lGODlhHgAKAIABAKqqqv///yH5BAEAAAEALAAAAAAeAAoAAAItjI+pG8APjZOTzgtqy7I3f1yehmQcFY4WKZbqByutmW4aHUd6vfcVbgudgpYCADs=)}.mce-visualblocks ul{background-image:url(data:image/gif;base64,R0lGODlhDQAKAIAAALu7u////yH5BAEAAAEALAAAAAANAAoAAAIXjI8GybGuYnqUVSjvw26DzzXiqIDlVwAAOw==)}.mce-visualblocks ol{background-image:url(data:image/gif;base64,R0lGODlhDQAKAIABALu7u////yH5BAEAAAEALAAAAAANAAoAAAIXjI8GybH6HHt0qourxC6CvzXieHyeWQAAOw==)}.mce-visualblocks dl{background-image:url(data:image/gif;base64,R0lGODlhDQAKAIABALu7u////yH5BAEAAAEALAAAAAANAAoAAAIXjI8GybEOnmOvUoWznTqeuEjNSCqeGRUAOw==)}.mce-visualblocks:not([dir=rtl]) address,.mce-visualblocks:not([dir=rtl]) article,.mce-visualblocks:not([dir=rtl]) aside,.mce-visualblocks:not([dir=rtl]) blockquote,.mce-visualblocks:not([dir=rtl]) div:not([data-mce-bogus]),.mce-visualblocks:not([dir=rtl]) dl,.mce-visualblocks:not([dir=rtl]) figcaption,.mce-visualblocks:not([dir=rtl]) figure,.mce-visualblocks:not([dir=rtl]) h1,.mce-visualblocks:not([dir=rtl]) h2,.mce-visualblocks:not([dir=rtl]) h3,.mce-visualblocks:not([dir=rtl]) h4,.mce-visualblocks:not([dir=rtl]) h5,.mce-visualblocks:not([dir=rtl]) h6,.mce-visualblocks:not([dir=rtl]) hgroup,.mce-visualblocks:not([dir=rtl]) ol,.mce-visualblocks:not([dir=rtl]) p,.mce-visualblocks:not([dir=rtl]) pre,.mce-visualblocks:not([dir=rtl]) section,.mce-visualblocks:not([dir=rtl]) ul{margin-left:3px}.mce-visualblocks[dir=rtl] address,.mce-visualblocks[dir=rtl] article,.mce-visualblocks[dir=rtl] aside,.mce-visualblocks[dir=rtl] blockquote,.mce-visualblocks[dir=rtl] div:not([data-mce-bogus]),.mce-visualblocks[dir=rtl] dl,.mce-visualblocks[dir=rtl] figcaption,.mce-visualblocks[dir=rtl] figure,.mce-visualblocks[dir=rtl] h1,.mce-visualblocks[dir=rtl] h2,.mce-visualblocks[dir=rtl] h3,.mce-visualblocks[dir=rtl] h4,.mce-visualblocks[dir=rtl] h5,.mce-visualblocks[dir=rtl] h6,.mce-visualblocks[dir=rtl] hgroup,.mce-visualblocks[dir=rtl] ol,.mce-visualblocks[dir=rtl] p,.mce-visualblocks[dir=rtl] pre,.mce-visualblocks[dir=rtl] section,.mce-visualblocks[dir=rtl] ul{background-position-x:right;margin-right:3px}.mce-nbsp,.mce-shy{background:#aaa}.mce-shy::after{content:'-'}body{font-family:sans-serif}table{border-collapse:collapse}\")\n//# sourceMappingURL=content.js.map\n"
  },
  {
    "path": "apps/web-antd/public/tinymce/skins/ui/tinymce-5-dark/skin.js",
    "content": "tinymce.Resource.add('ui/tinymce-5-dark/skin.css', \".tox{box-shadow:none;box-sizing:content-box;color:#2a3746;cursor:auto;font-family:-apple-system,BlinkMacSystemFont,\\\"Segoe UI\\\",Roboto,Oxygen-Sans,Ubuntu,Cantarell,\\\"Helvetica Neue\\\",sans-serif;font-size:16px;font-style:normal;font-weight:400;line-height:normal;-webkit-tap-highlight-color:transparent;text-decoration:none;text-shadow:none;text-transform:none;vertical-align:initial;white-space:normal}.tox :not(svg):not(rect){box-sizing:inherit;color:inherit;cursor:inherit;direction:inherit;font-family:inherit;font-size:inherit;font-style:inherit;font-weight:inherit;line-height:inherit;-webkit-tap-highlight-color:inherit;text-align:inherit;text-decoration:inherit;text-shadow:inherit;text-transform:inherit;vertical-align:inherit;white-space:inherit}.tox :not(svg):not(rect){background:0 0;border:0;box-shadow:none;float:none;height:auto;margin:0;max-width:none;outline:0;padding:0;position:static;width:auto}.tox:not([dir=rtl]){direction:ltr;text-align:left}.tox[dir=rtl]{direction:rtl;text-align:right}.tox-tinymce{border:1px solid #000;border-radius:0;box-shadow:none;box-sizing:border-box;display:flex;flex-direction:column;font-family:-apple-system,BlinkMacSystemFont,\\\"Segoe UI\\\",Roboto,Oxygen-Sans,Ubuntu,Cantarell,\\\"Helvetica Neue\\\",sans-serif;overflow:hidden;position:relative;visibility:inherit!important}.tox.tox-tinymce-inline{border:none;box-shadow:none;overflow:initial}.tox.tox-tinymce-inline .tox-editor-container{overflow:initial}.tox.tox-tinymce-inline .tox-editor-header{background-color:#222f3e;border:1px solid #000;border-radius:0;box-shadow:none;overflow:hidden}.tox-tinymce-aux{font-family:-apple-system,BlinkMacSystemFont,\\\"Segoe UI\\\",Roboto,Oxygen-Sans,Ubuntu,Cantarell,\\\"Helvetica Neue\\\",sans-serif;z-index:1300}.tox-tinymce :focus,.tox-tinymce-aux :focus{outline:0}button::-moz-focus-inner{border:0}.tox[dir=rtl] .tox-icon--flip svg{transform:rotateY(180deg)}.tox .accessibility-issue__header{align-items:center;display:flex;margin-bottom:4px}.tox .accessibility-issue__description{align-items:stretch;border-radius:3px;display:flex;justify-content:space-between}.tox .accessibility-issue__description>div{padding-bottom:4px}.tox .accessibility-issue__description>div>div{align-items:center;display:flex;margin-bottom:4px}.tox .accessibility-issue__description>div>div .tox-icon svg{display:block}.tox .accessibility-issue__repair{margin-top:16px}.tox .tox-dialog__body-content .accessibility-issue--info .accessibility-issue__description{background-color:rgba(30,113,170,.4);color:#fff}.tox .tox-dialog__body-content .accessibility-issue--info .tox-form__group h2{color:#fff}.tox .tox-dialog__body-content .accessibility-issue--info .tox-icon svg{fill:#fff}.tox .tox-dialog__body-content .accessibility-issue--info a.tox-button--naked.tox-button--icon{background-color:#207ab7;color:#fff}.tox .tox-dialog__body-content .accessibility-issue--info a.tox-button--naked.tox-button--icon:focus,.tox .tox-dialog__body-content .accessibility-issue--info a.tox-button--naked.tox-button--icon:hover{background-color:#1c6ca1}.tox .tox-dialog__body-content .accessibility-issue--info a.tox-button--naked.tox-button--icon:active{background-color:#185d8c}.tox .tox-dialog__body-content .accessibility-issue--warn .accessibility-issue__description{background-color:rgba(255,165,0,.5);color:#fff}.tox .tox-dialog__body-content .accessibility-issue--warn .tox-form__group h2{color:#fff}.tox .tox-dialog__body-content .accessibility-issue--warn .tox-icon svg{fill:#fff}.tox .tox-dialog__body-content .accessibility-issue--warn a.tox-button--naked.tox-button--icon{background-color:#ffe89d;color:#2a3746}.tox .tox-dialog__body-content .accessibility-issue--warn a.tox-button--naked.tox-button--icon:focus,.tox .tox-dialog__body-content .accessibility-issue--warn a.tox-button--naked.tox-button--icon:hover{background-color:#f2d574;color:#2a3746}.tox .tox-dialog__body-content .accessibility-issue--warn a.tox-button--naked.tox-button--icon:active{background-color:#e8c657;color:#2a3746}.tox .tox-dialog__body-content .accessibility-issue--error .accessibility-issue__description{background-color:rgba(204,0,0,.5);color:#fff}.tox .tox-dialog__body-content .accessibility-issue--error .tox-form__group h2{color:#fff}.tox .tox-dialog__body-content .accessibility-issue--error .tox-icon svg{fill:#fff}.tox .tox-dialog__body-content .accessibility-issue--error a.tox-button--naked.tox-button--icon{background-color:#f2bfbf;color:#2a3746}.tox .tox-dialog__body-content .accessibility-issue--error a.tox-button--naked.tox-button--icon:focus,.tox .tox-dialog__body-content .accessibility-issue--error a.tox-button--naked.tox-button--icon:hover{background-color:#e9a4a4;color:#2a3746}.tox .tox-dialog__body-content .accessibility-issue--error a.tox-button--naked.tox-button--icon:active{background-color:#ee9494;color:#2a3746}.tox .tox-dialog__body-content .accessibility-issue--success .accessibility-issue__description{background-color:rgba(120,171,70,.5);color:#fff}.tox .tox-dialog__body-content .accessibility-issue--success .accessibility-issue__description>:last-child{display:none}.tox .tox-dialog__body-content .accessibility-issue--success .tox-form__group h2{color:#fff}.tox .tox-dialog__body-content .accessibility-issue--success .tox-icon svg{fill:#fff}.tox .tox-dialog__body-content .accessibility-issue__header .tox-form__group h1,.tox .tox-dialog__body-content .tox-form__group .accessibility-issue__description h2{font-size:14px;margin-top:0}.tox:not([dir=rtl]) .tox-dialog__body-content .accessibility-issue__header .tox-button{margin-left:4px}.tox:not([dir=rtl]) .tox-dialog__body-content .accessibility-issue__header>:nth-last-child(2){margin-left:auto}.tox:not([dir=rtl]) .tox-dialog__body-content .accessibility-issue__description{padding:4px 4px 4px 8px}.tox[dir=rtl] .tox-dialog__body-content .accessibility-issue__header .tox-button{margin-right:4px}.tox[dir=rtl] .tox-dialog__body-content .accessibility-issue__header>:nth-last-child(2){margin-right:auto}.tox[dir=rtl] .tox-dialog__body-content .accessibility-issue__description{padding:4px 8px 4px 4px}.tox .mce-codemirror{background:#fff;bottom:0;font-size:13px;left:0;position:absolute;right:0;top:0;z-index:1}.tox .mce-codemirror.tox-inline-codemirror{margin:8px;position:absolute}.tox .tox-advtemplate .tox-form__grid{flex:1}.tox .tox-advtemplate .tox-form__grid>div:first-child{display:flex;flex-direction:column;width:30%}.tox .tox-advtemplate .tox-form__grid>div:first-child>div:nth-child(2){flex-basis:0;flex-grow:1;overflow:auto}@media only screen and (max-width:767px){body:not(.tox-force-desktop) .tox .tox-advtemplate .tox-form__grid>div:first-child{width:100%}}.tox .tox-advtemplate iframe{border-color:#000;border-radius:0;border-style:solid;border-width:1px;margin:0 10px}.tox .tox-anchorbar{display:flex;flex:0 0 auto}.tox .tox-bottom-anchorbar{display:flex;flex:0 0 auto}.tox .tox-bar{display:flex;flex:0 0 auto}.tox .tox-button{background-color:#207ab7;background-image:none;background-position:0 0;background-repeat:repeat;border-color:#207ab7;border-radius:3px;border-style:solid;border-width:1px;box-shadow:none;box-sizing:border-box;color:#fff;cursor:pointer;display:inline-block;font-family:-apple-system,BlinkMacSystemFont,\\\"Segoe UI\\\",Roboto,Oxygen-Sans,Ubuntu,Cantarell,\\\"Helvetica Neue\\\",sans-serif;font-size:14px;font-style:normal;font-weight:700;letter-spacing:normal;line-height:24px;margin:0;outline:0;padding:4px 16px;position:relative;text-align:center;text-decoration:none;text-transform:none;white-space:nowrap}.tox .tox-button::before{border-radius:3px;bottom:-1px;box-shadow:inset 0 0 0 1px #fff,0 0 0 2px #207ab7;content:'';left:-1px;opacity:0;pointer-events:none;position:absolute;right:-1px;top:-1px}.tox .tox-button[disabled]{background-color:#207ab7;background-image:none;border-color:#207ab7;box-shadow:none;color:rgba(255,255,255,.5);cursor:not-allowed}.tox .tox-button:focus:not(:disabled){background-color:#1c6ca1;background-image:none;border-color:#1c6ca1;box-shadow:none;color:#fff}.tox .tox-button:focus:not(:disabled)::before{opacity:1}.tox .tox-button:hover:not(:disabled){background-color:#1c6ca1;background-image:none;border-color:#1c6ca1;box-shadow:none;color:#fff}.tox .tox-button:active:not(:disabled){background-color:#185d8c;background-image:none;border-color:#185d8c;box-shadow:none;color:#fff}.tox .tox-button.tox-button--enabled{background-color:#185d8c;background-image:none;border-color:#185d8c;box-shadow:none;color:#fff}.tox .tox-button.tox-button--enabled[disabled]{background-color:#185d8c;background-image:none;border-color:#185d8c;box-shadow:none;color:rgba(255,255,255,.5);cursor:not-allowed}.tox .tox-button.tox-button--enabled:focus:not(:disabled){background-color:#154f76;background-image:none;border-color:#154f76;box-shadow:none;color:#fff}.tox .tox-button.tox-button--enabled:hover:not(:disabled){background-color:#154f76;background-image:none;border-color:#154f76;box-shadow:none;color:#fff}.tox .tox-button.tox-button--enabled:active:not(:disabled){background-color:#114060;background-image:none;border-color:#114060;box-shadow:none;color:#fff}.tox .tox-button--icon-and-text,.tox .tox-button.tox-button--icon-and-text,.tox .tox-button.tox-button--secondary.tox-button--icon-and-text{display:flex;padding:5px 4px}.tox .tox-button--icon-and-text .tox-icon svg,.tox .tox-button.tox-button--icon-and-text .tox-icon svg,.tox .tox-button.tox-button--secondary.tox-button--icon-and-text .tox-icon svg{display:block;fill:currentColor}.tox .tox-button--secondary{background-color:#3d546f;background-image:none;background-position:0 0;background-repeat:repeat;border-color:#3d546f;border-radius:3px;border-style:solid;border-width:1px;box-shadow:none;color:#fff;font-size:14px;font-style:normal;font-weight:700;letter-spacing:normal;outline:0;padding:4px 16px;text-decoration:none;text-transform:none}.tox .tox-button--secondary[disabled]{background-color:#3d546f;background-image:none;border-color:#3d546f;box-shadow:none;color:rgba(255,255,255,.5)}.tox .tox-button--secondary:focus:not(:disabled){background-color:#34485f;background-image:none;border-color:#34485f;box-shadow:none;color:#fff}.tox .tox-button--secondary:hover:not(:disabled){background-color:#34485f;background-image:none;border-color:#34485f;box-shadow:none;color:#fff}.tox .tox-button--secondary:active:not(:disabled){background-color:#2b3b4e;background-image:none;border-color:#2b3b4e;box-shadow:none;color:#fff}.tox .tox-button--secondary.tox-button--enabled{background-color:#346085;background-image:none;border-color:#346085;box-shadow:none;color:#fff}.tox .tox-button--secondary.tox-button--enabled[disabled]{background-color:#346085;background-image:none;border-color:#346085;box-shadow:none;color:rgba(255,255,255,.5)}.tox .tox-button--secondary.tox-button--enabled:focus:not(:disabled){background-color:#2d5373;background-image:none;border-color:#2d5373;box-shadow:none;color:#fff}.tox .tox-button--secondary.tox-button--enabled:hover:not(:disabled){background-color:#2d5373;background-image:none;border-color:#2d5373;box-shadow:none;color:#fff}.tox .tox-button--secondary.tox-button--enabled:active:not(:disabled){background-color:#264560;background-image:none;border-color:#264560;box-shadow:none;color:#fff}.tox .tox-button--icon,.tox .tox-button.tox-button--icon,.tox .tox-button.tox-button--secondary.tox-button--icon{padding:4px}.tox .tox-button--icon .tox-icon svg,.tox .tox-button.tox-button--icon .tox-icon svg,.tox .tox-button.tox-button--secondary.tox-button--icon .tox-icon svg{display:block;fill:currentColor}.tox .tox-button-link{background:0;border:none;box-sizing:border-box;cursor:pointer;display:inline-block;font-family:-apple-system,BlinkMacSystemFont,\\\"Segoe UI\\\",Roboto,Oxygen-Sans,Ubuntu,Cantarell,\\\"Helvetica Neue\\\",sans-serif;font-size:16px;font-weight:400;line-height:1.3;margin:0;padding:0;white-space:nowrap}.tox .tox-button-link--sm{font-size:14px}.tox .tox-button--naked{background-color:transparent;border-color:transparent;box-shadow:unset;color:#fff}.tox .tox-button--naked[disabled]{background-color:#3d546f;border-color:#3d546f;box-shadow:none;color:rgba(255,255,255,.5)}.tox .tox-button--naked:hover:not(:disabled){background-color:#34485f;border-color:#34485f;box-shadow:none;color:#fff}.tox .tox-button--naked:focus:not(:disabled){background-color:#34485f;border-color:#34485f;box-shadow:none;color:#fff}.tox .tox-button--naked:active:not(:disabled){background-color:#2b3b4e;border-color:#2b3b4e;box-shadow:none;color:#fff}.tox .tox-button--naked .tox-icon svg{fill:currentColor}.tox .tox-button--naked.tox-button--icon:hover:not(:disabled){color:#fff}.tox .tox-checkbox{align-items:center;border-radius:3px;cursor:pointer;display:flex;height:36px;min-width:36px}.tox .tox-checkbox__input{height:1px;overflow:hidden;position:absolute;top:auto;width:1px}.tox .tox-checkbox__icons{align-items:center;border-radius:3px;box-shadow:0 0 0 2px transparent;box-sizing:content-box;display:flex;height:24px;justify-content:center;padding:calc(4px - 1px);width:24px}.tox .tox-checkbox__icons .tox-checkbox-icon__unchecked svg{display:block;fill:rgba(255,255,255,.2)}@media (forced-colors:active){.tox .tox-checkbox__icons .tox-checkbox-icon__unchecked svg{fill:currentColor!important}}.tox .tox-checkbox__icons .tox-checkbox-icon__indeterminate svg{display:none;fill:#207ab7}.tox .tox-checkbox__icons .tox-checkbox-icon__checked svg{display:none;fill:#207ab7}.tox .tox-checkbox--disabled{color:rgba(255,255,255,.5);cursor:not-allowed}.tox .tox-checkbox--disabled .tox-checkbox__icons .tox-checkbox-icon__checked svg{fill:rgba(255,255,255,.5)}.tox .tox-checkbox--disabled .tox-checkbox__icons .tox-checkbox-icon__unchecked svg{fill:rgba(255,255,255,.5)}.tox .tox-checkbox--disabled .tox-checkbox__icons .tox-checkbox-icon__indeterminate svg{fill:rgba(255,255,255,.5)}.tox input.tox-checkbox__input:checked+.tox-checkbox__icons .tox-checkbox-icon__unchecked svg{display:none}.tox input.tox-checkbox__input:checked+.tox-checkbox__icons .tox-checkbox-icon__checked svg{display:block}.tox input.tox-checkbox__input:indeterminate+.tox-checkbox__icons .tox-checkbox-icon__unchecked svg{display:none}.tox input.tox-checkbox__input:indeterminate+.tox-checkbox__icons .tox-checkbox-icon__indeterminate svg{display:block}.tox input.tox-checkbox__input:focus+.tox-checkbox__icons{border-radius:3px;box-shadow:inset 0 0 0 1px #207ab7;padding:calc(4px - 1px)}.tox:not([dir=rtl]) .tox-checkbox__label{margin-left:4px}.tox:not([dir=rtl]) .tox-checkbox__input{left:-10000px}.tox:not([dir=rtl]) .tox-bar .tox-checkbox{margin-left:4px}.tox[dir=rtl] .tox-checkbox__label{margin-right:4px}.tox[dir=rtl] .tox-checkbox__input{right:-10000px}.tox[dir=rtl] .tox-bar .tox-checkbox{margin-right:4px}.tox .tox-collection--toolbar .tox-collection__group{display:flex;padding:0}.tox .tox-collection--grid .tox-collection__group{display:flex;flex-wrap:wrap;max-height:208px;overflow-x:hidden;overflow-y:auto;padding:0}.tox .tox-collection--list .tox-collection__group{border-bottom-width:0;border-color:#1a1a1a;border-left-width:0;border-right-width:0;border-style:solid;border-top-width:1px;padding:4px 0}.tox .tox-collection--list .tox-collection__group:first-child{border-top-width:0}.tox .tox-collection__group-heading{background-color:#333;color:#fff;cursor:default;font-size:12px;font-style:normal;font-weight:400;margin-bottom:4px;margin-top:-4px;padding:4px 8px;text-transform:none;-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}.tox .tox-collection__item{align-items:center;border-radius:3px;color:#fff;display:flex;-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}.tox .tox-collection--list .tox-collection__item{padding:4px 8px}.tox .tox-collection--toolbar .tox-collection__item{border-radius:3px;padding:4px}.tox .tox-collection--grid .tox-collection__item{border-radius:3px;padding:4px}.tox .tox-collection--list .tox-collection__item--enabled{background-color:#2b3b4e;color:#fff}.tox .tox-collection--list .tox-collection__item--active{background-color:#4a5562}.tox .tox-collection--toolbar .tox-collection__item--enabled,.tox .tox-collection--toolbar .tox-collection__item--enabled.tox-collection__item--active,.tox .tox-collection--toolbar .tox-collection__item--enabled.tox-collection__item--active:hover{background-color:#757d87;color:#fff}@media (forced-colors:active){.tox .tox-collection--toolbar .tox-collection__item--enabled,.tox .tox-collection--toolbar .tox-collection__item--enabled.tox-collection__item--active,.tox .tox-collection--toolbar .tox-collection__item--enabled.tox-collection__item--active:hover{border-radius:3px;outline:solid 1px}}.tox .tox-collection--toolbar .tox-collection__item--active{background-color:#2b3b4e;position:relative}.tox .tox-collection--toolbar .tox-collection__item--active:hover{background-color:#4a5562;color:#fff}.tox .tox-collection--toolbar .tox-collection__item--active:focus{background-color:#4a5562;color:#fff}.tox .tox-collection--toolbar .tox-collection__item--active:focus::after{border-radius:3px;bottom:0;box-shadow:0 0 0 0 transparent;content:'';left:0;position:absolute;right:0;top:0}@media (forced-colors:active){.tox .tox-collection--toolbar .tox-collection__item--active:focus::after{border:2px solid highlight}}.tox .tox-collection--grid .tox-collection__item--enabled{background-color:#757d87;color:#fff}.tox .tox-collection--grid .tox-collection__item--active:not(.tox-collection__item--state-disabled){background-color:#4a5562;color:#fff;position:relative;z-index:1}.tox .tox-collection--grid .tox-collection__item--active:not(.tox-collection__item--state-disabled):focus::after{border-radius:3px;bottom:0;box-shadow:0 0 0 0 transparent inset;content:'';left:0;position:absolute;right:0;top:0}@media (forced-colors:active){.tox .tox-collection--grid .tox-collection__item--active:not(.tox-collection__item--state-disabled):focus::after{border:2px solid highlight}}.tox .tox-collection--list .tox-collection__item--active:not(.tox-collection__item--state-disabled){color:#fff}@media (forced-colors:active){.tox .tox-collection--list .tox-collection__item--active:not(.tox-collection__item--state-disabled){border:solid 1px}}.tox .tox-collection--toolbar .tox-collection__item--active:not(.tox-collection__item--state-disabled){color:#fff}@media (forced-colors:active){.tox .tox-collection--toolbar .tox-collection__item--active:not(.tox-collection__item--state-disabled):hover{border-radius:3px;outline:solid 1px}}.tox .tox-collection__item-checkmark,.tox .tox-collection__item-icon{align-items:center;display:flex;height:24px;justify-content:center;width:24px}.tox .tox-collection__item-checkmark svg,.tox .tox-collection__item-icon svg{fill:currentColor}.tox .tox-collection--toolbar-lg .tox-collection__item-icon{height:48px;width:48px}.tox .tox-collection__item-label{color:currentColor;display:inline-block;flex:1;font-size:14px;font-style:normal;font-weight:400;line-height:24px;max-width:100%;text-transform:none;word-break:break-all}.tox .tox-collection__item-accessory{color:currentColor;display:inline-block;font-size:14px;height:24px;line-height:24px;text-transform:none}.tox .tox-collection__item-caret{align-items:center;display:flex;min-height:24px}.tox .tox-collection__item-caret::after{content:'';font-size:0;min-height:inherit}.tox .tox-collection__item-caret svg{fill:currentColor}.tox .tox-collection__item--state-disabled{background-color:transparent;color:rgba(255,255,255,.5);cursor:not-allowed}.tox .tox-collection__item--state-disabled .tox-collection__item-caret svg{fill:rgba(255,255,255,.5)}.tox .tox-collection--list .tox-collection__item:not(.tox-collection__item--enabled) .tox-collection__item-checkmark svg{display:none}.tox .tox-collection--list .tox-collection__item:not(.tox-collection__item--enabled) .tox-collection__item-accessory+.tox-collection__item-checkmark{display:none}.tox .tox-collection--horizontal{background-color:#2b3b4e;border:1px solid #1a1a1a;border-radius:3px;box-shadow:0 0 2px 0 rgba(42,55,70,.2),0 4px 8px 0 rgba(42,55,70,.15);display:flex;flex:0 0 auto;flex-shrink:0;flex-wrap:nowrap;margin-bottom:0;overflow-x:auto;padding:0}.tox .tox-collection--horizontal .tox-collection__group{align-items:center;display:flex;flex-wrap:nowrap;margin:0;padding:0 4px}.tox .tox-collection--horizontal .tox-collection__item{height:34px;margin:3px 0 2px 0;padding:0 4px}.tox .tox-collection--horizontal .tox-collection__item-label{white-space:nowrap}.tox .tox-collection--horizontal .tox-collection__item-caret{margin-left:4px}.tox .tox-collection__item-container{display:flex}.tox .tox-collection__item-container--row{align-items:center;flex:1 1 auto;flex-direction:row}.tox .tox-collection__item-container--row.tox-collection__item-container--align-left{margin-right:auto}.tox .tox-collection__item-container--row.tox-collection__item-container--align-right{justify-content:flex-end;margin-left:auto}.tox .tox-collection__item-container--row.tox-collection__item-container--valign-top{align-items:flex-start;margin-bottom:auto}.tox .tox-collection__item-container--row.tox-collection__item-container--valign-middle{align-items:center}.tox .tox-collection__item-container--row.tox-collection__item-container--valign-bottom{align-items:flex-end;margin-top:auto}.tox .tox-collection__item-container--column{align-self:center;flex:1 1 auto;flex-direction:column}.tox .tox-collection__item-container--column.tox-collection__item-container--align-left{align-items:flex-start}.tox .tox-collection__item-container--column.tox-collection__item-container--align-right{align-items:flex-end}.tox .tox-collection__item-container--column.tox-collection__item-container--valign-top{align-self:flex-start}.tox .tox-collection__item-container--column.tox-collection__item-container--valign-middle{align-self:center}.tox .tox-collection__item-container--column.tox-collection__item-container--valign-bottom{align-self:flex-end}.tox:not([dir=rtl]) .tox-collection--horizontal .tox-collection__group:not(:last-of-type){border-right:1px solid #000}.tox:not([dir=rtl]) .tox-collection--list .tox-collection__item>:not(:first-child){margin-left:8px}.tox:not([dir=rtl]) .tox-collection--list .tox-collection__item>.tox-collection__item-label:first-child{margin-left:4px}.tox:not([dir=rtl]) .tox-collection__item-accessory{margin-left:16px;text-align:right}.tox:not([dir=rtl]) .tox-collection .tox-collection__item-caret{margin-left:16px}.tox[dir=rtl] .tox-collection--horizontal .tox-collection__group:not(:last-of-type){border-left:1px solid #000}.tox[dir=rtl] .tox-collection--list .tox-collection__item>:not(:first-child){margin-right:8px}.tox[dir=rtl] .tox-collection--list .tox-collection__item>.tox-collection__item-label:first-child{margin-right:4px}.tox[dir=rtl] .tox-collection__item-accessory{margin-right:16px;text-align:left}.tox[dir=rtl] .tox-collection .tox-collection__item-caret{margin-right:16px;transform:rotateY(180deg)}.tox[dir=rtl] .tox-collection--horizontal .tox-collection__item-caret{margin-right:4px}@media (forced-colors:active){.tox .tox-hue-slider,.tox .tox-rgb-form .tox-rgba-preview{background-color:currentColor!important;border:1px solid highlight!important;forced-color-adjust:none}}.tox .tox-color-picker-container{display:flex;flex-direction:row;height:225px;margin:0}.tox .tox-sv-palette{box-sizing:border-box;display:flex;height:100%}.tox .tox-sv-palette-spectrum{height:100%}.tox .tox-sv-palette,.tox .tox-sv-palette-spectrum{width:225px}.tox .tox-sv-palette-thumb{background:0 0;border:1px solid #000;border-radius:50%;box-sizing:content-box;height:12px;position:absolute;width:12px}.tox .tox-sv-palette-inner-thumb{border:1px solid #fff;border-radius:50%;height:10px;position:absolute;width:10px}.tox .tox-hue-slider{box-sizing:border-box;height:100%;width:25px}.tox .tox-hue-slider-spectrum{background:linear-gradient(to bottom,red,#ff0080,#f0f,#8000ff,#00f,#0080ff,#0ff,#00ff80,#0f0,#80ff00,#ff0,#ff8000,red);height:100%;width:100%}.tox .tox-hue-slider,.tox .tox-hue-slider-spectrum{width:20px}.tox .tox-hue-slider-spectrum:focus,.tox .tox-sv-palette-spectrum:focus{outline:#08f solid}.tox .tox-hue-slider-thumb{background:#fff;border:1px solid #000;box-sizing:content-box;height:4px;width:100%}.tox .tox-rgb-form{display:flex;flex-direction:column;justify-content:space-between}.tox .tox-rgb-form div{align-items:center;display:flex;justify-content:space-between;margin-bottom:5px;width:inherit}.tox .tox-rgb-form input{width:6em}.tox .tox-rgb-form input.tox-invalid{border:1px solid red!important}.tox .tox-rgb-form .tox-rgba-preview{border:1px solid #000;flex-grow:2;margin-bottom:0}.tox:not([dir=rtl]) .tox-sv-palette{margin-right:15px}.tox:not([dir=rtl]) .tox-hue-slider{margin-right:15px}.tox:not([dir=rtl]) .tox-hue-slider-thumb{margin-left:-1px}.tox:not([dir=rtl]) .tox-rgb-form label{margin-right:.5em}.tox[dir=rtl] .tox-sv-palette{margin-left:15px}.tox[dir=rtl] .tox-hue-slider{margin-left:15px}.tox[dir=rtl] .tox-hue-slider-thumb{margin-right:-1px}.tox[dir=rtl] .tox-rgb-form label{margin-left:.5em}.tox .tox-toolbar .tox-swatches,.tox .tox-toolbar__overflow .tox-swatches,.tox .tox-toolbar__primary .tox-swatches{margin:2px 0 3px 4px}.tox .tox-collection--list .tox-collection__group .tox-swatches-menu{border:0;margin:-4px 0}.tox .tox-swatches__row{display:flex}@media (forced-colors:active){.tox .tox-swatches__row{forced-color-adjust:none}}.tox .tox-swatch{height:30px;transition:transform .15s,box-shadow .15s;width:30px}.tox .tox-swatch:focus,.tox .tox-swatch:hover{box-shadow:0 0 0 1px rgba(127,127,127,.3) inset;transform:scale(.8)}.tox .tox-swatch--remove{align-items:center;display:flex;justify-content:center}.tox .tox-swatch--remove svg path{stroke:#e74c3c}.tox .tox-swatches__picker-btn{align-items:center;background-color:transparent;border:0;cursor:pointer;display:flex;height:30px;justify-content:center;outline:0;padding:0;width:30px}.tox .tox-swatches__picker-btn svg{fill:#fff;height:24px;width:24px}.tox .tox-swatches__picker-btn:hover{background:#4a5562}.tox div.tox-swatch:not(.tox-swatch--remove) svg{display:none;fill:#fff;height:24px;margin:calc((30px - 24px)/ 2) calc((30px - 24px)/ 2);width:24px}.tox div.tox-swatch:not(.tox-swatch--remove) svg path{fill:#fff;paint-order:stroke;stroke:#222f3e;stroke-width:2px}.tox div.tox-swatch:not(.tox-swatch--remove).tox-collection__item--enabled svg{display:block}.tox:not([dir=rtl]) .tox-swatches__picker-btn{margin-left:auto}.tox[dir=rtl] .tox-swatches__picker-btn{margin-right:auto}.tox .tox-comment-thread{background:#2b3b4e;position:relative}.tox .tox-comment-thread>:not(:first-child){margin-top:8px}.tox .tox-comment{background:#2b3b4e;border:1px solid #000;border-radius:3px;box-shadow:0 4px 8px 0 rgba(42,55,70,.1);padding:8px 8px 16px 8px;position:relative}.tox .tox-comment__header{align-items:center;color:#fff;display:flex;justify-content:space-between}.tox .tox-comment__date{color:#fff;font-size:12px;line-height:18px}.tox .tox-comment__body{color:#fff;font-size:14px;font-style:normal;font-weight:400;line-height:1.3;margin-top:8px;position:relative;text-transform:initial}.tox .tox-comment__body textarea{resize:none;white-space:normal;width:100%}.tox .tox-comment__expander{padding-top:8px}.tox .tox-comment__expander p{color:rgba(255,255,255,.5);font-size:14px;font-style:normal}.tox .tox-comment__body p{margin:0}.tox .tox-comment__buttonspacing{padding-top:16px;text-align:center}.tox .tox-comment-thread__overlay::after{background:#2b3b4e;bottom:0;content:\\\"\\\";display:flex;left:0;opacity:.9;position:absolute;right:0;top:0;z-index:5}.tox .tox-comment__reply{display:flex;flex-shrink:0;flex-wrap:wrap;justify-content:flex-end;margin-top:8px}.tox .tox-comment__reply>:first-child{margin-bottom:8px;width:100%}.tox .tox-comment__edit{display:flex;flex-wrap:wrap;justify-content:flex-end;margin-top:16px}.tox .tox-comment__gradient::after{background:linear-gradient(rgba(43,59,78,0),#2b3b4e);bottom:0;content:\\\"\\\";display:block;height:5em;margin-top:-40px;position:absolute;width:100%}.tox .tox-comment__overlay{background:#2b3b4e;bottom:0;display:flex;flex-direction:column;flex-grow:1;left:0;opacity:.9;position:absolute;right:0;text-align:center;top:0;z-index:5}.tox .tox-comment__loading-text{align-items:center;color:#fff;display:flex;flex-direction:column;position:relative}.tox .tox-comment__loading-text>div{padding-bottom:16px}.tox .tox-comment__overlaytext{bottom:0;flex-direction:column;font-size:14px;left:0;padding:1em;position:absolute;right:0;top:0;z-index:10}.tox .tox-comment__overlaytext p{background-color:#2b3b4e;box-shadow:0 0 8px 8px #2b3b4e;color:#fff;text-align:center}.tox .tox-comment__overlaytext div:nth-of-type(2){font-size:.8em}.tox .tox-comment__busy-spinner{align-items:center;background-color:#2b3b4e;bottom:0;display:flex;justify-content:center;left:0;position:absolute;right:0;top:0;z-index:20}.tox .tox-comment__scroll{display:flex;flex-direction:column;flex-shrink:1;overflow:auto}.tox .tox-conversations{margin:8px}.tox:not([dir=rtl]) .tox-comment__edit{margin-left:8px}.tox:not([dir=rtl]) .tox-comment__buttonspacing>:last-child,.tox:not([dir=rtl]) .tox-comment__edit>:last-child,.tox:not([dir=rtl]) .tox-comment__reply>:last-child{margin-left:8px}.tox[dir=rtl] .tox-comment__edit{margin-right:8px}.tox[dir=rtl] .tox-comment__buttonspacing>:last-child,.tox[dir=rtl] .tox-comment__edit>:last-child,.tox[dir=rtl] .tox-comment__reply>:last-child{margin-right:8px}.tox .tox-user{align-items:center;display:flex}.tox .tox-user__avatar svg{fill:rgba(255,255,255,.5)}.tox .tox-user__avatar img{border-radius:50%;height:36px;object-fit:cover;vertical-align:middle;width:36px}.tox .tox-user__name{color:#fff;font-size:14px;font-style:normal;font-weight:700;line-height:18px;text-transform:none}.tox:not([dir=rtl]) .tox-user__avatar img,.tox:not([dir=rtl]) .tox-user__avatar svg{margin-right:8px}.tox:not([dir=rtl]) .tox-user__avatar+.tox-user__name{margin-left:8px}.tox[dir=rtl] .tox-user__avatar img,.tox[dir=rtl] .tox-user__avatar svg{margin-left:8px}.tox[dir=rtl] .tox-user__avatar+.tox-user__name{margin-right:8px}.tox .tox-dialog-wrap{align-items:center;bottom:0;display:flex;justify-content:center;left:0;position:fixed;right:0;top:0;z-index:1100}.tox .tox-dialog-wrap__backdrop{background-color:rgba(34,47,62,.75);bottom:0;left:0;position:absolute;right:0;top:0;z-index:1}.tox .tox-dialog-wrap__backdrop--opaque{background-color:#222f3e}.tox .tox-dialog{background-color:#2b3b4e;border-color:#000;border-radius:3px;border-style:solid;border-width:1px;box-shadow:0 16px 16px -10px rgba(42,55,70,.15),0 0 40px 1px rgba(42,55,70,.15);display:flex;flex-direction:column;max-height:100%;max-width:480px;overflow:hidden;position:relative;width:95vw;z-index:2}@media only screen and (max-width:767px){body:not(.tox-force-desktop) .tox .tox-dialog{align-self:flex-start;margin:8px auto;max-height:calc(100vh - 8px * 2);width:calc(100vw - 16px)}}.tox .tox-dialog-inline{z-index:1100}.tox .tox-dialog__header{align-items:center;background-color:#2b3b4e;border-bottom:none;color:#fff;display:flex;font-size:16px;justify-content:space-between;padding:8px 16px 0 16px;position:relative}.tox .tox-dialog__header .tox-button{z-index:1}.tox .tox-dialog__draghandle{cursor:grab;height:100%;left:0;position:absolute;top:0;width:100%}.tox .tox-dialog__draghandle:active{cursor:grabbing}.tox .tox-dialog__dismiss{margin-left:auto}.tox .tox-dialog__title{font-family:-apple-system,BlinkMacSystemFont,\\\"Segoe UI\\\",Roboto,Oxygen-Sans,Ubuntu,Cantarell,\\\"Helvetica Neue\\\",sans-serif;font-size:20px;font-style:normal;font-weight:400;line-height:1.3;margin:0;text-transform:none}.tox .tox-dialog__body{color:#fff;display:flex;flex:1;font-size:16px;font-style:normal;font-weight:400;line-height:1.3;min-width:0;text-align:left;text-transform:none}@media only screen and (max-width:767px){body:not(.tox-force-desktop) .tox .tox-dialog__body{flex-direction:column}}.tox .tox-dialog__body-nav{align-items:flex-start;display:flex;flex-direction:column;flex-shrink:0;padding:16px 16px}@media only screen and (min-width:768px){.tox .tox-dialog__body-nav{max-width:11em}}@media only screen and (max-width:767px){body:not(.tox-force-desktop) .tox .tox-dialog__body-nav{flex-direction:row;-webkit-overflow-scrolling:touch;overflow-x:auto;padding-bottom:0}}.tox .tox-dialog__body-nav-item{border-bottom:2px solid transparent;color:rgba(255,255,255,.5);display:inline-block;flex-shrink:0;font-size:14px;line-height:1.3;margin-bottom:8px;max-width:13em;text-decoration:none}.tox .tox-dialog__body-nav-item:focus{background-color:rgba(32,122,183,.1)}.tox .tox-dialog__body-nav-item--active{border-bottom:2px solid #207ab7;color:#207ab7}@media (forced-colors:active){.tox .tox-dialog__body-nav-item--active{border-bottom:2px solid highlight;color:highlight}}.tox .tox-dialog__body-content{box-sizing:border-box;display:flex;flex:1;flex-direction:column;max-height:min(650px,calc(100vh - 110px));overflow:auto;-webkit-overflow-scrolling:touch;padding:16px 16px}.tox .tox-dialog__body-content>*{margin-bottom:0;margin-top:16px}.tox .tox-dialog__body-content>:first-child{margin-top:0}.tox .tox-dialog__body-content>:last-child{margin-bottom:0}.tox .tox-dialog__body-content>:only-child{margin-bottom:0;margin-top:0}.tox .tox-dialog__body-content a{color:#207ab7;cursor:pointer;text-decoration:underline}.tox .tox-dialog__body-content a:focus,.tox .tox-dialog__body-content a:hover{color:#114060;text-decoration:underline}.tox .tox-dialog__body-content a:focus-visible{border-radius:1px;outline:2px solid #207ab7;outline-offset:2px}.tox .tox-dialog__body-content a:active{color:#092335;text-decoration:underline}.tox .tox-dialog__body-content svg{fill:#fff}.tox .tox-dialog__body-content strong{font-weight:700}.tox .tox-dialog__body-content ul{list-style-type:disc}.tox .tox-dialog__body-content dd,.tox .tox-dialog__body-content ol,.tox .tox-dialog__body-content ul{padding-inline-start:2.5rem}.tox .tox-dialog__body-content dl,.tox .tox-dialog__body-content ol,.tox .tox-dialog__body-content ul{margin-bottom:16px}.tox .tox-dialog__body-content dd,.tox .tox-dialog__body-content dl,.tox .tox-dialog__body-content dt,.tox .tox-dialog__body-content ol,.tox .tox-dialog__body-content ul{display:block;margin-inline-end:0;margin-inline-start:0}.tox .tox-dialog__body-content .tox-form__group h1{color:#fff;font-size:20px;font-style:normal;font-weight:700;letter-spacing:normal;margin-bottom:16px;margin-top:2rem;text-transform:none}.tox .tox-dialog__body-content .tox-form__group h2{color:#fff;font-size:16px;font-style:normal;font-weight:700;letter-spacing:normal;margin-bottom:16px;margin-top:2rem;text-transform:none}.tox .tox-dialog__body-content .tox-form__group p{margin-bottom:16px}.tox .tox-dialog__body-content .tox-form__group h1:first-child,.tox .tox-dialog__body-content .tox-form__group h2:first-child,.tox .tox-dialog__body-content .tox-form__group p:first-child{margin-top:0}.tox .tox-dialog__body-content .tox-form__group h1:last-child,.tox .tox-dialog__body-content .tox-form__group h2:last-child,.tox .tox-dialog__body-content .tox-form__group p:last-child{margin-bottom:0}.tox .tox-dialog__body-content .tox-form__group h1:only-child,.tox .tox-dialog__body-content .tox-form__group h2:only-child,.tox .tox-dialog__body-content .tox-form__group p:only-child{margin-bottom:0;margin-top:0}.tox .tox-dialog__body-content .tox-form__group .tox-label.tox-label--center{text-align:center}.tox .tox-dialog__body-content .tox-form__group .tox-label.tox-label--end{text-align:end}.tox .tox-dialog--width-lg{height:650px;max-width:1200px}.tox .tox-dialog--fullscreen{height:100%;max-width:100%}.tox .tox-dialog--fullscreen .tox-dialog__body-content{max-height:100%}.tox .tox-dialog--width-md{max-width:800px}.tox .tox-dialog--width-md .tox-dialog__body-content{overflow:auto}.tox .tox-dialog__body-content--centered{text-align:center}.tox .tox-dialog__footer{align-items:center;background-color:#2b3b4e;border-top:1px solid #000;display:flex;justify-content:space-between;padding:8px 16px}.tox .tox-dialog__footer-end,.tox .tox-dialog__footer-start{display:flex}.tox .tox-dialog__busy-spinner{align-items:center;background-color:rgba(34,47,62,.75);bottom:0;display:flex;justify-content:center;left:0;position:absolute;right:0;top:0;z-index:3}.tox .tox-dialog__table{border-collapse:collapse;width:100%}.tox .tox-dialog__table thead th{font-weight:700;padding-bottom:8px}.tox .tox-dialog__table thead th:first-child{padding-right:8px}.tox .tox-dialog__table tbody tr{border-bottom:1px solid #000}.tox .tox-dialog__table tbody tr:last-child{border-bottom:none}.tox .tox-dialog__table td{padding-bottom:8px;padding-top:8px}.tox .tox-dialog__table td:first-child{padding-right:8px}.tox .tox-dialog__iframe{min-height:200px}.tox .tox-dialog__iframe.tox-dialog__iframe--opaque{background:#fff}.tox .tox-navobj-bordered{position:relative}.tox .tox-navobj-bordered::before{border:1px solid #000;border-radius:3px;content:'';inset:0;opacity:1;pointer-events:none;position:absolute;z-index:1}.tox .tox-navobj-bordered iframe{border-radius:3px}.tox .tox-navobj-bordered-focus.tox-navobj-bordered::before{border-color:#207ab7;box-shadow:none;outline:2px solid rgba(32,122,183,.25)}.tox .tox-dialog__popups{position:absolute;width:100%;z-index:1100}.tox .tox-dialog__body-iframe{display:flex;flex:1;flex-direction:column}.tox .tox-dialog__body-iframe .tox-navobj{display:flex;flex:1}.tox .tox-dialog__body-iframe .tox-navobj :nth-child(2){flex:1;height:100%}.tox .tox-dialog-dock-fadeout{opacity:0;visibility:hidden}.tox .tox-dialog-dock-fadein{opacity:1;visibility:visible}.tox .tox-dialog-dock-transition{transition:visibility 0s linear .3s,opacity .3s ease}.tox .tox-dialog-dock-transition.tox-dialog-dock-fadein{transition-delay:0s}@media only screen and (max-width:767px){body:not(.tox-force-desktop) .tox:not([dir=rtl]) .tox-dialog__body-nav{margin-right:0}}@media only screen and (max-width:767px){body:not(.tox-force-desktop) .tox:not([dir=rtl]) .tox-dialog__body-nav-item:not(:first-child){margin-left:8px}}.tox:not([dir=rtl]) .tox-dialog__footer .tox-dialog__footer-end>*,.tox:not([dir=rtl]) .tox-dialog__footer .tox-dialog__footer-start>*{margin-left:8px}.tox[dir=rtl] .tox-dialog__body{text-align:right}@media only screen and (max-width:767px){body:not(.tox-force-desktop) .tox[dir=rtl] .tox-dialog__body-nav{margin-left:0}}@media only screen and (max-width:767px){body:not(.tox-force-desktop) .tox[dir=rtl] .tox-dialog__body-nav-item:not(:first-child){margin-right:8px}}.tox[dir=rtl] .tox-dialog__footer .tox-dialog__footer-end>*,.tox[dir=rtl] .tox-dialog__footer .tox-dialog__footer-start>*{margin-right:8px}body.tox-dialog__disable-scroll{overflow:hidden}.tox .tox-dropzone-container{display:flex;flex:1}.tox .tox-dropzone{align-items:center;background:#fff;border:2px dashed #000;box-sizing:border-box;display:flex;flex-direction:column;flex-grow:1;justify-content:center;min-height:100px;padding:10px}.tox .tox-dropzone p{color:rgba(255,255,255,.5);margin:0 0 16px 0}.tox .tox-edit-area{display:flex;flex:1;overflow:hidden;position:relative}.tox .tox-edit-area::before{border:0 solid transparent;border-radius:4px;content:'';inset:0;opacity:0;pointer-events:none;position:absolute;transition:opacity .15s;z-index:1}@media (forced-colors:active){.tox .tox-edit-area::before{border:0 solid highlight}}.tox .tox-edit-area__iframe{background-color:#fff;border:0;box-sizing:border-box;flex:1;height:100%;position:absolute;width:100%}.tox.tox-edit-focus .tox-edit-area::before{opacity:1}.tox.tox-inline-edit-area{border:1px dotted #000}.tox .tox-editor-container{display:flex;flex:1 1 auto;flex-direction:column;overflow:hidden}.tox .tox-editor-header{display:grid;grid-template-columns:1fr min-content;z-index:2}.tox:not(.tox-tinymce-inline) .tox-editor-header{background-color:#222f3e;border-bottom:none;box-shadow:none;padding:4px 0}.tox:not(.tox-tinymce-inline) .tox-editor-header:not(.tox-editor-dock-transition){transition:box-shadow .5s}.tox:not(.tox-tinymce-inline).tox-tinymce--toolbar-bottom .tox-editor-header{border-top:1px solid #000;box-shadow:none}.tox:not(.tox-tinymce-inline).tox-tinymce--toolbar-sticky-on .tox-editor-header{background-color:#222f3e;box-shadow:0 4px 4px -3px rgba(0,0,0,.25);padding:4px 0}.tox:not(.tox-tinymce-inline).tox-tinymce--toolbar-sticky-on.tox-tinymce--toolbar-bottom .tox-editor-header{box-shadow:0 4px 4px -3px rgba(0,0,0,.25)}.tox.tox:not(.tox-tinymce-inline) .tox-editor-header.tox-editor-header--empty{background:0 0;border:none;box-shadow:none;padding:0}.tox-editor-dock-fadeout{opacity:0;visibility:hidden}.tox-editor-dock-fadein{opacity:1;visibility:visible}.tox-editor-dock-transition{transition:visibility 0s linear .25s,opacity .25s ease}.tox-editor-dock-transition.tox-editor-dock-fadein{transition-delay:0s}.tox .tox-control-wrap{flex:1;position:relative}.tox .tox-control-wrap:not(.tox-control-wrap--status-invalid) .tox-control-wrap__status-icon-invalid,.tox .tox-control-wrap:not(.tox-control-wrap--status-unknown) .tox-control-wrap__status-icon-unknown,.tox .tox-control-wrap:not(.tox-control-wrap--status-valid) .tox-control-wrap__status-icon-valid{display:none}.tox .tox-control-wrap svg{display:block}.tox .tox-control-wrap__status-icon-wrap{position:absolute;top:50%;transform:translateY(-50%)}.tox .tox-control-wrap__status-icon-invalid svg{fill:#c00}.tox .tox-control-wrap__status-icon-unknown svg{fill:orange}.tox .tox-control-wrap__status-icon-valid svg{fill:green}.tox:not([dir=rtl]) .tox-control-wrap--status-invalid .tox-textfield,.tox:not([dir=rtl]) .tox-control-wrap--status-unknown .tox-textfield,.tox:not([dir=rtl]) .tox-control-wrap--status-valid .tox-textfield{padding-right:32px}.tox:not([dir=rtl]) .tox-control-wrap__status-icon-wrap{right:4px}.tox[dir=rtl] .tox-control-wrap--status-invalid .tox-textfield,.tox[dir=rtl] .tox-control-wrap--status-unknown .tox-textfield,.tox[dir=rtl] .tox-control-wrap--status-valid .tox-textfield{padding-left:32px}.tox[dir=rtl] .tox-control-wrap__status-icon-wrap{left:4px}.tox .tox-custom-preview{border-color:#000;border-radius:3px;border-style:solid;border-width:1px;flex:1;padding:8px}.tox .tox-autocompleter{max-width:25em}.tox .tox-autocompleter .tox-menu{box-sizing:border-box;max-width:25em}.tox .tox-autocompleter .tox-autocompleter-highlight{font-weight:700}.tox .tox-color-input{display:flex;position:relative;z-index:1}.tox .tox-color-input .tox-textfield{z-index:-1}.tox .tox-color-input span{border-color:rgba(42,55,70,.2);border-radius:3px;border-style:solid;border-width:1px;box-shadow:none;box-sizing:border-box;height:24px;position:absolute;top:6px;width:24px}@media (forced-colors:active){.tox .tox-color-input span{border-color:currentColor;border-width:2px!important;forced-color-adjust:none}}.tox .tox-color-input span:focus:not([aria-disabled=true]),.tox .tox-color-input span:hover:not([aria-disabled=true]){border-color:#207ab7;cursor:pointer}.tox .tox-color-input span::before{background-image:linear-gradient(45deg,rgba(255,255,255,.25) 25%,transparent 25%),linear-gradient(-45deg,rgba(255,255,255,.25) 25%,transparent 25%),linear-gradient(45deg,transparent 75%,rgba(255,255,255,.25) 75%),linear-gradient(-45deg,transparent 75%,rgba(255,255,255,.25) 75%);background-position:0 0,0 6px,6px -6px,-6px 0;background-size:12px 12px;border:1px solid #2b3b4e;border-radius:3px;box-sizing:border-box;content:'';height:24px;left:-1px;position:absolute;top:-1px;width:24px;z-index:-1}@media (forced-colors:active){.tox .tox-color-input span::before{border:none}}.tox .tox-color-input span[aria-disabled=true]{cursor:not-allowed}.tox:not([dir=rtl]) .tox-color-input .tox-textfield{padding-left:36px}.tox:not([dir=rtl]) .tox-color-input span{left:6px}.tox[dir=rtl] .tox-color-input .tox-textfield{padding-right:36px}.tox[dir=rtl] .tox-color-input span{right:6px}.tox .tox-label,.tox .tox-toolbar-label{color:rgba(255,255,255,.5);display:block;font-size:14px;font-style:normal;font-weight:400;line-height:1.3;padding:0 8px 0 0;text-transform:none;white-space:nowrap}.tox .tox-toolbar-label{padding:0 8px}.tox[dir=rtl] .tox-label{padding:0 0 0 8px}.tox .tox-form{display:flex;flex:1;flex-direction:column}.tox .tox-form__group{box-sizing:border-box;margin-bottom:4px}.tox .tox-form-group--maximize{flex:1}.tox .tox-form__group--error{color:#c00}.tox .tox-form__group--collection{display:flex}.tox .tox-form__grid{display:flex;flex-direction:row;flex-wrap:wrap;justify-content:space-between}.tox .tox-form__grid--2col>.tox-form__group{width:calc(50% - (8px / 2))}.tox .tox-form__grid--3col>.tox-form__group{width:calc(100% / 3 - (8px / 2))}.tox .tox-form__grid--4col>.tox-form__group{width:calc(25% - (8px / 2))}.tox .tox-form__controls-h-stack{align-items:center;display:flex}.tox .tox-form__group--inline{align-items:center;display:flex}.tox .tox-form__group--stretched{display:flex;flex:1;flex-direction:column}.tox .tox-form__group--stretched .tox-textarea{flex:1}.tox .tox-form__group--stretched .tox-navobj{display:flex;flex:1}.tox .tox-form__group--stretched .tox-navobj :nth-child(2){flex:1;height:100%}.tox:not([dir=rtl]) .tox-form__controls-h-stack>:not(:first-child){margin-left:4px}.tox[dir=rtl] .tox-form__controls-h-stack>:not(:first-child){margin-right:4px}.tox .tox-lock.tox-locked .tox-lock-icon__unlock,.tox .tox-lock:not(.tox-locked) .tox-lock-icon__lock{display:none}.tox .tox-listboxfield .tox-listbox--select,.tox .tox-textarea,.tox .tox-textarea-wrap .tox-textarea:focus,.tox .tox-textfield,.tox .tox-toolbar-textfield{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#2b3b4e;border-color:#000;border-radius:3px;border-style:solid;border-width:1px;box-shadow:none;box-sizing:border-box;color:#fff;font-family:-apple-system,BlinkMacSystemFont,\\\"Segoe UI\\\",Roboto,Oxygen-Sans,Ubuntu,Cantarell,\\\"Helvetica Neue\\\",sans-serif;font-size:16px;line-height:24px;margin:0;min-height:34px;outline:0;padding:5px 4.75px;resize:none;width:100%}.tox .tox-textarea[disabled],.tox .tox-textfield[disabled]{background-color:#222f3e;color:rgba(255,255,255,.85);cursor:not-allowed}.tox .tox-custom-editor:focus-within,.tox .tox-listboxfield .tox-listbox--select:focus,.tox .tox-textarea-wrap:focus-within,.tox .tox-textarea:focus,.tox .tox-textfield:focus{background-color:#2b3b4e;border-color:#207ab7;box-shadow:none;outline:2px solid rgba(32,122,183,.25)}.tox .tox-toolbar-textfield{border-width:0;margin-bottom:3px;margin-top:2px;max-width:250px}.tox .tox-naked-btn{background-color:transparent;border:0;border-color:transparent;box-shadow:unset;color:#207ab7;cursor:pointer;display:block;margin:0;padding:0}.tox .tox-naked-btn svg{display:block;fill:#fff}.tox:not([dir=rtl]) .tox-toolbar-textfield+*{margin-left:4px}.tox[dir=rtl] .tox-toolbar-textfield+*{margin-right:4px}.tox .tox-listboxfield{cursor:pointer;position:relative}.tox .tox-listboxfield .tox-listbox--select[disabled]{background-color:#19232e;color:rgba(255,255,255,.85);cursor:not-allowed}.tox .tox-listbox__select-label{cursor:default;flex:1;margin:0 4px}.tox .tox-listbox__select-chevron{align-items:center;display:flex;justify-content:center;width:16px}.tox .tox-listbox__select-chevron svg{fill:#fff}@media (forced-colors:active){.tox .tox-listbox__select-chevron svg{fill:currentColor!important}}.tox .tox-listboxfield .tox-listbox--select{align-items:center;display:flex}.tox:not([dir=rtl]) .tox-listboxfield svg{right:8px}.tox[dir=rtl] .tox-listboxfield svg{left:8px}.tox .tox-selectfield{cursor:pointer;position:relative}.tox .tox-selectfield select{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#2b3b4e;border-color:#000;border-radius:3px;border-style:solid;border-width:1px;box-shadow:none;box-sizing:border-box;color:#fff;font-family:-apple-system,BlinkMacSystemFont,\\\"Segoe UI\\\",Roboto,Oxygen-Sans,Ubuntu,Cantarell,\\\"Helvetica Neue\\\",sans-serif;font-size:16px;line-height:24px;margin:0;min-height:34px;outline:0;padding:5px 4.75px;resize:none;width:100%}.tox .tox-selectfield select[disabled]{background-color:#19232e;color:rgba(255,255,255,.85);cursor:not-allowed}.tox .tox-selectfield select::-ms-expand{display:none}.tox .tox-selectfield select:focus{background-color:#2b3b4e;border-color:#207ab7;box-shadow:none;outline:2px solid rgba(32,122,183,.25)}.tox .tox-selectfield svg{pointer-events:none;position:absolute;top:50%;transform:translateY(-50%)}.tox:not([dir=rtl]) .tox-selectfield select[size=\\\"0\\\"],.tox:not([dir=rtl]) .tox-selectfield select[size=\\\"1\\\"]{padding-right:24px}.tox:not([dir=rtl]) .tox-selectfield svg{right:8px}.tox[dir=rtl] .tox-selectfield select[size=\\\"0\\\"],.tox[dir=rtl] .tox-selectfield select[size=\\\"1\\\"]{padding-left:24px}.tox[dir=rtl] .tox-selectfield svg{left:8px}.tox .tox-textarea-wrap{border-color:#000;border-radius:3px;border-style:solid;border-width:1px;display:flex;flex:1;overflow:hidden}.tox .tox-textarea{-webkit-appearance:textarea;-moz-appearance:textarea;appearance:textarea;white-space:pre-wrap}.tox .tox-textarea-wrap .tox-textarea{border:none}.tox .tox-textarea-wrap .tox-textarea:focus{border:none}.tox-fullscreen{border:0;height:100%;margin:0;overflow:hidden;overscroll-behavior:none;padding:0;touch-action:pinch-zoom;width:100%}.tox.tox-tinymce.tox-fullscreen .tox-statusbar__resize-handle{display:none}.tox-shadowhost.tox-fullscreen,.tox.tox-tinymce.tox-fullscreen{left:0;position:fixed;top:0;z-index:1200}.tox.tox-tinymce.tox-fullscreen{background-color:transparent}.tox-fullscreen .tox.tox-tinymce-aux,.tox-fullscreen~.tox.tox-tinymce-aux{z-index:1201}.tox .tox-help__more-link{list-style:none;margin-top:1em}.tox .tox-imagepreview{background-color:#666;height:380px;overflow:hidden;position:relative;width:100%}.tox .tox-imagepreview.tox-imagepreview__loaded{overflow:auto}.tox .tox-imagepreview__container{display:flex;left:100vw;position:absolute;top:100vw}.tox .tox-imagepreview__image{background:url(data:image/gif;base64,R0lGODdhDAAMAIABAMzMzP///ywAAAAADAAMAAACFoQfqYeabNyDMkBQb81Uat85nxguUAEAOw==)}.tox .tox-image-tools .tox-spacer{flex:1}.tox .tox-image-tools .tox-bar{align-items:center;display:flex;height:60px;justify-content:center}.tox .tox-image-tools .tox-imagepreview,.tox .tox-image-tools .tox-imagepreview+.tox-bar{margin-top:8px}.tox .tox-image-tools .tox-croprect-block{background:#000;opacity:.5;position:absolute;zoom:1}.tox .tox-image-tools .tox-croprect-handle{border:2px solid #fff;height:20px;left:0;position:absolute;top:0;width:20px}.tox .tox-image-tools .tox-croprect-handle-move{border:0;cursor:move;position:absolute}.tox .tox-image-tools .tox-croprect-handle-nw{border-width:2px 0 0 2px;cursor:nw-resize;left:100px;margin:-2px 0 0 -2px;top:100px}.tox .tox-image-tools .tox-croprect-handle-ne{border-width:2px 2px 0 0;cursor:ne-resize;left:200px;margin:-2px 0 0 -20px;top:100px}.tox .tox-image-tools .tox-croprect-handle-sw{border-width:0 0 2px 2px;cursor:sw-resize;left:100px;margin:-20px 2px 0 -2px;top:200px}.tox .tox-image-tools .tox-croprect-handle-se{border-width:0 2px 2px 0;cursor:se-resize;left:200px;margin:-20px 0 0 -20px;top:200px}.tox .tox-insert-table-picker{background-color:#222f3e;display:flex;flex-wrap:wrap;width:170px}.tox .tox-insert-table-picker>div{border-color:#000;border-style:solid;border-width:0 1px 1px 0;box-sizing:border-box;height:17px;width:17px}.tox .tox-collection--list .tox-collection__group .tox-insert-table-picker{margin:0 -4px}.tox .tox-insert-table-picker .tox-insert-table-picker__selected{background-color:rgba(32,122,183,.5);border-color:rgba(32,122,183,.5)}@media (forced-colors:active){.tox .tox-insert-table-picker .tox-insert-table-picker__selected{border-color:Highlight;filter:contrast(50%)}}.tox .tox-insert-table-picker__label{color:#fff;display:block;font-size:14px;padding:4px;text-align:center;width:100%}.tox:not([dir=rtl]) .tox-insert-table-picker>div:nth-child(10n){border-right:0}.tox[dir=rtl] .tox-insert-table-picker>div:nth-child(10n+1){border-right:0}.tox .tox-menu{background-color:#2b3b4e;border:1px solid #000;border-radius:3px;box-shadow:0 4px 8px 0 rgba(42,55,70,.1);display:inline-block;overflow:hidden;vertical-align:top;z-index:1150}.tox .tox-menu.tox-collection.tox-collection--list{padding:0 0}.tox .tox-menu.tox-collection.tox-collection--toolbar{padding:4px}.tox .tox-menu.tox-collection.tox-collection--grid{padding:4px}@media only screen and (min-width:768px){.tox .tox-menu .tox-collection__item-label{overflow-wrap:break-word;word-break:normal}.tox .tox-dialog__popups .tox-menu .tox-collection__item-label{word-break:break-all}}.tox .tox-menu__label blockquote,.tox .tox-menu__label code,.tox .tox-menu__label h1,.tox .tox-menu__label h2,.tox .tox-menu__label h3,.tox .tox-menu__label h4,.tox .tox-menu__label h5,.tox .tox-menu__label h6,.tox .tox-menu__label p{margin:0}.tox .tox-menubar{background:url(\\\"data:image/svg+xml;charset=utf8,%3Csvg height='39px' viewBox='0 0 40 39px' width='40' xmlns='http://www.w3.org/2000/svg'%3E%3Crect x='0' y='38px' width='100' height='1' fill='%23000000'/%3E%3C/svg%3E\\\") left 0 top 0 #222f3e;background-color:#222f3e;display:flex;flex:0 0 auto;flex-shrink:0;flex-wrap:wrap;grid-column:1/-1;grid-row:1;padding:0 4px 0 4px}.tox .tox-promotion+.tox-menubar{grid-column:1}.tox .tox-promotion{background:url(\\\"data:image/svg+xml;charset=utf8,%3Csvg height='39px' viewBox='0 0 40 39px' width='40' xmlns='http://www.w3.org/2000/svg'%3E%3Crect x='0' y='38px' width='100' height='1' fill='%23000000'/%3E%3C/svg%3E\\\") left 0 top 0 #222f3e;background-color:#222f3e;grid-column:2;grid-row:1;padding-inline-end:8px;padding-inline-start:4px;padding-top:5px}.tox .tox-promotion-link{align-items:unsafe center;background-color:#e8f1f8;border-radius:5px;color:#086be6;cursor:pointer;display:flex;font-size:14px;height:26.6px;padding:4px 8px;white-space:nowrap}.tox .tox-promotion-link:hover{background-color:#b4d7ff}.tox .tox-promotion-link:focus{background-color:#d9edf7}.tox .tox-mbtn{align-items:center;background:#222f3e;border:0;border-radius:3px;box-shadow:none;color:#fff;display:flex;flex:0 0 auto;font-size:14px;font-style:normal;font-weight:400;height:34px;justify-content:center;margin:2px 0 3px 0;outline:0;padding:0 4px;text-transform:none;width:auto}.tox .tox-mbtn[disabled]{background-color:#222f3e;border:0;box-shadow:none;color:rgba(255,255,255,.5);cursor:not-allowed}.tox .tox-mbtn:focus:not(:disabled){background:#4a5562;border:0;box-shadow:none;color:#fff;position:relative;z-index:1}.tox .tox-mbtn:focus:not(:disabled)::after{border-radius:3px;bottom:0;box-shadow:0 0 0 0 transparent;content:'';left:0;position:absolute;right:0;top:0}@media (forced-colors:active){.tox .tox-mbtn:focus:not(:disabled)::after{border:2px solid highlight}}.tox .tox-mbtn--active,.tox .tox-mbtn:not(:disabled).tox-mbtn--active:focus{background:#757d87;border:0;box-shadow:none;color:#fff}.tox .tox-mbtn:hover:not(:disabled):not(.tox-mbtn--active){background:#4a5562;border:0;box-shadow:none;color:#fff}.tox .tox-mbtn__select-label{cursor:default;font-weight:400;margin:0 4px}.tox .tox-mbtn[disabled] .tox-mbtn__select-label{cursor:not-allowed}.tox .tox-mbtn__select-chevron{align-items:center;display:flex;justify-content:center;width:16px;display:none}.tox .tox-notification{border-radius:3px;border-style:solid;border-width:1px;box-shadow:none;box-sizing:border-box;display:grid;font-size:14px;font-weight:400;grid-template-columns:minmax(40px,1fr) auto minmax(40px,1fr);margin-left:auto;margin-right:auto;margin-top:4px;opacity:0;padding:4px;transition:transform .1s ease-in,opacity 150ms ease-in;width:-moz-max-content;width:max-content}.tox .tox-notification a{cursor:pointer;text-decoration:underline}.tox .tox-notification p{font-size:14px;font-weight:400}.tox .tox-notification:focus{border-color:#207ab7;box-shadow:none}.tox .tox-notification--in{opacity:1}.tox .tox-notification--success{background-color:#334840;border-color:#3c5440;color:#fff}.tox .tox-notification--success p{color:#fff}.tox .tox-notification--success a{color:#b5d199}.tox .tox-notification--success a:focus,.tox .tox-notification--success a:hover{color:#82b153;text-decoration:underline}.tox .tox-notification--success a:focus-visible{border-radius:1px;outline:2px solid #b5d199;outline-offset:2px}.tox .tox-notification--success a:active{color:#689041;text-decoration:underline}.tox .tox-notification--success svg{fill:#fff}.tox .tox-notification--error{background-color:#442632;border-color:#55212b;color:#fff}.tox .tox-notification--error p{color:#fff}.tox .tox-notification--error a{color:#e68080}.tox .tox-notification--error a:focus,.tox .tox-notification--error a:hover{color:#d42b2b;text-decoration:underline}.tox .tox-notification--error a:focus-visible{border-radius:1px;outline:2px solid #e68080;outline-offset:2px}.tox .tox-notification--error a:active{color:#a22;text-decoration:underline}.tox .tox-notification--error svg{fill:#fff}.tox .tox-notification--warn,.tox .tox-notification--warning{background-color:#222f3e;border-color:#000;color:#fff0b3}.tox .tox-notification--warn p,.tox .tox-notification--warning p{color:#fff0b3}.tox .tox-notification--warn a,.tox .tox-notification--warning a{color:#fc0}.tox .tox-notification--warn a:focus,.tox .tox-notification--warn a:hover,.tox .tox-notification--warning a:focus,.tox .tox-notification--warning a:hover{color:#997a00;text-decoration:underline}.tox .tox-notification--warn a:focus-visible,.tox .tox-notification--warning a:focus-visible{border-radius:1px;outline:2px solid #fc0;outline-offset:2px}.tox .tox-notification--warn a:active,.tox .tox-notification--warning a:active{color:#665200;text-decoration:underline}.tox .tox-notification--warn svg,.tox .tox-notification--warning svg{fill:#fff0b3}.tox .tox-notification--info{background-color:#254161;border-color:#264972;color:#fff}.tox .tox-notification--info p{color:#fff}.tox .tox-notification--info a{color:#83b7f3}.tox .tox-notification--info a:focus,.tox .tox-notification--info a:hover{color:#2681ea;text-decoration:underline}.tox .tox-notification--info a:focus-visible{border-radius:1px;outline:2px solid #83b7f3;outline-offset:2px}.tox .tox-notification--info a:active{color:#1368c9;text-decoration:underline}.tox .tox-notification--info svg{fill:#fff}.tox .tox-notification__body{align-self:center;color:#fff;font-size:14px;grid-column-end:3;grid-column-start:2;grid-row-end:2;grid-row-start:1;text-align:center;white-space:normal;word-break:break-all;word-break:break-word}.tox .tox-notification__body>*{margin:0}.tox .tox-notification__body>*+*{margin-top:1rem}.tox .tox-notification__icon{align-self:center;grid-column-end:2;grid-column-start:1;grid-row-end:2;grid-row-start:1;justify-self:end}.tox .tox-notification__icon svg{display:block}.tox .tox-notification__dismiss{align-self:start;grid-column-end:4;grid-column-start:3;grid-row-end:2;grid-row-start:1;justify-self:end}.tox .tox-notification .tox-progress-bar{grid-column-end:4;grid-column-start:1;grid-row-end:3;grid-row-start:2;justify-self:center}.tox .tox-notification-container-dock-fadeout{opacity:0;visibility:hidden}.tox .tox-notification-container-dock-fadein{opacity:1;visibility:visible}.tox .tox-notification-container-dock-transition{transition:visibility 0s linear .3s,opacity .3s ease}.tox .tox-notification-container-dock-transition.tox-notification-container-dock-fadein{transition-delay:0s}.tox .tox-pop{display:inline-block;position:relative}.tox .tox-pop--resizing{transition:width .1s ease}.tox .tox-pop--resizing .tox-toolbar,.tox .tox-pop--resizing .tox-toolbar__group{flex-wrap:nowrap}.tox .tox-pop--transition{transition:.15s ease;transition-property:left,right,top,bottom}.tox .tox-pop--transition::after,.tox .tox-pop--transition::before{transition:all .15s,visibility 0s,opacity 75ms ease 75ms}.tox .tox-pop__dialog{background-color:#222f3e;border:1px solid #000;border-radius:3px;box-shadow:0 0 2px 0 rgba(42,55,70,.2),0 4px 8px 0 rgba(42,55,70,.15);min-width:0;overflow:hidden}.tox .tox-pop__dialog>:not(.tox-toolbar){margin:4px 4px 4px 8px}.tox .tox-pop__dialog .tox-toolbar{background-color:transparent;margin-bottom:-1px}.tox .tox-pop::after,.tox .tox-pop::before{border-style:solid;content:'';display:block;height:0;opacity:1;position:absolute;width:0}@media (forced-colors:active){.tox .tox-pop::after,.tox .tox-pop::before{content:none}}.tox .tox-pop.tox-pop--inset::after,.tox .tox-pop.tox-pop--inset::before{opacity:0;transition:all 0s .15s,visibility 0s,opacity 75ms ease}.tox .tox-pop.tox-pop--bottom::after,.tox .tox-pop.tox-pop--bottom::before{left:50%;top:100%}.tox .tox-pop.tox-pop--bottom::after{border-color:#222f3e transparent transparent transparent;border-width:8px;margin-left:-8px;margin-top:-1px}.tox .tox-pop.tox-pop--bottom::before{border-color:#000 transparent transparent transparent;border-width:9px;margin-left:-9px}.tox .tox-pop.tox-pop--top::after,.tox .tox-pop.tox-pop--top::before{left:50%;top:0;transform:translateY(-100%)}.tox .tox-pop.tox-pop--top::after{border-color:transparent transparent #222f3e transparent;border-width:8px;margin-left:-8px;margin-top:1px}.tox .tox-pop.tox-pop--top::before{border-color:transparent transparent #000 transparent;border-width:9px;margin-left:-9px}.tox .tox-pop.tox-pop--left::after,.tox .tox-pop.tox-pop--left::before{left:0;top:calc(50% - 1px);transform:translateY(-50%)}.tox .tox-pop.tox-pop--left::after{border-color:transparent #222f3e transparent transparent;border-width:8px;margin-left:-15px}.tox .tox-pop.tox-pop--left::before{border-color:transparent #000 transparent transparent;border-width:10px;margin-left:-19px}.tox .tox-pop.tox-pop--right::after,.tox .tox-pop.tox-pop--right::before{left:100%;top:calc(50% + 1px);transform:translateY(-50%)}.tox .tox-pop.tox-pop--right::after{border-color:transparent transparent transparent #222f3e;border-width:8px;margin-left:-1px}.tox .tox-pop.tox-pop--right::before{border-color:transparent transparent transparent #000;border-width:10px;margin-left:-1px}.tox .tox-pop.tox-pop--align-left::after,.tox .tox-pop.tox-pop--align-left::before{left:20px}.tox .tox-pop.tox-pop--align-right::after,.tox .tox-pop.tox-pop--align-right::before{left:calc(100% - 20px)}.tox .tox-sidebar-wrap{display:flex;flex-direction:row;flex-grow:1;min-height:0}.tox .tox-sidebar{background-color:#222f3e;display:flex;flex-direction:row;justify-content:flex-end}.tox .tox-sidebar__slider{display:flex;overflow:hidden}.tox .tox-sidebar__pane-container{display:flex}.tox .tox-sidebar__pane{display:flex}.tox .tox-sidebar--sliding-closed{opacity:0}.tox .tox-sidebar--sliding-open{opacity:1}.tox .tox-sidebar--sliding-growing,.tox .tox-sidebar--sliding-shrinking{transition:width .5s ease,opacity .5s ease}.tox .tox-selector{background-color:#4099ff;border-color:#4099ff;border-style:solid;border-width:1px;box-sizing:border-box;display:inline-block;height:10px;position:absolute;width:10px}.tox.tox-platform-touch .tox-selector{height:12px;width:12px}.tox .tox-slider{align-items:center;display:flex;flex:1;height:24px;justify-content:center;position:relative}.tox .tox-slider__rail{background-color:transparent;border:1px solid #000;border-radius:3px;height:10px;min-width:120px;width:100%}.tox .tox-slider__handle{background-color:#207ab7;border:2px solid #185d8c;border-radius:3px;box-shadow:none;height:24px;left:50%;position:absolute;top:50%;transform:translateX(-50%) translateY(-50%);width:14px}.tox .tox-form__controls-h-stack>.tox-slider:not(:first-of-type){margin-inline-start:8px}.tox .tox-form__controls-h-stack>.tox-form__group+.tox-slider{margin-inline-start:32px}.tox .tox-form__controls-h-stack>.tox-slider+.tox-form__group{margin-inline-start:32px}.tox .tox-source-code{overflow:auto}.tox .tox-spinner{display:flex}.tox .tox-spinner>div{animation:tam-bouncing-dots 1.5s ease-in-out 0s infinite both;background-color:rgba(255,255,255,.5);border-radius:100%;height:8px;width:8px}.tox .tox-spinner>div:nth-child(1){animation-delay:-.32s}.tox .tox-spinner>div:nth-child(2){animation-delay:-.16s}@keyframes tam-bouncing-dots{0%,100%,80%{transform:scale(0)}40%{transform:scale(1)}}.tox:not([dir=rtl]) .tox-spinner>div:not(:first-child){margin-left:4px}.tox[dir=rtl] .tox-spinner>div:not(:first-child){margin-right:4px}.tox .tox-statusbar{align-items:center;background-color:#222f3e;border-top:1px solid #000;color:#fff;display:flex;flex:0 0 auto;font-size:12px;font-weight:400;height:18px;overflow:hidden;padding:0 8px;position:relative;text-transform:uppercase}.tox .tox-statusbar__path{display:flex;flex:1 1 auto;text-overflow:ellipsis;white-space:nowrap}.tox .tox-statusbar__right-container{display:flex;justify-content:flex-end;white-space:nowrap}.tox .tox-statusbar__help-text{text-align:center}.tox .tox-statusbar__text-container{align-items:flex-start;display:flex;flex:1 1 auto;height:16px;justify-content:space-between;overflow:hidden}@media only screen and (min-width:768px){.tox .tox-statusbar__text-container.tox-statusbar__text-container-3-cols>.tox-statusbar__help-text,.tox .tox-statusbar__text-container.tox-statusbar__text-container-3-cols>.tox-statusbar__path,.tox .tox-statusbar__text-container.tox-statusbar__text-container-3-cols>.tox-statusbar__right-container{flex:0 0 calc(100% / 3)}}.tox .tox-statusbar__text-container.tox-statusbar__text-container--flex-end{justify-content:flex-end}.tox .tox-statusbar__text-container.tox-statusbar__text-container--flex-start{justify-content:flex-start}.tox .tox-statusbar__text-container.tox-statusbar__text-container--space-around{justify-content:space-around}.tox .tox-statusbar__path>*{display:inline;white-space:nowrap}.tox .tox-statusbar__wordcount{flex:0 0 auto;margin-left:1ch}@media only screen and (max-width:767px){.tox .tox-statusbar__text-container .tox-statusbar__help-text{display:none}.tox .tox-statusbar__text-container .tox-statusbar__help-text:only-child{display:block}}.tox .tox-statusbar a,.tox .tox-statusbar__path-item,.tox .tox-statusbar__wordcount{color:#fff;position:relative;text-decoration:none}.tox .tox-statusbar a:focus:not(:disabled):not([aria-disabled=true]),.tox .tox-statusbar a:hover:not(:disabled):not([aria-disabled=true]),.tox .tox-statusbar__path-item:focus:not(:disabled):not([aria-disabled=true]),.tox .tox-statusbar__path-item:hover:not(:disabled):not([aria-disabled=true]),.tox .tox-statusbar__wordcount:focus:not(:disabled):not([aria-disabled=true]),.tox .tox-statusbar__wordcount:hover:not(:disabled):not([aria-disabled=true]){color:#fff;cursor:pointer}.tox .tox-statusbar a:focus-visible::after,.tox .tox-statusbar__path-item:focus-visible::after,.tox .tox-statusbar__wordcount:focus-visible::after{border-radius:3px;bottom:0;box-shadow:0 0 0 0 transparent;content:'';left:0;position:absolute;right:0;top:0}@media (forced-colors:active){.tox .tox-statusbar a:focus-visible::after,.tox .tox-statusbar__path-item:focus-visible::after,.tox .tox-statusbar__wordcount:focus-visible::after{border:2px solid highlight}}.tox .tox-statusbar__branding svg{fill:rgba(255,255,255,.8);height:1em;margin-left:.3em;width:auto}@media (forced-colors:active){.tox .tox-statusbar__branding svg{fill:currentColor}}.tox .tox-statusbar__branding a{align-items:center;display:inline-flex}.tox .tox-statusbar__branding a:focus:not(:disabled):not([aria-disabled=true]) svg,.tox .tox-statusbar__branding a:hover:not(:disabled):not([aria-disabled=true]) svg{fill:#fff}.tox .tox-statusbar__resize-handle{align-items:flex-end;align-self:stretch;cursor:nwse-resize;display:flex;flex:0 0 auto;justify-content:flex-end;margin-bottom:3px;margin-left:4px;margin-right:calc(3px - 8px);margin-top:3px;padding-bottom:0;padding-left:0;padding-right:0;position:relative}.tox .tox-statusbar__resize-handle svg{display:block;fill:rgba(255,255,255,.5)}.tox .tox-statusbar__resize-handle:focus svg,.tox .tox-statusbar__resize-handle:hover svg{fill:#fff}.tox .tox-statusbar__resize-handle:focus-visible{background-color:transparent;border-radius:1px 1px -4px 1px;box-shadow:0 0 0 2px transparent}.tox .tox-statusbar__resize-handle:focus-visible::after{border-radius:3px;bottom:0;box-shadow:0 0 0 0 transparent;content:'';left:0;position:absolute;right:0;top:0}@media (forced-colors:active){.tox .tox-statusbar__resize-handle:focus-visible::after{border:2px solid highlight}}.tox:not([dir=rtl]) .tox-statusbar__path>*{margin-right:4px}.tox:not([dir=rtl]) .tox-statusbar__branding{margin-left:2ch}.tox[dir=rtl] .tox-statusbar{flex-direction:row-reverse}.tox[dir=rtl] .tox-statusbar__path>*{margin-left:4px}.tox[dir=rtl] .tox-statusbar__branding svg{margin-left:0;margin-right:.3em}.tox .tox-throbber{z-index:1299}.tox .tox-throbber__busy-spinner{align-items:center;background-color:rgba(34,47,62,.6);bottom:0;display:flex;justify-content:center;left:0;position:absolute;right:0;top:0}.tox .tox-tbtn{align-items:center;background:#222f3e;border:0;border-radius:3px;box-shadow:none;color:#fff;display:flex;flex:0 0 auto;font-size:14px;font-style:normal;font-weight:400;height:34px;justify-content:center;margin:3px 0 2px 0;outline:0;padding:0;text-transform:none;width:34px}@media (forced-colors:active){.tox .tox-tbtn.tox-tbtn:hover,.tox .tox-tbtn:hover{outline:1px dashed currentColor}.tox .tox-tbtn.tox-tbtn--active,.tox .tox-tbtn.tox-tbtn--enabled,.tox .tox-tbtn.tox-tbtn--enabled:focus,.tox .tox-tbtn.tox-tbtn--enabled:hover,.tox .tox-tbtn:focus:not(.tox-tbtn--disabled){outline:1px solid currentColor;position:relative}}.tox .tox-tbtn svg{display:block;fill:#fff}@media (forced-colors:active){.tox .tox-tbtn svg{fill:currentColor!important}.tox .tox-tbtn svg.tox-tbtn--enabled,.tox .tox-tbtn svg:focus:not(.tox-tbtn--disabled){fill:currentColor!important}.tox .tox-tbtn svg .tox-tbtn:disabled,.tox .tox-tbtn svg .tox-tbtn:disabled:hover,.tox .tox-tbtn svg.tox-tbtn--disabled,.tox .tox-tbtn svg.tox-tbtn--disabled:hover{filter:contrast(0)}}.tox .tox-tbtn.tox-tbtn-more{padding-left:5px;padding-right:5px;width:inherit}.tox .tox-tbtn:focus{background:#4a5562;border:0;box-shadow:none;position:relative;z-index:1}.tox .tox-tbtn:focus::after{border-radius:3px;bottom:0;box-shadow:0 0 0 0 transparent;content:'';left:0;position:absolute;right:0;top:0}@media (forced-colors:active){.tox .tox-tbtn:focus::after{border:2px solid highlight}}.tox .tox-tbtn:hover{background:#4a5562;border:0;box-shadow:none;color:#fff}.tox .tox-tbtn:hover svg{fill:#fff}.tox .tox-tbtn:active{background:#757d87;border:0;box-shadow:none;color:#fff}.tox .tox-tbtn:active svg{fill:#fff}.tox .tox-tbtn--disabled .tox-tbtn--enabled svg{fill:rgba(255,255,255,.5)}.tox .tox-tbtn--disabled,.tox .tox-tbtn--disabled:hover,.tox .tox-tbtn:disabled,.tox .tox-tbtn:disabled:hover{background:#222f3e;border:0;box-shadow:none;color:rgba(255,255,255,.5);cursor:not-allowed}.tox .tox-tbtn--disabled svg,.tox .tox-tbtn--disabled:hover svg,.tox .tox-tbtn:disabled svg,.tox .tox-tbtn:disabled:hover svg{fill:rgba(255,255,255,.5)}.tox .tox-tbtn--active,.tox .tox-tbtn--enabled,.tox .tox-tbtn--enabled:focus,.tox .tox-tbtn--enabled:hover{background:#757d87;border:0;box-shadow:none;color:#fff;position:relative}.tox .tox-tbtn--active>*,.tox .tox-tbtn--enabled:focus>*,.tox .tox-tbtn--enabled:hover>*,.tox .tox-tbtn--enabled>*{transform:none}.tox .tox-tbtn--active svg,.tox .tox-tbtn--enabled svg,.tox .tox-tbtn--enabled:focus svg,.tox .tox-tbtn--enabled:hover svg{fill:#fff}.tox .tox-tbtn--active.tox-tbtn--disabled svg,.tox .tox-tbtn--enabled.tox-tbtn--disabled svg,.tox .tox-tbtn--enabled:focus.tox-tbtn--disabled svg,.tox .tox-tbtn--enabled:hover.tox-tbtn--disabled svg{fill:rgba(255,255,255,.5)}.tox .tox-tbtn--enabled:focus::after{border-radius:3px;bottom:0;box-shadow:0 0 0 0 transparent;content:'';left:0;position:absolute;right:0;top:0}@media (forced-colors:active){.tox .tox-tbtn--enabled:focus::after{border:2px solid highlight}}.tox .tox-tbtn:focus:not(.tox-tbtn--disabled){color:#fff}.tox .tox-tbtn:focus:not(.tox-tbtn--disabled) svg{fill:#fff}.tox .tox-tbtn:active>*{transform:none}.tox .tox-tbtn--md{height:51px;width:51px}.tox .tox-tbtn--lg{flex-direction:column;height:68px;width:68px}.tox .tox-tbtn--return{align-self:stretch;height:unset;width:16px}.tox .tox-tbtn--labeled{padding:0 4px;width:unset}.tox .tox-tbtn__vlabel{display:block;font-size:10px;font-weight:400;letter-spacing:-.025em;margin-bottom:4px;white-space:nowrap}.tox .tox-number-input{background:0 0;border-radius:3px;display:flex;margin:3px 0 2px 0;position:relative;width:auto}.tox .tox-number-input:focus{background:#4a5562}.tox .tox-number-input:focus::after{border-radius:3px;bottom:0;box-shadow:0 0 0 0 transparent;content:'';left:0;position:absolute;right:0;top:0}@media (forced-colors:active){.tox .tox-number-input:focus::after{border:2px solid highlight}}.tox .tox-number-input .tox-input-wrapper{display:flex;pointer-events:none;position:relative;text-align:center}.tox .tox-number-input .tox-input-wrapper:focus{background-color:#4a5562;z-index:1}.tox .tox-number-input .tox-input-wrapper:focus::after{border-radius:3px;bottom:0;box-shadow:0 0 0 0 transparent;content:'';left:0;position:absolute;right:0;top:0}@media (forced-colors:active){.tox .tox-number-input .tox-input-wrapper:focus::after{border:2px solid highlight}}.tox .tox-number-input .tox-input-wrapper:has(input:focus)::after{border-radius:3px;bottom:0;box-shadow:0 0 0 0 transparent;content:'';left:0;position:absolute;right:0;top:0}@media (forced-colors:active){.tox .tox-number-input .tox-input-wrapper:has(input:focus)::after{border:2px solid highlight}}.tox .tox-number-input input{border-radius:3px;color:#fff;font-size:14px;margin:2px 0;pointer-events:all;position:relative;width:60px}.tox .tox-number-input input:hover{background:#4a5562;color:#fff}.tox .tox-number-input input:focus{background-color:#4a5562}.tox .tox-number-input input:disabled{background:#222f3e;border:0;box-shadow:none;color:rgba(255,255,255,.5);cursor:not-allowed}.tox .tox-number-input button{color:#fff;height:34px;position:relative;text-align:center;width:24px}@media (forced-colors:active){.tox .tox-number-input button:active,.tox .tox-number-input button:focus,.tox .tox-number-input button:hover{outline:1px solid currentColor!important}}.tox .tox-number-input button svg{display:block;fill:#fff;margin:0 auto;transform:scale(.67)}@media (forced-colors:active){.tox .tox-number-input button svg,.tox .tox-number-input button svg:active,.tox .tox-number-input button svg:hover{fill:currentColor!important}.tox .tox-number-input button svg:disabled{filter:contrast(0)}}.tox .tox-number-input button:focus{background:#4a5562;z-index:1}.tox .tox-number-input button:focus::after{border-radius:3px;bottom:0;box-shadow:0 0 0 0 transparent;content:'';left:0;position:absolute;right:0;top:0}@media (forced-colors:active){.tox .tox-number-input button:focus::after{border:2px solid highlight}}.tox .tox-number-input button:hover{background:#4a5562;border:0;box-shadow:none;color:#fff}.tox .tox-number-input button:hover svg{fill:#fff}.tox .tox-number-input button:active{background:#757d87;border:0;box-shadow:none;color:#fff}.tox .tox-number-input button:active svg{fill:#fff}.tox .tox-number-input button:disabled{background:#222f3e;border:0;box-shadow:none;color:rgba(255,255,255,.5);cursor:not-allowed}.tox .tox-number-input button:disabled svg{fill:rgba(255,255,255,.5)}.tox .tox-number-input button.minus{border-radius:3px 0 0 3px}.tox .tox-number-input button.plus{border-radius:0 3px 3px 0}.tox .tox-number-input:focus:not(:active)>.tox-input-wrapper,.tox .tox-number-input:focus:not(:active)>button{background:#4a5562}.tox .tox-tbtn--select{margin:3px 0 2px 0;padding:0 4px;width:auto}.tox .tox-tbtn__select-label{cursor:default;font-weight:400;height:initial;margin:0 4px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.tox .tox-tbtn__select-chevron{align-items:center;display:flex;justify-content:center;width:16px}.tox .tox-tbtn__select-chevron svg{fill:rgba(255,255,255,.5)}@media (forced-colors:active){.tox .tox-tbtn__select-chevron svg{fill:currentColor}}.tox .tox-tbtn--bespoke{background:0 0}.tox .tox-tbtn--bespoke:focus{background:#4a5562}.tox .tox-tbtn--bespoke+.tox-tbtn--bespoke{margin-inline-start:0}.tox .tox-tbtn--bespoke .tox-tbtn__select-label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;width:7em}.tox .tox-tbtn--disabled .tox-tbtn__select-label,.tox .tox-tbtn--select:disabled .tox-tbtn__select-label{cursor:not-allowed}.tox .tox-split-button{border:0;border-radius:3px;box-sizing:border-box;display:flex;margin:3px 0 2px 0}.tox .tox-split-button:hover{box-shadow:0 0 0 1px #4a5562 inset}.tox .tox-split-button:focus{background:#4a5562;box-shadow:none;color:#fff;position:relative;z-index:1}.tox .tox-split-button:focus::after{pointer-events:none;border-radius:3px;bottom:0;box-shadow:0 0 0 0 transparent;content:'';left:0;position:absolute;right:0;top:0}@media (forced-colors:active){.tox .tox-split-button:focus::after{border:2px solid highlight}}.tox .tox-split-button>*{border-radius:0}.tox .tox-split-button>:nth-child(1){border-bottom-left-radius:3px;border-top-left-radius:3px}.tox .tox-split-button>:nth-child(2){border-bottom-right-radius:3px;border-top-right-radius:3px}.tox .tox-split-button__chevron{width:16px}.tox .tox-split-button__chevron svg{fill:rgba(255,255,255,.5)}@media (forced-colors:active){.tox .tox-split-button__chevron svg{fill:currentColor}}.tox .tox-split-button .tox-tbtn{margin:0}.tox .tox-split-button:focus .tox-tbtn{background-color:transparent}.tox .tox-split-button.tox-tbtn--disabled .tox-tbtn:focus,.tox .tox-split-button.tox-tbtn--disabled .tox-tbtn:hover,.tox .tox-split-button.tox-tbtn--disabled:focus,.tox .tox-split-button.tox-tbtn--disabled:hover{background:#222f3e;box-shadow:none;color:rgba(255,255,255,.5)}.tox.tox-platform-touch .tox-split-button .tox-tbtn--select{padding:0 0}.tox.tox-platform-touch .tox-split-button .tox-tbtn:not(.tox-tbtn--select):first-child{width:30px}.tox.tox-platform-touch .tox-split-button__chevron{width:20px}.tox .tox-split-button.tox-tbtn--disabled svg #tox-icon-highlight-bg-color__color,.tox .tox-split-button.tox-tbtn--disabled svg #tox-icon-text-color__color{opacity:.6}.tox .tox-toolbar-overlord{background-color:#222f3e}.tox .tox-toolbar,.tox .tox-toolbar__overflow,.tox .tox-toolbar__primary{background-attachment:local;background-color:#222f3e;background-image:repeating-linear-gradient(#000 0 1px,transparent 1px 39px);background-position:center top 39px;background-repeat:no-repeat;background-size:calc(100% - 4px * 2) calc(100% - 39px);display:flex;flex:0 0 auto;flex-shrink:0;flex-wrap:wrap;padding:0 0;transform:perspective(1px)}.tox .tox-toolbar-overlord>.tox-toolbar,.tox .tox-toolbar-overlord>.tox-toolbar__overflow,.tox .tox-toolbar-overlord>.tox-toolbar__primary{background-position:center top 0;background-size:calc(100% - 4px * 2) calc(100% - 0px)}.tox .tox-toolbar__overflow.tox-toolbar__overflow--closed{height:0;opacity:0;padding-bottom:0;padding-top:0;visibility:hidden}.tox .tox-toolbar__overflow--growing{transition:height .3s ease,opacity .2s linear .1s}.tox .tox-toolbar__overflow--shrinking{transition:opacity .3s ease,height .2s linear .1s,visibility 0s linear .3s}.tox .tox-anchorbar,.tox .tox-toolbar-overlord{grid-column:1/-1}.tox .tox-menubar+.tox-toolbar,.tox .tox-menubar+.tox-toolbar-overlord{border-top:1px solid #000;margin-top:-1px;padding-bottom:0;padding-top:0}@media (forced-colors:active){.tox .tox-menubar+.tox-toolbar,.tox .tox-menubar+.tox-toolbar-overlord{outline:1px solid currentColor}}.tox .tox-toolbar--scrolling{flex-wrap:nowrap;overflow-x:auto}.tox .tox-pop .tox-toolbar{border-width:0}.tox .tox-toolbar--no-divider{background-image:none}.tox .tox-toolbar-overlord .tox-toolbar:not(.tox-toolbar--scrolling):first-child,.tox .tox-toolbar-overlord .tox-toolbar__primary{background-position:center top 39px}.tox .tox-editor-header>.tox-toolbar--scrolling,.tox .tox-toolbar-overlord .tox-toolbar--scrolling:first-child{background-image:none}.tox.tox-tinymce-aux .tox-toolbar__overflow{background-color:#222f3e;background-position:center top 43px;background-size:calc(100% - 8px * 2) calc(100% - 51px);border:none;border-radius:3px;box-shadow:0 0 2px 0 rgba(42,55,70,.2),0 4px 8px 0 rgba(42,55,70,.15);overscroll-behavior:none;padding:4px 0}@media (forced-colors:active){.tox.tox-tinymce-aux .tox-toolbar__overflow{border:solid}}.tox-pop .tox-pop__dialog .tox-toolbar{background-position:center top 43px;background-size:calc(100% - 4px * 2) calc(100% - 51px);padding:4px 0}.tox .tox-toolbar__group{align-items:center;display:flex;flex-wrap:wrap;margin:0 0;padding:0 4px 0 4px}.tox .tox-toolbar__group--pull-right{margin-left:auto}.tox .tox-toolbar--scrolling .tox-toolbar__group{flex-shrink:0;flex-wrap:nowrap}.tox:not([dir=rtl]) .tox-toolbar__group:not(:last-of-type){border-right:1px solid #000}.tox[dir=rtl] .tox-toolbar__group:not(:last-of-type){border-left:1px solid #000}.tox .tox-tooltip{display:inline-block;max-width:15em;padding:8px;pointer-events:none;position:relative;width:-moz-max-content;width:max-content;z-index:1150}.tox .tox-tooltip__body{background-color:#2a3746;border-radius:3px;box-shadow:none;color:#fff;font-size:12px;font-style:normal;font-weight:600;overflow-wrap:break-word;padding:4px 6px;text-transform:none}@media (forced-colors:active){.tox .tox-tooltip__body{outline:outset 1px}}.tox .tox-tooltip__arrow{position:absolute}.tox .tox-tooltip--down .tox-tooltip__arrow{border-left:8px solid transparent;border-right:8px solid transparent;border-top:8px solid #2a3746;bottom:0;left:50%;position:absolute;transform:translateX(-50%)}.tox .tox-tooltip--up .tox-tooltip__arrow{border-bottom:8px solid #2a3746;border-left:8px solid transparent;border-right:8px solid transparent;left:50%;position:absolute;top:0;transform:translateX(-50%)}.tox .tox-tooltip--right .tox-tooltip__arrow{border-bottom:8px solid transparent;border-left:8px solid #2a3746;border-top:8px solid transparent;position:absolute;right:0;top:50%;transform:translateY(-50%)}.tox .tox-tooltip--left .tox-tooltip__arrow{border-bottom:8px solid transparent;border-right:8px solid #2a3746;border-top:8px solid transparent;left:0;position:absolute;top:50%;transform:translateY(-50%)}.tox .tox-tree{display:flex;flex-direction:column}.tox .tox-tree .tox-trbtn{align-items:center;background:0 0;border:0;border-radius:4px;box-shadow:none;color:#fff;display:flex;flex:0 0 auto;font-size:14px;font-style:normal;font-weight:400;height:28px;margin-bottom:4px;margin-top:4px;outline:0;overflow:hidden;padding:0;padding-left:8px;text-transform:none}.tox .tox-tree .tox-trbtn .tox-tree__label{cursor:default;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.tox .tox-tree .tox-trbtn svg{display:block;fill:#fff}.tox .tox-tree .tox-trbtn:focus{background:#4a5562;border:0;box-shadow:none}.tox .tox-tree .tox-trbtn:hover{background:#4a5562;border:0;box-shadow:none;color:#fff}.tox .tox-tree .tox-trbtn:hover svg{fill:#fff}.tox .tox-tree .tox-trbtn:active{background:#6ea9d0;border:0;box-shadow:none;color:#fff}.tox .tox-tree .tox-trbtn:active svg{fill:#fff}.tox .tox-tree .tox-trbtn--disabled,.tox .tox-tree .tox-trbtn--disabled:hover,.tox .tox-tree .tox-trbtn:disabled,.tox .tox-tree .tox-trbtn:disabled:hover{background:0 0;border:0;box-shadow:none;color:rgba(255,255,255,.5);cursor:not-allowed}.tox .tox-tree .tox-trbtn--disabled svg,.tox .tox-tree .tox-trbtn--disabled:hover svg,.tox .tox-tree .tox-trbtn:disabled svg,.tox .tox-tree .tox-trbtn:disabled:hover svg{fill:rgba(255,255,255,.5)}.tox .tox-tree .tox-trbtn--enabled,.tox .tox-tree .tox-trbtn--enabled:hover{background:#6ea9d0;border:0;box-shadow:none;color:#fff}.tox .tox-tree .tox-trbtn--enabled:hover>*,.tox .tox-tree .tox-trbtn--enabled>*{transform:none}.tox .tox-tree .tox-trbtn--enabled svg,.tox .tox-tree .tox-trbtn--enabled:hover svg{fill:#fff}.tox .tox-tree .tox-trbtn:focus:not(.tox-trbtn--disabled){color:#fff}.tox .tox-tree .tox-trbtn:focus:not(.tox-trbtn--disabled) svg{fill:#fff}.tox .tox-tree .tox-trbtn:active>*{transform:none}.tox .tox-tree .tox-trbtn--return{align-self:stretch;height:unset;width:16px}.tox .tox-tree .tox-trbtn--labeled{padding:0 4px;width:unset}.tox .tox-tree .tox-trbtn__vlabel{display:block;font-size:10px;font-weight:400;letter-spacing:-.025em;margin-bottom:4px;white-space:nowrap}.tox .tox-tree .tox-tree--directory{display:flex;flex-direction:column}.tox .tox-tree .tox-tree--directory .tox-tree--directory__label{font-weight:700}.tox .tox-tree .tox-tree--directory .tox-tree--directory__label .tox-mbtn{margin-left:auto}.tox .tox-tree .tox-tree--directory .tox-tree--directory__label .tox-mbtn svg{fill:transparent}.tox .tox-tree .tox-tree--directory .tox-tree--directory__label .tox-mbtn.tox-mbtn--active svg,.tox .tox-tree .tox-tree--directory .tox-tree--directory__label .tox-mbtn:focus svg{fill:#fff}.tox .tox-tree .tox-tree--directory .tox-tree--directory__label:focus .tox-mbtn svg,.tox .tox-tree .tox-tree--directory .tox-tree--directory__label:hover .tox-mbtn svg{fill:#fff}.tox .tox-tree .tox-tree--directory .tox-tree--directory__label:hover:has(.tox-mbtn:hover){background-color:transparent;color:#fff}.tox .tox-tree .tox-tree--directory .tox-tree--directory__label:hover:has(.tox-mbtn:hover) .tox-chevron svg{fill:#fff}.tox .tox-tree .tox-tree--directory .tox-tree--directory__label .tox-chevron{margin-right:6px}.tox .tox-tree .tox-tree--directory .tox-tree--directory__label:has(+.tox-tree--directory__children--growing) .tox-chevron,.tox .tox-tree .tox-tree--directory .tox-tree--directory__label:has(+.tox-tree--directory__children--shrinking) .tox-chevron{transition:transform .5s ease-in-out}.tox .tox-tree .tox-tree--directory .tox-tree--directory__label:has(+.tox-tree--directory__children--growing) .tox-chevron,.tox .tox-tree .tox-tree--directory .tox-tree--directory__label:has(+.tox-tree--directory__children--open) .tox-chevron{transform:rotate(90deg)}.tox .tox-tree .tox-tree--leaf__label{font-weight:400}.tox .tox-tree .tox-tree--leaf__label .tox-mbtn{margin-left:auto}.tox .tox-tree .tox-tree--leaf__label .tox-mbtn svg{fill:transparent}.tox .tox-tree .tox-tree--leaf__label .tox-mbtn.tox-mbtn--active svg,.tox .tox-tree .tox-tree--leaf__label .tox-mbtn:focus svg{fill:#fff}.tox .tox-tree .tox-tree--leaf__label:hover .tox-mbtn svg{fill:#fff}.tox .tox-tree .tox-tree--leaf__label:hover:has(.tox-mbtn:hover){background-color:transparent;color:#fff}.tox .tox-tree .tox-tree--leaf__label:hover:has(.tox-mbtn:hover) .tox-chevron svg{fill:#fff}.tox .tox-tree .tox-tree--directory__children{overflow:hidden;padding-left:16px}.tox .tox-tree .tox-tree--directory__children.tox-tree--directory__children--growing,.tox .tox-tree .tox-tree--directory__children.tox-tree--directory__children--shrinking{transition:height .5s ease-in-out}.tox .tox-tree .tox-trbtn.tox-tree--leaf__label{display:flex;justify-content:space-between}.tox .tox-revisionhistory__pane{padding:0!important}.tox .tox-revisionhistory__container{display:flex;flex-direction:column;height:100%}.tox .tox-revisionhistory{background-color:#2b3b4e;border-radius:4px;border-top:1px solid #000;display:flex;flex:1;height:100%;margin-top:8px;overflow-x:auto;overflow-y:hidden;position:relative;width:100%}.tox .tox-revisionhistory--align-right{margin-left:auto}.tox .tox-revisionhistory__iframe{flex:1}.tox .tox-revisionhistory__sidebar{border-left:1px solid #000;height:100%;max-width:360px}.tox .tox-revisionhistory__sidebar .tox-revisionhistory__sidebar-title{border-bottom:1px solid #000;color:#fff;font-size:20px;font-weight:400;height:60px;min-width:192px;padding:16px}.tox .tox-revisionhistory__sidebar .tox-revisionhistory__revisions{flex-direction:column;max-height:calc(100% - 60px);min-width:192px;overflow-y:auto;padding:8px}.tox .tox-revisionhistory__sidebar .tox-revisionhistory__revisions:focus{height:100%;position:relative;z-index:1}.tox .tox-revisionhistory__sidebar .tox-revisionhistory__revisions:focus::after{bottom:0;box-shadow:0 0 0 0 transparent;content:'';left:0;position:absolute;right:0;top:0;border-radius:3px;bottom:1px;left:1px;right:1px;top:1px}@media (forced-colors:active){.tox .tox-revisionhistory__sidebar .tox-revisionhistory__revisions:focus::after{border:2px solid highlight}}.tox .tox-revisionhistory__sidebar .tox-revisionhistory__revisions .tox-revisionhistory__card{border:1px solid #000;border-radius:3px;color:#fff;cursor:pointer;font-size:14px;margin-bottom:8px;padding:8px;text-overflow:ellipsis;text-wrap:nowrap;width:100%}.tox .tox-revisionhistory__sidebar .tox-revisionhistory__revisions .tox-revisionhistory__card:hover{background-color:#4a5562;box-shadow:none;color:#fff}.tox .tox-revisionhistory__sidebar .tox-revisionhistory__revisions .tox-revisionhistory__card:focus{position:relative;z-index:1}.tox .tox-revisionhistory__sidebar .tox-revisionhistory__revisions .tox-revisionhistory__card:focus::after{border-radius:3px!important;border-radius:3px;bottom:0;box-shadow:0 0 0 0 transparent;content:'';left:0;position:absolute;right:0;top:0}@media (forced-colors:active){.tox .tox-revisionhistory__sidebar .tox-revisionhistory__revisions .tox-revisionhistory__card:focus::after{border:2px solid highlight}}.tox .tox-revisionhistory__sidebar .tox-revisionhistory__revisions .tox-revisionhistory__card.tox-revisionhistory__card--selected{background-color:#6ea9d0;box-shadow:none;color:#fff}.tox .tox-revisionhistory__sidebar .tox-revisionhistory__revisions .tox-revisionhistory__norevision{color:rgba(255,255,255,.5);font-size:16px;line-height:24px;padding:5px 5.5px}.tox .tox-view-wrap,.tox .tox-view-wrap__slot-container{background-color:#222f3e;display:flex;flex:1;flex-direction:column;height:100%}.tox .tox-view{display:flex;flex:1 1 auto;flex-direction:column;overflow:hidden}.tox .tox-view__header{align-items:center;display:flex;font-size:16px;justify-content:space-between;padding:8px 8px 0 8px;position:relative}.tox .tox-view__label{color:#fff;font-weight:700;line-height:24px;padding:4px 16px;text-align:center;white-space:nowrap}.tox .tox-view__label--normal{font-size:16px}.tox .tox-view__label--large{font-size:20px}.tox .tox-view--mobile.tox-view__header,.tox .tox-view--mobile.tox-view__toolbar{padding:8px}.tox .tox-view--scrolling{flex-wrap:nowrap;overflow-x:auto}.tox .tox-view__toolbar{display:flex;flex-direction:row;gap:8px;justify-content:space-between;overflow-x:auto;padding:8px 8px 0 8px}.tox .tox-view__toolbar__group{display:flex;flex-direction:row;gap:12px}.tox .tox-view__header-end,.tox .tox-view__header-start{display:flex}.tox .tox-view__pane{height:100%;padding:8px;position:relative;width:100%}.tox .tox-view__pane_panel{border:1px solid #000;border-radius:3px}.tox:not([dir=rtl]) .tox-view__header .tox-view__header-end>*,.tox:not([dir=rtl]) .tox-view__header .tox-view__header-start>*{margin-left:8px}.tox[dir=rtl] .tox-view__header .tox-view__header-end>*,.tox[dir=rtl] .tox-view__header .tox-view__header-start>*{margin-right:8px}.tox .tox-well{border:1px solid #000;border-radius:3px;padding:8px;width:100%}.tox .tox-well>:first-child{margin-top:0}.tox .tox-well>:last-child{margin-bottom:0}.tox .tox-well>:only-child{margin:0}.tox .tox-custom-editor{border:1px solid #000;border-radius:3px;display:flex;flex:1;overflow:hidden;position:relative}.tox .tox-dialog-loading::before{background-color:rgba(0,0,0,.5);content:\\\"\\\";height:100%;position:absolute;width:100%;z-index:1000}.tox .tox-tab{cursor:pointer}.tox .tox-dialog__content-js{display:flex;flex:1}.tox .tox-dialog__body-content .tox-collection{display:flex;flex:1}.tox:not(.tox-tinymce-inline) .tox-editor-header{background-color:none;padding:0}.tox.tox-tinymce--toolbar-bottom .tox-editor-header,.tox.tox-tinymce-inline .tox-editor-header{margin-bottom:-1px}.tox.tox-tinymce-inline .tox-editor-container{overflow:hidden}.tox:not(.tox-tinymce-inline).tox-tinymce--toolbar-bottom .tox-editor-header{border-top:none;box-shadow:none}.tox.tox.tox-tinymce--toolbar-sticky-on .tox-editor-header{background-color:transparent;box-shadow:0 4px 4px -3px rgba(0,0,0,.25);padding:0}.tox.tox.tox-tinymce--toolbar-sticky-on.tox-tinymce--toolbar-bottom .tox-editor-header{box-shadow:0 4px 4px -3px rgba(0,0,0,.25)}.tox .tox-collection--list .tox-collection__group .tox-insert-table-picker{margin:-4px 0}.tox .tox-menu.tox-collection.tox-collection--list{padding:0}.tox .tox-pop{box-shadow:none}.tox .tox-number-input,.tox .tox-split-button,.tox .tox-tbtn,.tox .tox-tbtn--select{margin:2px 0 3px 0}.tox .tox-toolbar,.tox .tox-toolbar__overflow,.tox .tox-toolbar__primary{background:url(\\\"data:image/svg+xml;charset=utf8,%3Csvg height='39px' viewBox='0 0 40 39px' width='40' xmlns='http://www.w3.org/2000/svg'%3E%3Crect x='0' y='38px' width='100' height='1' fill='%23000000'/%3E%3C/svg%3E\\\") left 0 top 0 #222f3e!important}.tox .tox-menubar+.tox-toolbar-overlord{border-top:none}.tox .tox-menubar+.tox-toolbar,.tox .tox-menubar+.tox-toolbar-overlord .tox-toolbar__primary{border-top:1px solid #000;margin-top:-1px}.tox.tox-tinymce-aux .tox-toolbar__overflow{border:1px solid #000;padding:0}.tox .tox-pop .tox-pop__dialog .tox-toolbar{padding:0}.tox:not(.tox-tinymce-inline) .tox-editor-header:not(:first-child) .tox-menubar{border-top:1px solid #000}.tox:not(.tox-tinymce-inline) .tox-editor-header:not(:first-child) .tox-toolbar-overlord:first-child .tox-toolbar__primary,.tox:not(.tox-tinymce-inline) .tox-editor-header:not(:first-child) .tox-toolbar:first-child{border-top:1px solid #000}.tox .tox-toolbar__group{padding:0 4px 0 4px}.tox .tox-collection__item{border-radius:0;cursor:pointer}.tox .tox-statusbar a:focus:not(:disabled):not([aria-disabled=true]),.tox .tox-statusbar a:hover:not(:disabled):not([aria-disabled=true]),.tox .tox-statusbar__path-item:focus:not(:disabled):not([aria-disabled=true]),.tox .tox-statusbar__path-item:hover:not(:disabled):not([aria-disabled=true]),.tox .tox-statusbar__wordcount:focus:not(:disabled):not([aria-disabled=true]),.tox .tox-statusbar__wordcount:hover:not(:disabled):not([aria-disabled=true]){color:#fff}.tox .tox-statusbar__branding svg{fill:rgba(255,255,255,.8);height:1em;margin-left:.3em;width:auto}@media (forced-colors:active){.tox .tox-statusbar__branding svg{fill:currentColor}}.tox .tox-statusbar__branding a{align-items:center;display:inline-flex}.tox .tox-statusbar__branding a:focus:not(:disabled):not([aria-disabled=true]) svg,.tox .tox-statusbar__branding a:hover:not(:disabled):not([aria-disabled=true]) svg{fill:#fff}.tox:not([dir=rtl]) .tox-statusbar__branding{margin-left:1ch}.tox[dir=rtl] .tox-statusbar__branding svg{margin-left:0;margin-right:.3em}.tox .tox-statusbar__resize-handle{padding-bottom:0;padding-right:0}.tox .tox-button::before{display:none}\")\n//# sourceMappingURL=skin.js.map\n"
  },
  {
    "path": "apps/web-antd/public/tinymce/skins/ui/tinymce-5-dark/skin.shadowdom.js",
    "content": "tinymce.Resource.add('ui/tinymce-5-dark/skin.shadowdom.css', \"body.tox-dialog__disable-scroll{overflow:hidden}.tox-fullscreen{border:0;height:100%;margin:0;overflow:hidden;overscroll-behavior:none;padding:0;touch-action:pinch-zoom;width:100%}.tox.tox-tinymce.tox-fullscreen .tox-statusbar__resize-handle{display:none}.tox-shadowhost.tox-fullscreen,.tox.tox-tinymce.tox-fullscreen{left:0;position:fixed;top:0;z-index:1200}.tox.tox-tinymce.tox-fullscreen{background-color:transparent}.tox-fullscreen .tox.tox-tinymce-aux,.tox-fullscreen~.tox.tox-tinymce-aux{z-index:1201}\")\n//# sourceMappingURL=skin.shadowdom.js.map\n"
  },
  {
    "path": "apps/web-antd/public/tinymce/tinymce.d.ts",
    "content": "interface StringPathBookmark {\n    start: string;\n    end?: string;\n    forward?: boolean;\n}\ninterface RangeBookmark {\n    rng: Range;\n    forward?: boolean;\n}\ninterface IdBookmark {\n    id: string;\n    keep?: boolean;\n    forward?: boolean;\n}\ninterface IndexBookmark {\n    name: string;\n    index: number;\n}\ninterface PathBookmark {\n    start: number[];\n    end?: number[];\n    isFakeCaret?: boolean;\n    forward?: boolean;\n}\ntype Bookmark = StringPathBookmark | RangeBookmark | IdBookmark | IndexBookmark | PathBookmark;\ntype NormalizedEvent<E, T = any> = E & {\n    readonly type: string;\n    readonly target: T;\n    readonly isDefaultPrevented: () => boolean;\n    readonly preventDefault: () => void;\n    readonly isPropagationStopped: () => boolean;\n    readonly stopPropagation: () => void;\n    readonly isImmediatePropagationStopped: () => boolean;\n    readonly stopImmediatePropagation: () => void;\n};\ntype MappedEvent<T extends {}, K extends string> = K extends keyof T ? T[K] : any;\ninterface NativeEventMap {\n    'beforepaste': Event;\n    'blur': FocusEvent;\n    'beforeinput': InputEvent;\n    'click': MouseEvent;\n    'compositionend': Event;\n    'compositionstart': Event;\n    'compositionupdate': Event;\n    'contextmenu': PointerEvent;\n    'copy': ClipboardEvent;\n    'cut': ClipboardEvent;\n    'dblclick': MouseEvent;\n    'drag': DragEvent;\n    'dragdrop': DragEvent;\n    'dragend': DragEvent;\n    'draggesture': DragEvent;\n    'dragover': DragEvent;\n    'dragstart': DragEvent;\n    'drop': DragEvent;\n    'focus': FocusEvent;\n    'focusin': FocusEvent;\n    'focusout': FocusEvent;\n    'input': InputEvent;\n    'keydown': KeyboardEvent;\n    'keypress': KeyboardEvent;\n    'keyup': KeyboardEvent;\n    'mousedown': MouseEvent;\n    'mouseenter': MouseEvent;\n    'mouseleave': MouseEvent;\n    'mousemove': MouseEvent;\n    'mouseout': MouseEvent;\n    'mouseover': MouseEvent;\n    'mouseup': MouseEvent;\n    'paste': ClipboardEvent;\n    'selectionchange': Event;\n    'submit': Event;\n    'touchend': TouchEvent;\n    'touchmove': TouchEvent;\n    'touchstart': TouchEvent;\n    'touchcancel': TouchEvent;\n    'wheel': WheelEvent;\n}\ntype EditorEvent<T> = NormalizedEvent<T>;\ninterface EventDispatcherSettings {\n    scope?: any;\n    toggleEvent?: (name: string, state: boolean) => void | boolean;\n    beforeFire?: <T>(args: EditorEvent<T>) => void;\n}\ninterface EventDispatcherConstructor<T extends {}> {\n    readonly prototype: EventDispatcher<T>;\n    new (settings?: EventDispatcherSettings): EventDispatcher<T>;\n    isNative: (name: string) => boolean;\n}\ndeclare class EventDispatcher<T extends {}> {\n    static isNative(name: string): boolean;\n    private readonly settings;\n    private readonly scope;\n    private readonly toggleEvent;\n    private bindings;\n    constructor(settings?: EventDispatcherSettings);\n    fire<K extends string, U extends MappedEvent<T, K>>(name: K, args?: U): EditorEvent<U>;\n    dispatch<K extends string, U extends MappedEvent<T, K>>(name: K, args?: U): EditorEvent<U>;\n    on<K extends string>(name: K, callback: false | ((event: EditorEvent<MappedEvent<T, K>>) => void | boolean), prepend?: boolean, extra?: {}): this;\n    off<K extends string>(name?: K, callback?: (event: EditorEvent<MappedEvent<T, K>>) => void): this;\n    once<K extends string>(name: K, callback: (event: EditorEvent<MappedEvent<T, K>>) => void, prepend?: boolean): this;\n    has(name: string): boolean;\n}\ntype UndoLevelType = 'fragmented' | 'complete';\ninterface BaseUndoLevel {\n    type: UndoLevelType;\n    bookmark: Bookmark | null;\n    beforeBookmark: Bookmark | null;\n}\ninterface FragmentedUndoLevel extends BaseUndoLevel {\n    type: 'fragmented';\n    fragments: string[];\n    content: '';\n}\ninterface CompleteUndoLevel extends BaseUndoLevel {\n    type: 'complete';\n    fragments: null;\n    content: string;\n}\ntype NewUndoLevel = CompleteUndoLevel | FragmentedUndoLevel;\ntype UndoLevel = NewUndoLevel & {\n    bookmark: Bookmark;\n};\ninterface UndoManager {\n    data: UndoLevel[];\n    typing: boolean;\n    add: (level?: Partial<UndoLevel>, event?: EditorEvent<any>) => UndoLevel | null;\n    dispatchChange: () => void;\n    beforeChange: () => void;\n    undo: () => UndoLevel | undefined;\n    redo: () => UndoLevel | undefined;\n    clear: () => void;\n    reset: () => void;\n    hasUndo: () => boolean;\n    hasRedo: () => boolean;\n    transact: (callback: () => void) => UndoLevel | null;\n    ignore: (callback: () => void) => void;\n    extra: (callback1: () => void, callback2: () => void) => void;\n}\ntype SchemaType = 'html4' | 'html5' | 'html5-strict';\ninterface ElementSettings {\n    block_elements?: string;\n    boolean_attributes?: string;\n    move_caret_before_on_enter_elements?: string;\n    non_empty_elements?: string;\n    self_closing_elements?: string;\n    text_block_elements?: string;\n    text_inline_elements?: string;\n    void_elements?: string;\n    whitespace_elements?: string;\n    transparent_elements?: string;\n    wrap_block_elements?: string;\n}\ninterface SchemaSettings extends ElementSettings {\n    custom_elements?: string | Record<string, CustomElementSpec>;\n    extended_valid_elements?: string;\n    invalid_elements?: string;\n    invalid_styles?: string | Record<string, string>;\n    schema?: SchemaType;\n    valid_children?: string;\n    valid_classes?: string | Record<string, string>;\n    valid_elements?: string;\n    valid_styles?: string | Record<string, string>;\n    verify_html?: boolean;\n    padd_empty_block_inline_children?: boolean;\n}\ninterface Attribute {\n    required?: boolean;\n    defaultValue?: string;\n    forcedValue?: string;\n    validValues?: Record<string, {}>;\n}\ninterface DefaultAttribute {\n    name: string;\n    value: string;\n}\ninterface AttributePattern extends Attribute {\n    pattern: RegExp;\n}\ninterface ElementRule {\n    attributes: Record<string, Attribute>;\n    attributesDefault?: DefaultAttribute[];\n    attributesForced?: DefaultAttribute[];\n    attributesOrder: string[];\n    attributePatterns?: AttributePattern[];\n    attributesRequired?: string[];\n    paddEmpty?: boolean;\n    removeEmpty?: boolean;\n    removeEmptyAttrs?: boolean;\n    paddInEmptyBlock?: boolean;\n}\ninterface SchemaElement extends ElementRule {\n    outputName?: string;\n    parentsRequired?: string[];\n    pattern?: RegExp;\n}\ninterface SchemaMap {\n    [name: string]: {};\n}\ninterface SchemaRegExpMap {\n    [name: string]: RegExp;\n}\ninterface CustomElementSpec {\n    extends?: string;\n    attributes?: string[];\n    children?: string[];\n    padEmpty?: boolean;\n}\ninterface Schema {\n    type: SchemaType;\n    children: Record<string, SchemaMap>;\n    elements: Record<string, SchemaElement>;\n    getValidStyles: () => Record<string, string[]> | undefined;\n    getValidClasses: () => Record<string, SchemaMap> | undefined;\n    getBlockElements: () => SchemaMap;\n    getInvalidStyles: () => Record<string, SchemaMap> | undefined;\n    getVoidElements: () => SchemaMap;\n    getTextBlockElements: () => SchemaMap;\n    getTextInlineElements: () => SchemaMap;\n    getBoolAttrs: () => SchemaMap;\n    getElementRule: (name: string) => SchemaElement | undefined;\n    getSelfClosingElements: () => SchemaMap;\n    getNonEmptyElements: () => SchemaMap;\n    getMoveCaretBeforeOnEnterElements: () => SchemaMap;\n    getWhitespaceElements: () => SchemaMap;\n    getTransparentElements: () => SchemaMap;\n    getSpecialElements: () => SchemaRegExpMap;\n    isValidChild: (name: string, child: string) => boolean;\n    isValid: (name: string, attr?: string) => boolean;\n    isBlock: (name: string) => boolean;\n    isInline: (name: string) => boolean;\n    isWrapper: (name: string) => boolean;\n    getCustomElements: () => SchemaMap;\n    addValidElements: (validElements: string) => void;\n    setValidElements: (validElements: string) => void;\n    addCustomElements: (customElements: string | Record<string, CustomElementSpec>) => void;\n    addValidChildren: (validChildren: any) => void;\n}\ntype Attributes$1 = Array<{\n    name: string;\n    value: string;\n}> & {\n    map: Record<string, string>;\n};\ninterface AstNodeConstructor {\n    readonly prototype: AstNode;\n    new (name: string, type: number): AstNode;\n    create(name: string, attrs?: Record<string, string>): AstNode;\n}\ndeclare class AstNode {\n    static create(name: string, attrs?: Record<string, string>): AstNode;\n    name: string;\n    type: number;\n    attributes?: Attributes$1;\n    value?: string;\n    parent?: AstNode | null;\n    firstChild?: AstNode | null;\n    lastChild?: AstNode | null;\n    next?: AstNode | null;\n    prev?: AstNode | null;\n    raw?: boolean;\n    constructor(name: string, type: number);\n    replace(node: AstNode): AstNode;\n    attr(name: string, value: string | null | undefined): AstNode | undefined;\n    attr(name: Record<string, string | null | undefined> | undefined): AstNode | undefined;\n    attr(name: string): string | undefined;\n    clone(): AstNode;\n    wrap(wrapper: AstNode): AstNode;\n    unwrap(): void;\n    remove(): AstNode;\n    append(node: AstNode): AstNode;\n    insert(node: AstNode, refNode: AstNode, before?: boolean): AstNode;\n    getAll(name: string): AstNode[];\n    children(): AstNode[];\n    empty(): AstNode;\n    isEmpty(elements: SchemaMap, whitespace?: SchemaMap, predicate?: (node: AstNode) => boolean): boolean;\n    walk(prev?: boolean): AstNode | null | undefined;\n}\ntype Content = string | AstNode;\ntype ContentFormat = 'raw' | 'text' | 'html' | 'tree';\ninterface GetContentArgs {\n    format: ContentFormat;\n    get: boolean;\n    getInner: boolean;\n    no_events?: boolean;\n    save?: boolean;\n    source_view?: boolean;\n    [key: string]: any;\n}\ninterface SetContentArgs {\n    format: string;\n    set: boolean;\n    content: Content;\n    no_events?: boolean;\n    no_selection?: boolean;\n    paste?: boolean;\n    load?: boolean;\n    initial?: boolean;\n    [key: string]: any;\n}\ninterface GetSelectionContentArgs extends GetContentArgs {\n    selection?: boolean;\n    contextual?: boolean;\n}\ninterface SetSelectionContentArgs extends SetContentArgs {\n    content: string;\n    selection?: boolean;\n}\ninterface BlobInfoData {\n    id?: string;\n    name?: string;\n    filename?: string;\n    blob: Blob;\n    base64: string;\n    blobUri?: string;\n    uri?: string;\n}\ninterface BlobInfo {\n    id: () => string;\n    name: () => string;\n    filename: () => string;\n    blob: () => Blob;\n    base64: () => string;\n    blobUri: () => string;\n    uri: () => string | undefined;\n}\ninterface BlobCache {\n    create: {\n        (o: BlobInfoData): BlobInfo;\n        (id: string, blob: Blob, base64: string, name?: string, filename?: string): BlobInfo;\n    };\n    add: (blobInfo: BlobInfo) => void;\n    get: (id: string) => BlobInfo | undefined;\n    getByUri: (blobUri: string) => BlobInfo | undefined;\n    getByData: (base64: string, type: string) => BlobInfo | undefined;\n    findFirst: (predicate: (blobInfo: BlobInfo) => boolean) => BlobInfo | undefined;\n    removeByUri: (blobUri: string) => void;\n    destroy: () => void;\n}\ninterface BlobInfoImagePair {\n    image: HTMLImageElement;\n    blobInfo: BlobInfo;\n}\ndeclare class NodeChange {\n    private readonly editor;\n    private lastPath;\n    constructor(editor: Editor);\n    nodeChanged(args?: Record<string, any>): void;\n    private isSameElementPath;\n}\ninterface SelectionOverrides {\n    showCaret: (direction: number, node: HTMLElement, before: boolean, scrollIntoView?: boolean) => Range | null;\n    showBlockCaretContainer: (blockCaretContainer: HTMLElement) => void;\n    hideFakeCaret: () => void;\n    destroy: () => void;\n}\ninterface Quirks {\n    refreshContentEditable(): void;\n    isHidden(): boolean;\n}\ntype DecoratorData = Record<string, any>;\ntype Decorator = (uid: string, data: DecoratorData) => {\n    attributes?: {};\n    classes?: string[];\n};\ntype AnnotationListener = (state: boolean, name: string, data?: {\n    uid: string;\n    nodes: any[];\n}) => void;\ntype AnnotationListenerApi = AnnotationListener;\ninterface AnnotatorSettings {\n    decorate: Decorator;\n    persistent?: boolean;\n}\ninterface Annotator {\n    register: (name: string, settings: AnnotatorSettings) => void;\n    annotate: (name: string, data: DecoratorData) => void;\n    annotationChanged: (name: string, f: AnnotationListenerApi) => void;\n    remove: (name: string) => void;\n    removeAll: (name: string) => void;\n    getAll: (name: string) => Record<string, Element[]>;\n}\ninterface IsEmptyOptions {\n    readonly skipBogus?: boolean;\n    readonly includeZwsp?: boolean;\n    readonly checkRootAsContent?: boolean;\n    readonly isContent?: (node: Node) => boolean;\n}\ninterface GeomRect {\n    readonly x: number;\n    readonly y: number;\n    readonly w: number;\n    readonly h: number;\n}\ninterface Rect {\n    inflate: (rect: GeomRect, w: number, h: number) => GeomRect;\n    relativePosition: (rect: GeomRect, targetRect: GeomRect, rel: string) => GeomRect;\n    findBestRelativePosition: (rect: GeomRect, targetRect: GeomRect, constrainRect: GeomRect, rels: string[]) => string | null;\n    intersect: (rect: GeomRect, cropRect: GeomRect) => GeomRect | null;\n    clamp: (rect: GeomRect, clampRect: GeomRect, fixedSize?: boolean) => GeomRect;\n    create: (x: number, y: number, w: number, h: number) => GeomRect;\n    fromClientRect: (clientRect: DOMRect) => GeomRect;\n}\ninterface NotificationManagerImpl {\n    open: (spec: NotificationSpec, closeCallback: () => void, hasEditorFocus: () => boolean) => NotificationApi;\n    close: <T extends NotificationApi>(notification: T) => void;\n    getArgs: <T extends NotificationApi>(notification: T) => NotificationSpec;\n}\ninterface NotificationSpec {\n    type?: 'info' | 'warning' | 'error' | 'success';\n    text: string;\n    icon?: string;\n    progressBar?: boolean;\n    timeout?: number;\n}\ninterface NotificationApi {\n    close: () => void;\n    progressBar: {\n        value: (percent: number) => void;\n    };\n    text: (text: string) => void;\n    reposition: () => void;\n    getEl: () => HTMLElement;\n    settings: NotificationSpec;\n}\ninterface NotificationManager {\n    open: (spec: NotificationSpec) => NotificationApi;\n    close: () => void;\n    getNotifications: () => NotificationApi[];\n}\ninterface UploadFailure {\n    message: string;\n    remove?: boolean;\n}\ntype ProgressFn = (percent: number) => void;\ntype UploadHandler = (blobInfo: BlobInfo, progress: ProgressFn) => Promise<string>;\ninterface UploadResult$2 {\n    url: string;\n    blobInfo: BlobInfo;\n    status: boolean;\n    error?: UploadFailure;\n}\ntype BlockPatternTrigger = 'enter' | 'space';\ninterface RawPattern {\n    start?: any;\n    end?: any;\n    format?: any;\n    cmd?: any;\n    value?: any;\n    replacement?: any;\n    trigger?: BlockPatternTrigger;\n}\ninterface InlineBasePattern {\n    readonly start: string;\n    readonly end: string;\n}\ninterface InlineFormatPattern extends InlineBasePattern {\n    readonly type: 'inline-format';\n    readonly format: string[];\n}\ninterface InlineCmdPattern extends InlineBasePattern {\n    readonly type: 'inline-command';\n    readonly cmd: string;\n    readonly value?: any;\n}\ntype InlinePattern = InlineFormatPattern | InlineCmdPattern;\ninterface BlockBasePattern {\n    readonly start: string;\n    readonly trigger: BlockPatternTrigger;\n}\ninterface BlockFormatPattern extends BlockBasePattern {\n    readonly type: 'block-format';\n    readonly format: string;\n}\ninterface BlockCmdPattern extends BlockBasePattern {\n    readonly type: 'block-command';\n    readonly cmd: string;\n    readonly value?: any;\n}\ntype BlockPattern = BlockFormatPattern | BlockCmdPattern;\ntype Pattern = InlinePattern | BlockPattern;\ninterface DynamicPatternContext {\n    readonly text: string;\n    readonly block: Element;\n}\ntype DynamicPatternsLookup = (ctx: DynamicPatternContext) => Pattern[];\ntype RawDynamicPatternsLookup = (ctx: DynamicPatternContext) => RawPattern[];\ninterface AlertBannerSpec {\n    type: 'alertbanner';\n    level: 'info' | 'warn' | 'error' | 'success';\n    text: string;\n    icon: string;\n    url?: string;\n}\ninterface ButtonSpec {\n    type: 'button';\n    text: string;\n    enabled?: boolean;\n    primary?: boolean;\n    name?: string;\n    icon?: string;\n    borderless?: boolean;\n    buttonType?: 'primary' | 'secondary' | 'toolbar';\n}\ninterface FormComponentSpec {\n    type: string;\n    name: string;\n}\ninterface FormComponentWithLabelSpec extends FormComponentSpec {\n    label?: string;\n}\ninterface CheckboxSpec extends FormComponentSpec {\n    type: 'checkbox';\n    label: string;\n    enabled?: boolean;\n}\ninterface CollectionSpec extends FormComponentWithLabelSpec {\n    type: 'collection';\n}\ninterface CollectionItem {\n    value: string;\n    text: string;\n    icon: string;\n}\ninterface ColorInputSpec extends FormComponentWithLabelSpec {\n    type: 'colorinput';\n    storageKey?: string;\n}\ninterface ColorPickerSpec extends FormComponentWithLabelSpec {\n    type: 'colorpicker';\n}\ninterface CustomEditorInit {\n    setValue: (value: string) => void;\n    getValue: () => string;\n    destroy: () => void;\n}\ntype CustomEditorInitFn = (elm: HTMLElement, settings: any) => Promise<CustomEditorInit>;\ninterface CustomEditorOldSpec extends FormComponentSpec {\n    type: 'customeditor';\n    tag?: string;\n    init: (e: HTMLElement) => Promise<CustomEditorInit>;\n}\ninterface CustomEditorNewSpec extends FormComponentSpec {\n    type: 'customeditor';\n    tag?: string;\n    scriptId: string;\n    scriptUrl: string;\n    onFocus?: (e: HTMLElement) => void;\n    settings?: any;\n}\ntype CustomEditorSpec = CustomEditorOldSpec | CustomEditorNewSpec;\ninterface DropZoneSpec extends FormComponentWithLabelSpec {\n    type: 'dropzone';\n}\ninterface GridSpec {\n    type: 'grid';\n    columns: number;\n    items: BodyComponentSpec[];\n}\ninterface HtmlPanelSpec {\n    type: 'htmlpanel';\n    html: string;\n    onInit?: (el: HTMLElement) => void;\n    presets?: 'presentation' | 'document';\n    stretched?: boolean;\n}\ninterface IframeSpec extends FormComponentWithLabelSpec {\n    type: 'iframe';\n    border?: boolean;\n    sandboxed?: boolean;\n    streamContent?: boolean;\n    transparent?: boolean;\n}\ninterface ImagePreviewSpec extends FormComponentSpec {\n    type: 'imagepreview';\n    height?: string;\n}\ninterface InputSpec extends FormComponentWithLabelSpec {\n    type: 'input';\n    inputMode?: string;\n    placeholder?: string;\n    maximized?: boolean;\n    enabled?: boolean;\n}\ntype Alignment = 'start' | 'center' | 'end';\ninterface LabelSpec {\n    type: 'label';\n    label: string;\n    items: BodyComponentSpec[];\n    align?: Alignment;\n    for?: string;\n}\ninterface ListBoxSingleItemSpec {\n    text: string;\n    value: string;\n}\ninterface ListBoxNestedItemSpec {\n    text: string;\n    items: ListBoxItemSpec[];\n}\ntype ListBoxItemSpec = ListBoxNestedItemSpec | ListBoxSingleItemSpec;\ninterface ListBoxSpec extends FormComponentWithLabelSpec {\n    type: 'listbox';\n    items: ListBoxItemSpec[];\n    disabled?: boolean;\n}\ninterface PanelSpec {\n    type: 'panel';\n    classes?: string[];\n    items: BodyComponentSpec[];\n}\ninterface SelectBoxItemSpec {\n    text: string;\n    value: string;\n}\ninterface SelectBoxSpec extends FormComponentWithLabelSpec {\n    type: 'selectbox';\n    items: SelectBoxItemSpec[];\n    size?: number;\n    enabled?: boolean;\n}\ninterface SizeInputSpec extends FormComponentWithLabelSpec {\n    type: 'sizeinput';\n    constrain?: boolean;\n    enabled?: boolean;\n}\ninterface SliderSpec extends FormComponentSpec {\n    type: 'slider';\n    label: string;\n    min?: number;\n    max?: number;\n}\ninterface TableSpec {\n    type: 'table';\n    header: string[];\n    cells: string[][];\n}\ninterface TextAreaSpec extends FormComponentWithLabelSpec {\n    type: 'textarea';\n    placeholder?: string;\n    maximized?: boolean;\n    enabled?: boolean;\n}\ninterface BaseToolbarButtonSpec<I extends BaseToolbarButtonInstanceApi> {\n    enabled?: boolean;\n    tooltip?: string;\n    icon?: string;\n    text?: string;\n    onSetup?: (api: I) => (api: I) => void;\n}\ninterface BaseToolbarButtonInstanceApi {\n    isEnabled: () => boolean;\n    setEnabled: (state: boolean) => void;\n    setText: (text: string) => void;\n    setIcon: (icon: string) => void;\n}\ninterface ToolbarButtonSpec extends BaseToolbarButtonSpec<ToolbarButtonInstanceApi> {\n    type?: 'button';\n    onAction: (api: ToolbarButtonInstanceApi) => void;\n    shortcut?: string;\n}\ninterface ToolbarButtonInstanceApi extends BaseToolbarButtonInstanceApi {\n}\ninterface ToolbarGroupSetting {\n    name: string;\n    items: string[];\n}\ntype ToolbarConfig = string | ToolbarGroupSetting[];\ninterface GroupToolbarButtonInstanceApi extends BaseToolbarButtonInstanceApi {\n}\ninterface GroupToolbarButtonSpec extends BaseToolbarButtonSpec<GroupToolbarButtonInstanceApi> {\n    type?: 'grouptoolbarbutton';\n    items?: ToolbarConfig;\n}\ninterface CardImageSpec {\n    type: 'cardimage';\n    src: string;\n    alt?: string;\n    classes?: string[];\n}\ninterface CardTextSpec {\n    type: 'cardtext';\n    text: string;\n    name?: string;\n    classes?: string[];\n}\ntype CardItemSpec = CardContainerSpec | CardImageSpec | CardTextSpec;\ntype CardContainerDirection = 'vertical' | 'horizontal';\ntype CardContainerAlign = 'left' | 'right';\ntype CardContainerValign = 'top' | 'middle' | 'bottom';\ninterface CardContainerSpec {\n    type: 'cardcontainer';\n    items: CardItemSpec[];\n    direction?: CardContainerDirection;\n    align?: CardContainerAlign;\n    valign?: CardContainerValign;\n}\ninterface CommonMenuItemSpec {\n    enabled?: boolean;\n    text?: string;\n    value?: string;\n    meta?: Record<string, any>;\n    shortcut?: string;\n}\ninterface CommonMenuItemInstanceApi {\n    isEnabled: () => boolean;\n    setEnabled: (state: boolean) => void;\n}\ninterface CardMenuItemInstanceApi extends CommonMenuItemInstanceApi {\n}\ninterface CardMenuItemSpec extends Omit<CommonMenuItemSpec, 'text' | 'shortcut'> {\n    type: 'cardmenuitem';\n    label?: string;\n    items: CardItemSpec[];\n    onSetup?: (api: CardMenuItemInstanceApi) => (api: CardMenuItemInstanceApi) => void;\n    onAction?: (api: CardMenuItemInstanceApi) => void;\n}\ninterface ChoiceMenuItemSpec extends CommonMenuItemSpec {\n    type?: 'choiceitem';\n    icon?: string;\n}\ninterface ChoiceMenuItemInstanceApi extends CommonMenuItemInstanceApi {\n    isActive: () => boolean;\n    setActive: (state: boolean) => void;\n}\ninterface ContextMenuItem extends CommonMenuItemSpec {\n    text: string;\n    icon?: string;\n    type?: 'item';\n    onAction: () => void;\n}\ninterface ContextSubMenu extends CommonMenuItemSpec {\n    type: 'submenu';\n    text: string;\n    icon?: string;\n    getSubmenuItems: () => string | Array<ContextMenuContents>;\n}\ntype ContextMenuContents = string | ContextMenuItem | SeparatorMenuItemSpec | ContextSubMenu;\ninterface ContextMenuApi {\n    update: (element: Element) => string | Array<ContextMenuContents>;\n}\ninterface FancyActionArgsMap {\n    'inserttable': {\n        numRows: number;\n        numColumns: number;\n    };\n    'colorswatch': {\n        value: string;\n    };\n}\ninterface BaseFancyMenuItemSpec<T extends keyof FancyActionArgsMap> {\n    type: 'fancymenuitem';\n    fancytype: T;\n    initData?: Record<string, unknown>;\n    onAction?: (data: FancyActionArgsMap[T]) => void;\n}\ninterface InsertTableMenuItemSpec extends BaseFancyMenuItemSpec<'inserttable'> {\n    fancytype: 'inserttable';\n    initData?: {};\n}\ninterface ColorSwatchMenuItemSpec extends BaseFancyMenuItemSpec<'colorswatch'> {\n    fancytype: 'colorswatch';\n    select?: (value: string) => boolean;\n    initData?: {\n        allowCustomColors?: boolean;\n        colors?: ChoiceMenuItemSpec[];\n        storageKey?: string;\n    };\n}\ntype FancyMenuItemSpec = InsertTableMenuItemSpec | ColorSwatchMenuItemSpec;\ninterface MenuItemSpec extends CommonMenuItemSpec {\n    type?: 'menuitem';\n    icon?: string;\n    onSetup?: (api: MenuItemInstanceApi) => (api: MenuItemInstanceApi) => void;\n    onAction?: (api: MenuItemInstanceApi) => void;\n}\ninterface MenuItemInstanceApi extends CommonMenuItemInstanceApi {\n}\ninterface SeparatorMenuItemSpec {\n    type?: 'separator';\n    text?: string;\n}\ninterface ToggleMenuItemSpec extends CommonMenuItemSpec {\n    type?: 'togglemenuitem';\n    icon?: string;\n    active?: boolean;\n    onSetup?: (api: ToggleMenuItemInstanceApi) => void;\n    onAction: (api: ToggleMenuItemInstanceApi) => void;\n}\ninterface ToggleMenuItemInstanceApi extends CommonMenuItemInstanceApi {\n    isActive: () => boolean;\n    setActive: (state: boolean) => void;\n}\ntype NestedMenuItemContents = string | MenuItemSpec | NestedMenuItemSpec | ToggleMenuItemSpec | SeparatorMenuItemSpec | FancyMenuItemSpec;\ninterface NestedMenuItemSpec extends CommonMenuItemSpec {\n    type?: 'nestedmenuitem';\n    icon?: string;\n    getSubmenuItems: () => string | Array<NestedMenuItemContents>;\n    onSetup?: (api: NestedMenuItemInstanceApi) => (api: NestedMenuItemInstanceApi) => void;\n}\ninterface NestedMenuItemInstanceApi extends CommonMenuItemInstanceApi {\n    setTooltip: (tooltip: string) => void;\n    setIconFill: (id: string, value: string) => void;\n}\ntype MenuButtonItemTypes = NestedMenuItemContents;\ntype SuccessCallback$1 = (menu: string | MenuButtonItemTypes[]) => void;\ninterface MenuButtonFetchContext {\n    pattern: string;\n}\ninterface BaseMenuButtonSpec {\n    text?: string;\n    tooltip?: string;\n    icon?: string;\n    search?: boolean | {\n        placeholder?: string;\n    };\n    fetch: (success: SuccessCallback$1, fetchContext: MenuButtonFetchContext, api: BaseMenuButtonInstanceApi) => void;\n    onSetup?: (api: BaseMenuButtonInstanceApi) => (api: BaseMenuButtonInstanceApi) => void;\n}\ninterface BaseMenuButtonInstanceApi {\n    isEnabled: () => boolean;\n    setEnabled: (state: boolean) => void;\n    isActive: () => boolean;\n    setActive: (state: boolean) => void;\n    setText: (text: string) => void;\n    setIcon: (icon: string) => void;\n}\ninterface ToolbarMenuButtonSpec extends BaseMenuButtonSpec {\n    type?: 'menubutton';\n    onSetup?: (api: ToolbarMenuButtonInstanceApi) => (api: ToolbarMenuButtonInstanceApi) => void;\n}\ninterface ToolbarMenuButtonInstanceApi extends BaseMenuButtonInstanceApi {\n}\ntype ToolbarSplitButtonItemTypes = ChoiceMenuItemSpec | SeparatorMenuItemSpec;\ntype SuccessCallback = (menu: ToolbarSplitButtonItemTypes[]) => void;\ntype SelectPredicate = (value: string) => boolean;\ntype PresetTypes = 'color' | 'normal' | 'listpreview';\ntype ColumnTypes$1 = number | 'auto';\ninterface ToolbarSplitButtonSpec {\n    type?: 'splitbutton';\n    tooltip?: string;\n    icon?: string;\n    text?: string;\n    select?: SelectPredicate;\n    presets?: PresetTypes;\n    columns?: ColumnTypes$1;\n    fetch: (success: SuccessCallback) => void;\n    onSetup?: (api: ToolbarSplitButtonInstanceApi) => (api: ToolbarSplitButtonInstanceApi) => void;\n    onAction: (api: ToolbarSplitButtonInstanceApi) => void;\n    onItemAction: (api: ToolbarSplitButtonInstanceApi, value: string) => void;\n}\ninterface ToolbarSplitButtonInstanceApi {\n    isEnabled: () => boolean;\n    setEnabled: (state: boolean) => void;\n    setIconFill: (id: string, value: string) => void;\n    isActive: () => boolean;\n    setActive: (state: boolean) => void;\n    setTooltip: (tooltip: string) => void;\n    setText: (text: string) => void;\n    setIcon: (icon: string) => void;\n}\ninterface BaseToolbarToggleButtonSpec<I extends BaseToolbarButtonInstanceApi> extends BaseToolbarButtonSpec<I> {\n    active?: boolean;\n}\ninterface BaseToolbarToggleButtonInstanceApi extends BaseToolbarButtonInstanceApi {\n    isActive: () => boolean;\n    setActive: (state: boolean) => void;\n}\ninterface ToolbarToggleButtonSpec extends BaseToolbarToggleButtonSpec<ToolbarToggleButtonInstanceApi> {\n    type?: 'togglebutton';\n    onAction: (api: ToolbarToggleButtonInstanceApi) => void;\n    shortcut?: string;\n}\ninterface ToolbarToggleButtonInstanceApi extends BaseToolbarToggleButtonInstanceApi {\n}\ntype Id = string;\ninterface TreeSpec {\n    type: 'tree';\n    items: TreeItemSpec[];\n    onLeafAction?: (id: Id) => void;\n    defaultExpandedIds?: Id[];\n    onToggleExpand?: (expandedIds: Id[], { expanded, node }: {\n        expanded: boolean;\n        node: Id;\n    }) => void;\n    defaultSelectedId?: Id;\n}\ninterface BaseTreeItemSpec {\n    title: string;\n    id: Id;\n    menu?: ToolbarMenuButtonSpec;\n}\ninterface DirectorySpec extends BaseTreeItemSpec {\n    type: 'directory';\n    children: TreeItemSpec[];\n}\ninterface LeafSpec extends BaseTreeItemSpec {\n    type: 'leaf';\n}\ntype TreeItemSpec = DirectorySpec | LeafSpec;\ninterface UrlInputSpec extends FormComponentWithLabelSpec {\n    type: 'urlinput';\n    filetype?: 'image' | 'media' | 'file';\n    enabled?: boolean;\n    picker_text?: string;\n}\ninterface UrlInputData {\n    value: string;\n    meta: {\n        text?: string;\n    };\n}\ntype BodyComponentSpec = BarSpec | ButtonSpec | CheckboxSpec | TextAreaSpec | InputSpec | ListBoxSpec | SelectBoxSpec | SizeInputSpec | SliderSpec | IframeSpec | HtmlPanelSpec | UrlInputSpec | DropZoneSpec | ColorInputSpec | GridSpec | ColorPickerSpec | ImagePreviewSpec | AlertBannerSpec | CollectionSpec | LabelSpec | TableSpec | TreeSpec | PanelSpec | CustomEditorSpec;\ninterface BarSpec {\n    type: 'bar';\n    items: BodyComponentSpec[];\n}\ninterface DialogToggleMenuItemSpec extends CommonMenuItemSpec {\n    type?: 'togglemenuitem';\n    name: string;\n}\ntype DialogFooterMenuButtonItemSpec = DialogToggleMenuItemSpec;\ninterface BaseDialogFooterButtonSpec {\n    name?: string;\n    align?: 'start' | 'end';\n    primary?: boolean;\n    enabled?: boolean;\n    icon?: string;\n    buttonType?: 'primary' | 'secondary';\n}\ninterface DialogFooterNormalButtonSpec extends BaseDialogFooterButtonSpec {\n    type: 'submit' | 'cancel' | 'custom';\n    text: string;\n}\ninterface DialogFooterMenuButtonSpec extends BaseDialogFooterButtonSpec {\n    type: 'menu';\n    text?: string;\n    tooltip?: string;\n    icon?: string;\n    items: DialogFooterMenuButtonItemSpec[];\n}\ninterface DialogFooterToggleButtonSpec extends BaseDialogFooterButtonSpec {\n    type: 'togglebutton';\n    tooltip?: string;\n    icon?: string;\n    text?: string;\n    active?: boolean;\n}\ntype DialogFooterButtonSpec = DialogFooterNormalButtonSpec | DialogFooterMenuButtonSpec | DialogFooterToggleButtonSpec;\ninterface TabSpec {\n    name?: string;\n    title: string;\n    items: BodyComponentSpec[];\n}\ninterface TabPanelSpec {\n    type: 'tabpanel';\n    tabs: TabSpec[];\n}\ntype DialogDataItem = any;\ntype DialogData = Record<string, DialogDataItem>;\ninterface DialogInstanceApi<T extends DialogData> {\n    getData: () => T;\n    setData: (data: Partial<T>) => void;\n    setEnabled: (name: string, state: boolean) => void;\n    focus: (name: string) => void;\n    showTab: (name: string) => void;\n    redial: (nu: DialogSpec<T>) => void;\n    block: (msg: string) => void;\n    unblock: () => void;\n    toggleFullscreen: () => void;\n    close: () => void;\n}\ninterface DialogActionDetails {\n    name: string;\n    value?: any;\n}\ninterface DialogChangeDetails<T> {\n    name: keyof T;\n}\ninterface DialogTabChangeDetails {\n    newTabName: string;\n    oldTabName: string;\n}\ntype DialogActionHandler<T extends DialogData> = (api: DialogInstanceApi<T>, details: DialogActionDetails) => void;\ntype DialogChangeHandler<T extends DialogData> = (api: DialogInstanceApi<T>, details: DialogChangeDetails<T>) => void;\ntype DialogSubmitHandler<T extends DialogData> = (api: DialogInstanceApi<T>) => void;\ntype DialogCloseHandler = () => void;\ntype DialogCancelHandler<T extends DialogData> = (api: DialogInstanceApi<T>) => void;\ntype DialogTabChangeHandler<T extends DialogData> = (api: DialogInstanceApi<T>, details: DialogTabChangeDetails) => void;\ntype DialogSize = 'normal' | 'medium' | 'large';\ninterface DialogSpec<T extends DialogData> {\n    title: string;\n    size?: DialogSize;\n    body: TabPanelSpec | PanelSpec;\n    buttons?: DialogFooterButtonSpec[];\n    initialData?: Partial<T>;\n    onAction?: DialogActionHandler<T>;\n    onChange?: DialogChangeHandler<T>;\n    onSubmit?: DialogSubmitHandler<T>;\n    onClose?: DialogCloseHandler;\n    onCancel?: DialogCancelHandler<T>;\n    onTabChange?: DialogTabChangeHandler<T>;\n}\ninterface UrlDialogInstanceApi {\n    block: (msg: string) => void;\n    unblock: () => void;\n    close: () => void;\n    sendMessage: (msg: any) => void;\n}\ninterface UrlDialogActionDetails {\n    name: string;\n    value?: any;\n}\ninterface UrlDialogMessage {\n    mceAction: string;\n    [key: string]: any;\n}\ntype UrlDialogActionHandler = (api: UrlDialogInstanceApi, actions: UrlDialogActionDetails) => void;\ntype UrlDialogCloseHandler = () => void;\ntype UrlDialogCancelHandler = (api: UrlDialogInstanceApi) => void;\ntype UrlDialogMessageHandler = (api: UrlDialogInstanceApi, message: UrlDialogMessage) => void;\ninterface UrlDialogFooterButtonSpec extends DialogFooterNormalButtonSpec {\n    type: 'cancel' | 'custom';\n}\ninterface UrlDialogSpec {\n    title: string;\n    url: string;\n    height?: number;\n    width?: number;\n    buttons?: UrlDialogFooterButtonSpec[];\n    onAction?: UrlDialogActionHandler;\n    onClose?: UrlDialogCloseHandler;\n    onCancel?: UrlDialogCancelHandler;\n    onMessage?: UrlDialogMessageHandler;\n}\ntype ColumnTypes = number | 'auto';\ntype SeparatorItemSpec = SeparatorMenuItemSpec;\ninterface AutocompleterItemSpec {\n    type?: 'autocompleteitem';\n    value: string;\n    text?: string;\n    icon?: string;\n    meta?: Record<string, any>;\n}\ntype AutocompleterContents = SeparatorItemSpec | AutocompleterItemSpec | CardMenuItemSpec;\ninterface AutocompleterSpec {\n    type?: 'autocompleter';\n    trigger: string;\n    minChars?: number;\n    columns?: ColumnTypes;\n    matches?: (rng: Range, text: string, pattern: string) => boolean;\n    fetch: (pattern: string, maxResults: number, fetchOptions: Record<string, any>) => Promise<AutocompleterContents[]>;\n    onAction: (autocompleterApi: AutocompleterInstanceApi, rng: Range, value: string, meta: Record<string, any>) => void;\n    maxResults?: number;\n    highlightOn?: string[];\n}\ninterface AutocompleterInstanceApi {\n    hide: () => void;\n    reload: (fetchOptions: Record<string, any>) => void;\n}\ntype ContextPosition = 'node' | 'selection' | 'line';\ntype ContextScope = 'node' | 'editor';\ninterface ContextBarSpec {\n    predicate?: (elem: Element) => boolean;\n    position?: ContextPosition;\n    scope?: ContextScope;\n}\ninterface ContextFormLaunchButtonApi extends BaseToolbarButtonSpec<BaseToolbarButtonInstanceApi> {\n    type: 'contextformbutton';\n}\ninterface ContextFormLaunchToggleButtonSpec extends BaseToolbarToggleButtonSpec<BaseToolbarToggleButtonInstanceApi> {\n    type: 'contextformtogglebutton';\n}\ninterface ContextFormButtonInstanceApi extends BaseToolbarButtonInstanceApi {\n}\ninterface ContextFormToggleButtonInstanceApi extends BaseToolbarToggleButtonInstanceApi {\n}\ninterface ContextFormButtonSpec extends BaseToolbarButtonSpec<ContextFormButtonInstanceApi> {\n    type?: 'contextformbutton';\n    primary?: boolean;\n    onAction: (formApi: ContextFormInstanceApi, api: ContextFormButtonInstanceApi) => void;\n}\ninterface ContextFormToggleButtonSpec extends BaseToolbarToggleButtonSpec<ContextFormToggleButtonInstanceApi> {\n    type?: 'contextformtogglebutton';\n    onAction: (formApi: ContextFormInstanceApi, buttonApi: ContextFormToggleButtonInstanceApi) => void;\n    primary?: boolean;\n}\ninterface ContextFormInstanceApi {\n    hide: () => void;\n    getValue: () => string;\n}\ninterface ContextFormSpec extends ContextBarSpec {\n    type?: 'contextform';\n    initValue?: () => string;\n    label?: string;\n    launch?: ContextFormLaunchButtonApi | ContextFormLaunchToggleButtonSpec;\n    commands: Array<ContextFormToggleButtonSpec | ContextFormButtonSpec>;\n}\ninterface ContextToolbarSpec extends ContextBarSpec {\n    type?: 'contexttoolbar';\n    items: string;\n}\ntype PublicDialog_d_AlertBannerSpec = AlertBannerSpec;\ntype PublicDialog_d_BarSpec = BarSpec;\ntype PublicDialog_d_BodyComponentSpec = BodyComponentSpec;\ntype PublicDialog_d_ButtonSpec = ButtonSpec;\ntype PublicDialog_d_CheckboxSpec = CheckboxSpec;\ntype PublicDialog_d_CollectionItem = CollectionItem;\ntype PublicDialog_d_CollectionSpec = CollectionSpec;\ntype PublicDialog_d_ColorInputSpec = ColorInputSpec;\ntype PublicDialog_d_ColorPickerSpec = ColorPickerSpec;\ntype PublicDialog_d_CustomEditorSpec = CustomEditorSpec;\ntype PublicDialog_d_CustomEditorInit = CustomEditorInit;\ntype PublicDialog_d_CustomEditorInitFn = CustomEditorInitFn;\ntype PublicDialog_d_DialogData = DialogData;\ntype PublicDialog_d_DialogSize = DialogSize;\ntype PublicDialog_d_DialogSpec<T extends DialogData> = DialogSpec<T>;\ntype PublicDialog_d_DialogInstanceApi<T extends DialogData> = DialogInstanceApi<T>;\ntype PublicDialog_d_DialogFooterButtonSpec = DialogFooterButtonSpec;\ntype PublicDialog_d_DialogActionDetails = DialogActionDetails;\ntype PublicDialog_d_DialogChangeDetails<T> = DialogChangeDetails<T>;\ntype PublicDialog_d_DialogTabChangeDetails = DialogTabChangeDetails;\ntype PublicDialog_d_DropZoneSpec = DropZoneSpec;\ntype PublicDialog_d_GridSpec = GridSpec;\ntype PublicDialog_d_HtmlPanelSpec = HtmlPanelSpec;\ntype PublicDialog_d_IframeSpec = IframeSpec;\ntype PublicDialog_d_ImagePreviewSpec = ImagePreviewSpec;\ntype PublicDialog_d_InputSpec = InputSpec;\ntype PublicDialog_d_LabelSpec = LabelSpec;\ntype PublicDialog_d_ListBoxSpec = ListBoxSpec;\ntype PublicDialog_d_ListBoxItemSpec = ListBoxItemSpec;\ntype PublicDialog_d_ListBoxNestedItemSpec = ListBoxNestedItemSpec;\ntype PublicDialog_d_ListBoxSingleItemSpec = ListBoxSingleItemSpec;\ntype PublicDialog_d_PanelSpec = PanelSpec;\ntype PublicDialog_d_SelectBoxSpec = SelectBoxSpec;\ntype PublicDialog_d_SelectBoxItemSpec = SelectBoxItemSpec;\ntype PublicDialog_d_SizeInputSpec = SizeInputSpec;\ntype PublicDialog_d_SliderSpec = SliderSpec;\ntype PublicDialog_d_TableSpec = TableSpec;\ntype PublicDialog_d_TabSpec = TabSpec;\ntype PublicDialog_d_TabPanelSpec = TabPanelSpec;\ntype PublicDialog_d_TextAreaSpec = TextAreaSpec;\ntype PublicDialog_d_TreeSpec = TreeSpec;\ntype PublicDialog_d_TreeItemSpec = TreeItemSpec;\ntype PublicDialog_d_UrlInputData = UrlInputData;\ntype PublicDialog_d_UrlInputSpec = UrlInputSpec;\ntype PublicDialog_d_UrlDialogSpec = UrlDialogSpec;\ntype PublicDialog_d_UrlDialogFooterButtonSpec = UrlDialogFooterButtonSpec;\ntype PublicDialog_d_UrlDialogInstanceApi = UrlDialogInstanceApi;\ntype PublicDialog_d_UrlDialogActionDetails = UrlDialogActionDetails;\ntype PublicDialog_d_UrlDialogMessage = UrlDialogMessage;\ndeclare namespace PublicDialog_d {\n    export { PublicDialog_d_AlertBannerSpec as AlertBannerSpec, PublicDialog_d_BarSpec as BarSpec, PublicDialog_d_BodyComponentSpec as BodyComponentSpec, PublicDialog_d_ButtonSpec as ButtonSpec, PublicDialog_d_CheckboxSpec as CheckboxSpec, PublicDialog_d_CollectionItem as CollectionItem, PublicDialog_d_CollectionSpec as CollectionSpec, PublicDialog_d_ColorInputSpec as ColorInputSpec, PublicDialog_d_ColorPickerSpec as ColorPickerSpec, PublicDialog_d_CustomEditorSpec as CustomEditorSpec, PublicDialog_d_CustomEditorInit as CustomEditorInit, PublicDialog_d_CustomEditorInitFn as CustomEditorInitFn, PublicDialog_d_DialogData as DialogData, PublicDialog_d_DialogSize as DialogSize, PublicDialog_d_DialogSpec as DialogSpec, PublicDialog_d_DialogInstanceApi as DialogInstanceApi, PublicDialog_d_DialogFooterButtonSpec as DialogFooterButtonSpec, PublicDialog_d_DialogActionDetails as DialogActionDetails, PublicDialog_d_DialogChangeDetails as DialogChangeDetails, PublicDialog_d_DialogTabChangeDetails as DialogTabChangeDetails, PublicDialog_d_DropZoneSpec as DropZoneSpec, PublicDialog_d_GridSpec as GridSpec, PublicDialog_d_HtmlPanelSpec as HtmlPanelSpec, PublicDialog_d_IframeSpec as IframeSpec, PublicDialog_d_ImagePreviewSpec as ImagePreviewSpec, PublicDialog_d_InputSpec as InputSpec, PublicDialog_d_LabelSpec as LabelSpec, PublicDialog_d_ListBoxSpec as ListBoxSpec, PublicDialog_d_ListBoxItemSpec as ListBoxItemSpec, PublicDialog_d_ListBoxNestedItemSpec as ListBoxNestedItemSpec, PublicDialog_d_ListBoxSingleItemSpec as ListBoxSingleItemSpec, PublicDialog_d_PanelSpec as PanelSpec, PublicDialog_d_SelectBoxSpec as SelectBoxSpec, PublicDialog_d_SelectBoxItemSpec as SelectBoxItemSpec, PublicDialog_d_SizeInputSpec as SizeInputSpec, PublicDialog_d_SliderSpec as SliderSpec, PublicDialog_d_TableSpec as TableSpec, PublicDialog_d_TabSpec as TabSpec, PublicDialog_d_TabPanelSpec as TabPanelSpec, PublicDialog_d_TextAreaSpec as TextAreaSpec, PublicDialog_d_TreeSpec as TreeSpec, PublicDialog_d_TreeItemSpec as TreeItemSpec, DirectorySpec as TreeDirectorySpec, LeafSpec as TreeLeafSpec, PublicDialog_d_UrlInputData as UrlInputData, PublicDialog_d_UrlInputSpec as UrlInputSpec, PublicDialog_d_UrlDialogSpec as UrlDialogSpec, PublicDialog_d_UrlDialogFooterButtonSpec as UrlDialogFooterButtonSpec, PublicDialog_d_UrlDialogInstanceApi as UrlDialogInstanceApi, PublicDialog_d_UrlDialogActionDetails as UrlDialogActionDetails, PublicDialog_d_UrlDialogMessage as UrlDialogMessage, };\n}\ntype PublicInlineContent_d_AutocompleterSpec = AutocompleterSpec;\ntype PublicInlineContent_d_AutocompleterItemSpec = AutocompleterItemSpec;\ntype PublicInlineContent_d_AutocompleterContents = AutocompleterContents;\ntype PublicInlineContent_d_AutocompleterInstanceApi = AutocompleterInstanceApi;\ntype PublicInlineContent_d_ContextPosition = ContextPosition;\ntype PublicInlineContent_d_ContextScope = ContextScope;\ntype PublicInlineContent_d_ContextFormSpec = ContextFormSpec;\ntype PublicInlineContent_d_ContextFormInstanceApi = ContextFormInstanceApi;\ntype PublicInlineContent_d_ContextFormButtonSpec = ContextFormButtonSpec;\ntype PublicInlineContent_d_ContextFormButtonInstanceApi = ContextFormButtonInstanceApi;\ntype PublicInlineContent_d_ContextFormToggleButtonSpec = ContextFormToggleButtonSpec;\ntype PublicInlineContent_d_ContextFormToggleButtonInstanceApi = ContextFormToggleButtonInstanceApi;\ntype PublicInlineContent_d_ContextToolbarSpec = ContextToolbarSpec;\ntype PublicInlineContent_d_SeparatorItemSpec = SeparatorItemSpec;\ndeclare namespace PublicInlineContent_d {\n    export { PublicInlineContent_d_AutocompleterSpec as AutocompleterSpec, PublicInlineContent_d_AutocompleterItemSpec as AutocompleterItemSpec, PublicInlineContent_d_AutocompleterContents as AutocompleterContents, PublicInlineContent_d_AutocompleterInstanceApi as AutocompleterInstanceApi, PublicInlineContent_d_ContextPosition as ContextPosition, PublicInlineContent_d_ContextScope as ContextScope, PublicInlineContent_d_ContextFormSpec as ContextFormSpec, PublicInlineContent_d_ContextFormInstanceApi as ContextFormInstanceApi, PublicInlineContent_d_ContextFormButtonSpec as ContextFormButtonSpec, PublicInlineContent_d_ContextFormButtonInstanceApi as ContextFormButtonInstanceApi, PublicInlineContent_d_ContextFormToggleButtonSpec as ContextFormToggleButtonSpec, PublicInlineContent_d_ContextFormToggleButtonInstanceApi as ContextFormToggleButtonInstanceApi, PublicInlineContent_d_ContextToolbarSpec as ContextToolbarSpec, PublicInlineContent_d_SeparatorItemSpec as SeparatorItemSpec, };\n}\ntype PublicMenu_d_MenuItemSpec = MenuItemSpec;\ntype PublicMenu_d_MenuItemInstanceApi = MenuItemInstanceApi;\ntype PublicMenu_d_NestedMenuItemContents = NestedMenuItemContents;\ntype PublicMenu_d_NestedMenuItemSpec = NestedMenuItemSpec;\ntype PublicMenu_d_NestedMenuItemInstanceApi = NestedMenuItemInstanceApi;\ntype PublicMenu_d_FancyMenuItemSpec = FancyMenuItemSpec;\ntype PublicMenu_d_ColorSwatchMenuItemSpec = ColorSwatchMenuItemSpec;\ntype PublicMenu_d_InsertTableMenuItemSpec = InsertTableMenuItemSpec;\ntype PublicMenu_d_ToggleMenuItemSpec = ToggleMenuItemSpec;\ntype PublicMenu_d_ToggleMenuItemInstanceApi = ToggleMenuItemInstanceApi;\ntype PublicMenu_d_ChoiceMenuItemSpec = ChoiceMenuItemSpec;\ntype PublicMenu_d_ChoiceMenuItemInstanceApi = ChoiceMenuItemInstanceApi;\ntype PublicMenu_d_SeparatorMenuItemSpec = SeparatorMenuItemSpec;\ntype PublicMenu_d_ContextMenuApi = ContextMenuApi;\ntype PublicMenu_d_ContextMenuContents = ContextMenuContents;\ntype PublicMenu_d_ContextMenuItem = ContextMenuItem;\ntype PublicMenu_d_ContextSubMenu = ContextSubMenu;\ntype PublicMenu_d_CardMenuItemSpec = CardMenuItemSpec;\ntype PublicMenu_d_CardMenuItemInstanceApi = CardMenuItemInstanceApi;\ntype PublicMenu_d_CardItemSpec = CardItemSpec;\ntype PublicMenu_d_CardContainerSpec = CardContainerSpec;\ntype PublicMenu_d_CardImageSpec = CardImageSpec;\ntype PublicMenu_d_CardTextSpec = CardTextSpec;\ndeclare namespace PublicMenu_d {\n    export { PublicMenu_d_MenuItemSpec as MenuItemSpec, PublicMenu_d_MenuItemInstanceApi as MenuItemInstanceApi, PublicMenu_d_NestedMenuItemContents as NestedMenuItemContents, PublicMenu_d_NestedMenuItemSpec as NestedMenuItemSpec, PublicMenu_d_NestedMenuItemInstanceApi as NestedMenuItemInstanceApi, PublicMenu_d_FancyMenuItemSpec as FancyMenuItemSpec, PublicMenu_d_ColorSwatchMenuItemSpec as ColorSwatchMenuItemSpec, PublicMenu_d_InsertTableMenuItemSpec as InsertTableMenuItemSpec, PublicMenu_d_ToggleMenuItemSpec as ToggleMenuItemSpec, PublicMenu_d_ToggleMenuItemInstanceApi as ToggleMenuItemInstanceApi, PublicMenu_d_ChoiceMenuItemSpec as ChoiceMenuItemSpec, PublicMenu_d_ChoiceMenuItemInstanceApi as ChoiceMenuItemInstanceApi, PublicMenu_d_SeparatorMenuItemSpec as SeparatorMenuItemSpec, PublicMenu_d_ContextMenuApi as ContextMenuApi, PublicMenu_d_ContextMenuContents as ContextMenuContents, PublicMenu_d_ContextMenuItem as ContextMenuItem, PublicMenu_d_ContextSubMenu as ContextSubMenu, PublicMenu_d_CardMenuItemSpec as CardMenuItemSpec, PublicMenu_d_CardMenuItemInstanceApi as CardMenuItemInstanceApi, PublicMenu_d_CardItemSpec as CardItemSpec, PublicMenu_d_CardContainerSpec as CardContainerSpec, PublicMenu_d_CardImageSpec as CardImageSpec, PublicMenu_d_CardTextSpec as CardTextSpec, };\n}\ninterface SidebarInstanceApi {\n    element: () => HTMLElement;\n}\ninterface SidebarSpec {\n    icon?: string;\n    tooltip?: string;\n    onShow?: (api: SidebarInstanceApi) => void;\n    onSetup?: (api: SidebarInstanceApi) => (api: SidebarInstanceApi) => void;\n    onHide?: (api: SidebarInstanceApi) => void;\n}\ntype PublicSidebar_d_SidebarSpec = SidebarSpec;\ntype PublicSidebar_d_SidebarInstanceApi = SidebarInstanceApi;\ndeclare namespace PublicSidebar_d {\n    export { PublicSidebar_d_SidebarSpec as SidebarSpec, PublicSidebar_d_SidebarInstanceApi as SidebarInstanceApi, };\n}\ntype PublicToolbar_d_ToolbarButtonSpec = ToolbarButtonSpec;\ntype PublicToolbar_d_ToolbarButtonInstanceApi = ToolbarButtonInstanceApi;\ntype PublicToolbar_d_ToolbarSplitButtonSpec = ToolbarSplitButtonSpec;\ntype PublicToolbar_d_ToolbarSplitButtonInstanceApi = ToolbarSplitButtonInstanceApi;\ntype PublicToolbar_d_ToolbarMenuButtonSpec = ToolbarMenuButtonSpec;\ntype PublicToolbar_d_ToolbarMenuButtonInstanceApi = ToolbarMenuButtonInstanceApi;\ntype PublicToolbar_d_ToolbarToggleButtonSpec = ToolbarToggleButtonSpec;\ntype PublicToolbar_d_ToolbarToggleButtonInstanceApi = ToolbarToggleButtonInstanceApi;\ntype PublicToolbar_d_GroupToolbarButtonSpec = GroupToolbarButtonSpec;\ntype PublicToolbar_d_GroupToolbarButtonInstanceApi = GroupToolbarButtonInstanceApi;\ndeclare namespace PublicToolbar_d {\n    export { PublicToolbar_d_ToolbarButtonSpec as ToolbarButtonSpec, PublicToolbar_d_ToolbarButtonInstanceApi as ToolbarButtonInstanceApi, PublicToolbar_d_ToolbarSplitButtonSpec as ToolbarSplitButtonSpec, PublicToolbar_d_ToolbarSplitButtonInstanceApi as ToolbarSplitButtonInstanceApi, PublicToolbar_d_ToolbarMenuButtonSpec as ToolbarMenuButtonSpec, PublicToolbar_d_ToolbarMenuButtonInstanceApi as ToolbarMenuButtonInstanceApi, PublicToolbar_d_ToolbarToggleButtonSpec as ToolbarToggleButtonSpec, PublicToolbar_d_ToolbarToggleButtonInstanceApi as ToolbarToggleButtonInstanceApi, PublicToolbar_d_GroupToolbarButtonSpec as GroupToolbarButtonSpec, PublicToolbar_d_GroupToolbarButtonInstanceApi as GroupToolbarButtonInstanceApi, };\n}\ninterface ViewButtonApi {\n    setIcon: (newIcon: string) => void;\n}\ninterface ViewToggleButtonApi extends ViewButtonApi {\n    isActive: () => boolean;\n    setActive: (state: boolean) => void;\n}\ninterface BaseButtonSpec<Api extends ViewButtonApi> {\n    text?: string;\n    icon?: string;\n    tooltip?: string;\n    buttonType?: 'primary' | 'secondary';\n    borderless?: boolean;\n    onAction: (api: Api) => void;\n}\ninterface ViewNormalButtonSpec extends BaseButtonSpec<ViewButtonApi> {\n    text: string;\n    type: 'button';\n}\ninterface ViewToggleButtonSpec extends BaseButtonSpec<ViewToggleButtonApi> {\n    type: 'togglebutton';\n    active?: boolean;\n    onAction: (api: ViewToggleButtonApi) => void;\n}\ninterface ViewButtonsGroupSpec {\n    type: 'group';\n    buttons: Array<ViewNormalButtonSpec | ViewToggleButtonSpec>;\n}\ntype ViewButtonSpec = ViewNormalButtonSpec | ViewToggleButtonSpec | ViewButtonsGroupSpec;\ninterface ViewInstanceApi {\n    getContainer: () => HTMLElement;\n}\ninterface ViewSpec {\n    buttons?: ViewButtonSpec[];\n    onShow: (api: ViewInstanceApi) => void;\n    onHide: (api: ViewInstanceApi) => void;\n}\ntype PublicView_d_ViewSpec = ViewSpec;\ntype PublicView_d_ViewInstanceApi = ViewInstanceApi;\ndeclare namespace PublicView_d {\n    export { PublicView_d_ViewSpec as ViewSpec, PublicView_d_ViewInstanceApi as ViewInstanceApi, };\n}\ninterface Registry$1 {\n    addButton: (name: string, spec: ToolbarButtonSpec) => void;\n    addGroupToolbarButton: (name: string, spec: GroupToolbarButtonSpec) => void;\n    addToggleButton: (name: string, spec: ToolbarToggleButtonSpec) => void;\n    addMenuButton: (name: string, spec: ToolbarMenuButtonSpec) => void;\n    addSplitButton: (name: string, spec: ToolbarSplitButtonSpec) => void;\n    addMenuItem: (name: string, spec: MenuItemSpec) => void;\n    addNestedMenuItem: (name: string, spec: NestedMenuItemSpec) => void;\n    addToggleMenuItem: (name: string, spec: ToggleMenuItemSpec) => void;\n    addContextMenu: (name: string, spec: ContextMenuApi) => void;\n    addContextToolbar: (name: string, spec: ContextToolbarSpec) => void;\n    addContextForm: (name: string, spec: ContextFormSpec) => void;\n    addIcon: (name: string, svgData: string) => void;\n    addAutocompleter: (name: string, spec: AutocompleterSpec) => void;\n    addSidebar: (name: string, spec: SidebarSpec) => void;\n    addView: (name: string, spec: ViewSpec) => void;\n    getAll: () => {\n        buttons: Record<string, ToolbarButtonSpec | GroupToolbarButtonSpec | ToolbarMenuButtonSpec | ToolbarSplitButtonSpec | ToolbarToggleButtonSpec>;\n        menuItems: Record<string, MenuItemSpec | NestedMenuItemSpec | ToggleMenuItemSpec>;\n        popups: Record<string, AutocompleterSpec>;\n        contextMenus: Record<string, ContextMenuApi>;\n        contextToolbars: Record<string, ContextToolbarSpec | ContextFormSpec>;\n        icons: Record<string, string>;\n        sidebars: Record<string, SidebarSpec>;\n        views: Record<string, ViewSpec>;\n    };\n}\ninterface AutocompleteLookupData {\n    readonly matchText: string;\n    readonly items: AutocompleterContents[];\n    readonly columns: ColumnTypes;\n    readonly onAction: (autoApi: AutocompleterInstanceApi, rng: Range, value: string, meta: Record<string, any>) => void;\n    readonly highlightOn: string[];\n}\ninterface AutocompleterEventArgs {\n    readonly lookupData: AutocompleteLookupData[];\n}\ninterface RangeLikeObject {\n    startContainer: Node;\n    startOffset: number;\n    endContainer: Node;\n    endOffset: number;\n}\ntype ApplyFormat = BlockFormat | InlineFormat | SelectorFormat;\ntype RemoveFormat = RemoveBlockFormat | RemoveInlineFormat | RemoveSelectorFormat;\ntype Format = ApplyFormat | RemoveFormat;\ntype Formats = Record<string, Format | Format[]>;\ntype FormatAttrOrStyleValue = string | ((vars?: FormatVars) => string | null);\ntype FormatVars = Record<string, string | null>;\ninterface BaseFormat<T> {\n    ceFalseOverride?: boolean;\n    classes?: string | string[];\n    collapsed?: boolean;\n    exact?: boolean;\n    expand?: boolean;\n    links?: boolean;\n    mixed?: boolean;\n    block_expand?: boolean;\n    onmatch?: (node: Element, fmt: T, itemName: string) => boolean;\n    remove?: 'none' | 'empty' | 'all';\n    remove_similar?: boolean;\n    split?: boolean;\n    deep?: boolean;\n    preserve_attributes?: string[];\n}\ninterface Block {\n    block: string;\n    list_block?: string;\n    wrapper?: boolean;\n}\ninterface Inline {\n    inline: string;\n}\ninterface Selector {\n    selector: string;\n    inherit?: boolean;\n}\ninterface CommonFormat<T> extends BaseFormat<T> {\n    attributes?: Record<string, FormatAttrOrStyleValue>;\n    styles?: Record<string, FormatAttrOrStyleValue>;\n    toggle?: boolean;\n    preview?: string | false;\n    onformat?: (elm: Element, fmt: T, vars?: FormatVars, node?: Node | RangeLikeObject | null) => void;\n    clear_child_styles?: boolean;\n    merge_siblings?: boolean;\n    merge_with_parents?: boolean;\n}\ninterface BlockFormat extends Block, CommonFormat<BlockFormat> {\n}\ninterface InlineFormat extends Inline, CommonFormat<InlineFormat> {\n}\ninterface SelectorFormat extends Selector, CommonFormat<SelectorFormat> {\n}\ninterface CommonRemoveFormat<T> extends BaseFormat<T> {\n    attributes?: string[] | Record<string, FormatAttrOrStyleValue>;\n    styles?: string[] | Record<string, FormatAttrOrStyleValue>;\n}\ninterface RemoveBlockFormat extends Block, CommonRemoveFormat<RemoveBlockFormat> {\n}\ninterface RemoveInlineFormat extends Inline, CommonRemoveFormat<RemoveInlineFormat> {\n}\ninterface RemoveSelectorFormat extends Selector, CommonRemoveFormat<RemoveSelectorFormat> {\n}\ninterface Filter<C extends Function> {\n    name: string;\n    callbacks: C[];\n}\ninterface ParserArgs {\n    getInner?: boolean | number;\n    forced_root_block?: boolean | string;\n    context?: string;\n    isRootContent?: boolean;\n    format?: string;\n    invalid?: boolean;\n    no_events?: boolean;\n    [key: string]: any;\n}\ntype ParserFilterCallback = (nodes: AstNode[], name: string, args: ParserArgs) => void;\ninterface ParserFilter extends Filter<ParserFilterCallback> {\n}\ninterface DomParserSettings {\n    allow_html_data_urls?: boolean;\n    allow_svg_data_urls?: boolean;\n    allow_conditional_comments?: boolean;\n    allow_html_in_named_anchor?: boolean;\n    allow_script_urls?: boolean;\n    allow_unsafe_link_target?: boolean;\n    blob_cache?: BlobCache;\n    convert_fonts_to_spans?: boolean;\n    convert_unsafe_embeds?: boolean;\n    document?: Document;\n    fix_list_elements?: boolean;\n    font_size_legacy_values?: string;\n    forced_root_block?: boolean | string;\n    forced_root_block_attrs?: Record<string, string>;\n    inline_styles?: boolean;\n    pad_empty_with_br?: boolean;\n    preserve_cdata?: boolean;\n    root_name?: string;\n    sandbox_iframes?: boolean;\n    sandbox_iframes_exclusions?: string[];\n    sanitize?: boolean;\n    validate?: boolean;\n}\ninterface DomParser {\n    schema: Schema;\n    addAttributeFilter: (name: string, callback: ParserFilterCallback) => void;\n    getAttributeFilters: () => ParserFilter[];\n    removeAttributeFilter: (name: string, callback?: ParserFilterCallback) => void;\n    addNodeFilter: (name: string, callback: ParserFilterCallback) => void;\n    getNodeFilters: () => ParserFilter[];\n    removeNodeFilter: (name: string, callback?: ParserFilterCallback) => void;\n    parse: (html: string, args?: ParserArgs) => AstNode;\n}\ninterface StyleSheetLoaderSettings {\n    maxLoadTime?: number;\n    contentCssCors?: boolean;\n    referrerPolicy?: ReferrerPolicy;\n}\ninterface StyleSheetLoader {\n    load: (url: string) => Promise<void>;\n    loadRawCss: (key: string, css: string) => void;\n    loadAll: (urls: string[]) => Promise<string[]>;\n    unload: (url: string) => void;\n    unloadRawCss: (key: string) => void;\n    unloadAll: (urls: string[]) => void;\n    _setReferrerPolicy: (referrerPolicy: ReferrerPolicy) => void;\n    _setContentCssCors: (contentCssCors: boolean) => void;\n}\ntype Registry = Registry$1;\ninterface EditorUiApi {\n    show: () => void;\n    hide: () => void;\n    setEnabled: (state: boolean) => void;\n    isEnabled: () => boolean;\n}\ninterface EditorUi extends EditorUiApi {\n    registry: Registry;\n    styleSheetLoader: StyleSheetLoader;\n}\ntype Ui_d_Registry = Registry;\ntype Ui_d_EditorUiApi = EditorUiApi;\ntype Ui_d_EditorUi = EditorUi;\ndeclare namespace Ui_d {\n    export { Ui_d_Registry as Registry, PublicDialog_d as Dialog, PublicInlineContent_d as InlineContent, PublicMenu_d as Menu, PublicView_d as View, PublicSidebar_d as Sidebar, PublicToolbar_d as Toolbar, Ui_d_EditorUiApi as EditorUiApi, Ui_d_EditorUi as EditorUi, };\n}\ninterface WindowParams {\n    readonly inline?: 'cursor' | 'toolbar' | 'bottom';\n    readonly ariaAttrs?: boolean;\n    readonly persistent?: boolean;\n}\ntype InstanceApi<T extends DialogData> = UrlDialogInstanceApi | DialogInstanceApi<T>;\ninterface WindowManagerImpl {\n    open: <T extends DialogData>(config: DialogSpec<T>, params: WindowParams | undefined, closeWindow: (dialog: DialogInstanceApi<T>) => void) => DialogInstanceApi<T>;\n    openUrl: (config: UrlDialogSpec, closeWindow: (dialog: UrlDialogInstanceApi) => void) => UrlDialogInstanceApi;\n    alert: (message: string, callback: () => void) => void;\n    confirm: (message: string, callback: (state: boolean) => void) => void;\n    close: (dialog: InstanceApi<any>) => void;\n}\ninterface WindowManager {\n    open: <T extends DialogData>(config: DialogSpec<T>, params?: WindowParams) => DialogInstanceApi<T>;\n    openUrl: (config: UrlDialogSpec) => UrlDialogInstanceApi;\n    alert: (message: string, callback?: () => void, scope?: any) => void;\n    confirm: (message: string, callback?: (state: boolean) => void, scope?: any) => void;\n    close: () => void;\n}\ninterface ExecCommandEvent {\n    command: string;\n    ui: boolean;\n    value?: any;\n}\ninterface BeforeGetContentEvent extends GetContentArgs {\n    selection?: boolean;\n}\ninterface GetContentEvent extends BeforeGetContentEvent {\n    content: string;\n}\ninterface BeforeSetContentEvent extends SetContentArgs {\n    content: string;\n    selection?: boolean;\n}\ninterface SetContentEvent extends BeforeSetContentEvent {\n    content: string;\n}\ninterface SaveContentEvent extends GetContentEvent {\n    save: boolean;\n}\ninterface NewBlockEvent {\n    newBlock: Element;\n}\ninterface NodeChangeEvent {\n    element: Element;\n    parents: Node[];\n    selectionChange?: boolean;\n    initial?: boolean;\n}\ninterface FormatEvent {\n    format: string;\n    vars?: FormatVars;\n    node?: Node | RangeLikeObject | null;\n}\ninterface ObjectResizeEvent {\n    target: HTMLElement;\n    width: number;\n    height: number;\n    origin: string;\n}\ninterface ObjectSelectedEvent {\n    target: Node;\n    targetClone?: Node;\n}\ninterface ScrollIntoViewEvent {\n    elm: HTMLElement;\n    alignToTop: boolean | undefined;\n}\ninterface SetSelectionRangeEvent {\n    range: Range;\n    forward: boolean | undefined;\n}\ninterface ShowCaretEvent {\n    target: Node;\n    direction: number;\n    before: boolean;\n}\ninterface SwitchModeEvent {\n    mode: string;\n}\ninterface ChangeEvent {\n    level: UndoLevel;\n    lastLevel: UndoLevel | undefined;\n}\ninterface AddUndoEvent extends ChangeEvent {\n    originalEvent: Event | undefined;\n}\ninterface UndoRedoEvent {\n    level: UndoLevel;\n}\ninterface WindowEvent<T extends DialogData> {\n    dialog: InstanceApi<T>;\n}\ninterface ProgressStateEvent {\n    state: boolean;\n    time?: number;\n}\ninterface AfterProgressStateEvent {\n    state: boolean;\n}\ninterface PlaceholderToggleEvent {\n    state: boolean;\n}\ninterface LoadErrorEvent {\n    message: string;\n}\ninterface PreProcessEvent extends ParserArgs {\n    node: Element;\n}\ninterface PostProcessEvent extends ParserArgs {\n    content: string;\n}\ninterface PastePlainTextToggleEvent {\n    state: boolean;\n}\ninterface PastePreProcessEvent {\n    content: string;\n    readonly internal: boolean;\n}\ninterface PastePostProcessEvent {\n    node: HTMLElement;\n    readonly internal: boolean;\n}\ninterface EditableRootStateChangeEvent {\n    state: boolean;\n}\ninterface NewTableRowEvent {\n    node: HTMLTableRowElement;\n}\ninterface NewTableCellEvent {\n    node: HTMLTableCellElement;\n}\ninterface TableEventData {\n    readonly structure: boolean;\n    readonly style: boolean;\n}\ninterface TableModifiedEvent extends TableEventData {\n    readonly table: HTMLTableElement;\n}\ninterface BeforeOpenNotificationEvent {\n    notification: NotificationSpec;\n}\ninterface OpenNotificationEvent {\n    notification: NotificationApi;\n}\ninterface EditorEventMap extends Omit<NativeEventMap, 'blur' | 'focus'> {\n    'activate': {\n        relatedTarget: Editor | null;\n    };\n    'deactivate': {\n        relatedTarget: Editor;\n    };\n    'focus': {\n        blurredEditor: Editor | null;\n    };\n    'blur': {\n        focusedEditor: Editor | null;\n    };\n    'resize': UIEvent;\n    'scroll': UIEvent;\n    'input': InputEvent;\n    'beforeinput': InputEvent;\n    'detach': {};\n    'remove': {};\n    'init': {};\n    'ScrollIntoView': ScrollIntoViewEvent;\n    'AfterScrollIntoView': ScrollIntoViewEvent;\n    'ObjectResized': ObjectResizeEvent;\n    'ObjectResizeStart': ObjectResizeEvent;\n    'SwitchMode': SwitchModeEvent;\n    'ScrollWindow': Event;\n    'ResizeWindow': UIEvent;\n    'SkinLoaded': {};\n    'SkinLoadError': LoadErrorEvent;\n    'PluginLoadError': LoadErrorEvent;\n    'ModelLoadError': LoadErrorEvent;\n    'IconsLoadError': LoadErrorEvent;\n    'ThemeLoadError': LoadErrorEvent;\n    'LanguageLoadError': LoadErrorEvent;\n    'BeforeExecCommand': ExecCommandEvent;\n    'ExecCommand': ExecCommandEvent;\n    'NodeChange': NodeChangeEvent;\n    'FormatApply': FormatEvent;\n    'FormatRemove': FormatEvent;\n    'ShowCaret': ShowCaretEvent;\n    'SelectionChange': {};\n    'ObjectSelected': ObjectSelectedEvent;\n    'BeforeObjectSelected': ObjectSelectedEvent;\n    'GetSelectionRange': {\n        range: Range;\n    };\n    'SetSelectionRange': SetSelectionRangeEvent;\n    'AfterSetSelectionRange': SetSelectionRangeEvent;\n    'BeforeGetContent': BeforeGetContentEvent;\n    'GetContent': GetContentEvent;\n    'BeforeSetContent': BeforeSetContentEvent;\n    'SetContent': SetContentEvent;\n    'SaveContent': SaveContentEvent;\n    'RawSaveContent': SaveContentEvent;\n    'LoadContent': {\n        load: boolean;\n        element: HTMLElement;\n    };\n    'PreviewFormats': {};\n    'AfterPreviewFormats': {};\n    'ScriptsLoaded': {};\n    'PreInit': {};\n    'PostRender': {};\n    'NewBlock': NewBlockEvent;\n    'ClearUndos': {};\n    'TypingUndo': {};\n    'Redo': UndoRedoEvent;\n    'Undo': UndoRedoEvent;\n    'BeforeAddUndo': AddUndoEvent;\n    'AddUndo': AddUndoEvent;\n    'change': ChangeEvent;\n    'CloseWindow': WindowEvent<any>;\n    'OpenWindow': WindowEvent<any>;\n    'ProgressState': ProgressStateEvent;\n    'AfterProgressState': AfterProgressStateEvent;\n    'PlaceholderToggle': PlaceholderToggleEvent;\n    'tap': TouchEvent;\n    'longpress': TouchEvent;\n    'longpresscancel': {};\n    'PreProcess': PreProcessEvent;\n    'PostProcess': PostProcessEvent;\n    'AutocompleterStart': AutocompleterEventArgs;\n    'AutocompleterUpdate': AutocompleterEventArgs;\n    'AutocompleterEnd': {};\n    'PastePlainTextToggle': PastePlainTextToggleEvent;\n    'PastePreProcess': PastePreProcessEvent;\n    'PastePostProcess': PastePostProcessEvent;\n    'TableModified': TableModifiedEvent;\n    'NewRow': NewTableRowEvent;\n    'NewCell': NewTableCellEvent;\n    'SetAttrib': SetAttribEvent;\n    'hide': {};\n    'show': {};\n    'dirty': {};\n    'BeforeOpenNotification': BeforeOpenNotificationEvent;\n    'OpenNotification': OpenNotificationEvent;\n}\ninterface EditorManagerEventMap {\n    'AddEditor': {\n        editor: Editor;\n    };\n    'RemoveEditor': {\n        editor: Editor;\n    };\n    'BeforeUnload': {\n        returnValue: any;\n    };\n}\ntype EventTypes_d_ExecCommandEvent = ExecCommandEvent;\ntype EventTypes_d_BeforeGetContentEvent = BeforeGetContentEvent;\ntype EventTypes_d_GetContentEvent = GetContentEvent;\ntype EventTypes_d_BeforeSetContentEvent = BeforeSetContentEvent;\ntype EventTypes_d_SetContentEvent = SetContentEvent;\ntype EventTypes_d_SaveContentEvent = SaveContentEvent;\ntype EventTypes_d_NewBlockEvent = NewBlockEvent;\ntype EventTypes_d_NodeChangeEvent = NodeChangeEvent;\ntype EventTypes_d_FormatEvent = FormatEvent;\ntype EventTypes_d_ObjectResizeEvent = ObjectResizeEvent;\ntype EventTypes_d_ObjectSelectedEvent = ObjectSelectedEvent;\ntype EventTypes_d_ScrollIntoViewEvent = ScrollIntoViewEvent;\ntype EventTypes_d_SetSelectionRangeEvent = SetSelectionRangeEvent;\ntype EventTypes_d_ShowCaretEvent = ShowCaretEvent;\ntype EventTypes_d_SwitchModeEvent = SwitchModeEvent;\ntype EventTypes_d_ChangeEvent = ChangeEvent;\ntype EventTypes_d_AddUndoEvent = AddUndoEvent;\ntype EventTypes_d_UndoRedoEvent = UndoRedoEvent;\ntype EventTypes_d_WindowEvent<T extends DialogData> = WindowEvent<T>;\ntype EventTypes_d_ProgressStateEvent = ProgressStateEvent;\ntype EventTypes_d_AfterProgressStateEvent = AfterProgressStateEvent;\ntype EventTypes_d_PlaceholderToggleEvent = PlaceholderToggleEvent;\ntype EventTypes_d_LoadErrorEvent = LoadErrorEvent;\ntype EventTypes_d_PreProcessEvent = PreProcessEvent;\ntype EventTypes_d_PostProcessEvent = PostProcessEvent;\ntype EventTypes_d_PastePlainTextToggleEvent = PastePlainTextToggleEvent;\ntype EventTypes_d_PastePreProcessEvent = PastePreProcessEvent;\ntype EventTypes_d_PastePostProcessEvent = PastePostProcessEvent;\ntype EventTypes_d_EditableRootStateChangeEvent = EditableRootStateChangeEvent;\ntype EventTypes_d_NewTableRowEvent = NewTableRowEvent;\ntype EventTypes_d_NewTableCellEvent = NewTableCellEvent;\ntype EventTypes_d_TableEventData = TableEventData;\ntype EventTypes_d_TableModifiedEvent = TableModifiedEvent;\ntype EventTypes_d_BeforeOpenNotificationEvent = BeforeOpenNotificationEvent;\ntype EventTypes_d_OpenNotificationEvent = OpenNotificationEvent;\ntype EventTypes_d_EditorEventMap = EditorEventMap;\ntype EventTypes_d_EditorManagerEventMap = EditorManagerEventMap;\ndeclare namespace EventTypes_d {\n    export { EventTypes_d_ExecCommandEvent as ExecCommandEvent, EventTypes_d_BeforeGetContentEvent as BeforeGetContentEvent, EventTypes_d_GetContentEvent as GetContentEvent, EventTypes_d_BeforeSetContentEvent as BeforeSetContentEvent, EventTypes_d_SetContentEvent as SetContentEvent, EventTypes_d_SaveContentEvent as SaveContentEvent, EventTypes_d_NewBlockEvent as NewBlockEvent, EventTypes_d_NodeChangeEvent as NodeChangeEvent, EventTypes_d_FormatEvent as FormatEvent, EventTypes_d_ObjectResizeEvent as ObjectResizeEvent, EventTypes_d_ObjectSelectedEvent as ObjectSelectedEvent, EventTypes_d_ScrollIntoViewEvent as ScrollIntoViewEvent, EventTypes_d_SetSelectionRangeEvent as SetSelectionRangeEvent, EventTypes_d_ShowCaretEvent as ShowCaretEvent, EventTypes_d_SwitchModeEvent as SwitchModeEvent, EventTypes_d_ChangeEvent as ChangeEvent, EventTypes_d_AddUndoEvent as AddUndoEvent, EventTypes_d_UndoRedoEvent as UndoRedoEvent, EventTypes_d_WindowEvent as WindowEvent, EventTypes_d_ProgressStateEvent as ProgressStateEvent, EventTypes_d_AfterProgressStateEvent as AfterProgressStateEvent, EventTypes_d_PlaceholderToggleEvent as PlaceholderToggleEvent, EventTypes_d_LoadErrorEvent as LoadErrorEvent, EventTypes_d_PreProcessEvent as PreProcessEvent, EventTypes_d_PostProcessEvent as PostProcessEvent, EventTypes_d_PastePlainTextToggleEvent as PastePlainTextToggleEvent, EventTypes_d_PastePreProcessEvent as PastePreProcessEvent, EventTypes_d_PastePostProcessEvent as PastePostProcessEvent, EventTypes_d_EditableRootStateChangeEvent as EditableRootStateChangeEvent, EventTypes_d_NewTableRowEvent as NewTableRowEvent, EventTypes_d_NewTableCellEvent as NewTableCellEvent, EventTypes_d_TableEventData as TableEventData, EventTypes_d_TableModifiedEvent as TableModifiedEvent, EventTypes_d_BeforeOpenNotificationEvent as BeforeOpenNotificationEvent, EventTypes_d_OpenNotificationEvent as OpenNotificationEvent, EventTypes_d_EditorEventMap as EditorEventMap, EventTypes_d_EditorManagerEventMap as EditorManagerEventMap, };\n}\ntype Format_d_Formats = Formats;\ntype Format_d_Format = Format;\ntype Format_d_ApplyFormat = ApplyFormat;\ntype Format_d_BlockFormat = BlockFormat;\ntype Format_d_InlineFormat = InlineFormat;\ntype Format_d_SelectorFormat = SelectorFormat;\ntype Format_d_RemoveFormat = RemoveFormat;\ntype Format_d_RemoveBlockFormat = RemoveBlockFormat;\ntype Format_d_RemoveInlineFormat = RemoveInlineFormat;\ntype Format_d_RemoveSelectorFormat = RemoveSelectorFormat;\ndeclare namespace Format_d {\n    export { Format_d_Formats as Formats, Format_d_Format as Format, Format_d_ApplyFormat as ApplyFormat, Format_d_BlockFormat as BlockFormat, Format_d_InlineFormat as InlineFormat, Format_d_SelectorFormat as SelectorFormat, Format_d_RemoveFormat as RemoveFormat, Format_d_RemoveBlockFormat as RemoveBlockFormat, Format_d_RemoveInlineFormat as RemoveInlineFormat, Format_d_RemoveSelectorFormat as RemoveSelectorFormat, };\n}\ntype StyleFormat = BlockStyleFormat | InlineStyleFormat | SelectorStyleFormat;\ntype AllowedFormat = Separator | FormatReference | StyleFormat | NestedFormatting;\ninterface Separator {\n    title: string;\n}\ninterface FormatReference {\n    title: string;\n    format: string;\n    icon?: string;\n}\ninterface NestedFormatting {\n    title: string;\n    items: Array<FormatReference | StyleFormat>;\n}\ninterface CommonStyleFormat {\n    name?: string;\n    title: string;\n    icon?: string;\n}\ninterface BlockStyleFormat extends BlockFormat, CommonStyleFormat {\n}\ninterface InlineStyleFormat extends InlineFormat, CommonStyleFormat {\n}\ninterface SelectorStyleFormat extends SelectorFormat, CommonStyleFormat {\n}\ntype EntityEncoding = 'named' | 'numeric' | 'raw' | 'named,numeric' | 'named+numeric' | 'numeric,named' | 'numeric+named';\ninterface ContentLanguage {\n    readonly title: string;\n    readonly code: string;\n    readonly customCode?: string;\n}\ntype ThemeInitFunc = (editor: Editor, elm: HTMLElement) => {\n    editorContainer: HTMLElement;\n    iframeContainer: HTMLElement;\n    height?: number;\n    iframeHeight?: number;\n    api?: EditorUiApi;\n};\ntype SetupCallback = (editor: Editor) => void;\ntype FilePickerCallback = (callback: (value: string, meta?: Record<string, any>) => void, value: string, meta: Record<string, any>) => void;\ntype FilePickerValidationStatus = 'valid' | 'unknown' | 'invalid' | 'none';\ntype FilePickerValidationCallback = (info: {\n    type: string;\n    url: string;\n}, callback: (validation: {\n    status: FilePickerValidationStatus;\n    message: string;\n}) => void) => void;\ntype PastePreProcessFn = (editor: Editor, args: PastePreProcessEvent) => void;\ntype PastePostProcessFn = (editor: Editor, args: PastePostProcessEvent) => void;\ntype URLConverter = (url: string, name: string, elm?: string | Element) => string;\ntype URLConverterCallback = (url: string, node: Node | string | undefined, on_save: boolean, name: string) => string;\ninterface ToolbarGroup {\n    name?: string;\n    items: string[];\n}\ntype ToolbarMode = 'floating' | 'sliding' | 'scrolling' | 'wrap';\ntype ToolbarLocation = 'top' | 'bottom' | 'auto';\ninterface BaseEditorOptions {\n    a11y_advanced_options?: boolean;\n    add_form_submit_trigger?: boolean;\n    add_unload_trigger?: boolean;\n    allow_conditional_comments?: boolean;\n    allow_html_data_urls?: boolean;\n    allow_html_in_named_anchor?: boolean;\n    allow_script_urls?: boolean;\n    allow_svg_data_urls?: boolean;\n    allow_unsafe_link_target?: boolean;\n    anchor_bottom?: false | string;\n    anchor_top?: false | string;\n    auto_focus?: string | true;\n    automatic_uploads?: boolean;\n    base_url?: string;\n    block_formats?: string;\n    block_unsupported_drop?: boolean;\n    body_id?: string;\n    body_class?: string;\n    br_in_pre?: boolean;\n    br_newline_selector?: string;\n    browser_spellcheck?: boolean;\n    branding?: boolean;\n    cache_suffix?: string;\n    color_cols?: number;\n    color_cols_foreground?: number;\n    color_cols_background?: number;\n    color_map?: string[];\n    color_map_foreground?: string[];\n    color_map_background?: string[];\n    color_default_foreground?: string;\n    color_default_background?: string;\n    content_css?: boolean | string | string[];\n    content_css_cors?: boolean;\n    content_security_policy?: string;\n    content_style?: string;\n    content_langs?: ContentLanguage[];\n    contextmenu?: string | string[] | false;\n    contextmenu_never_use_native?: boolean;\n    convert_fonts_to_spans?: boolean;\n    convert_unsafe_embeds?: boolean;\n    convert_urls?: boolean;\n    custom_colors?: boolean;\n    custom_elements?: string | Record<string, CustomElementSpec>;\n    custom_ui_selector?: string;\n    custom_undo_redo_levels?: number;\n    default_font_stack?: string[];\n    deprecation_warnings?: boolean;\n    directionality?: 'ltr' | 'rtl';\n    doctype?: string;\n    document_base_url?: string;\n    draggable_modal?: boolean;\n    editable_class?: string;\n    editable_root?: boolean;\n    element_format?: 'xhtml' | 'html';\n    elementpath?: boolean;\n    encoding?: string;\n    end_container_on_empty_block?: boolean | string;\n    entities?: string;\n    entity_encoding?: EntityEncoding;\n    extended_valid_elements?: string;\n    event_root?: string;\n    file_picker_callback?: FilePickerCallback;\n    file_picker_types?: string;\n    file_picker_validator_handler?: FilePickerValidationCallback;\n    fix_list_elements?: boolean;\n    fixed_toolbar_container?: string;\n    fixed_toolbar_container_target?: HTMLElement;\n    font_css?: string | string[];\n    font_family_formats?: string;\n    font_size_classes?: string;\n    font_size_legacy_values?: string;\n    font_size_style_values?: string;\n    font_size_formats?: string;\n    font_size_input_default_unit?: string;\n    forced_root_block?: string;\n    forced_root_block_attrs?: Record<string, string>;\n    formats?: Formats;\n    format_noneditable_selector?: string;\n    height?: number | string;\n    help_accessibility?: boolean;\n    hidden_input?: boolean;\n    highlight_on_focus?: boolean;\n    icons?: string;\n    icons_url?: string;\n    id?: string;\n    iframe_aria_text?: string;\n    iframe_attrs?: Record<string, string>;\n    images_file_types?: string;\n    images_replace_blob_uris?: boolean;\n    images_reuse_filename?: boolean;\n    images_upload_base_path?: string;\n    images_upload_credentials?: boolean;\n    images_upload_handler?: UploadHandler;\n    images_upload_url?: string;\n    indent?: boolean;\n    indent_after?: string;\n    indent_before?: string;\n    indent_use_margin?: boolean;\n    indentation?: string;\n    init_instance_callback?: SetupCallback;\n    inline?: boolean;\n    inline_boundaries?: boolean;\n    inline_boundaries_selector?: string;\n    inline_styles?: boolean;\n    invalid_elements?: string;\n    invalid_styles?: string | Record<string, string>;\n    keep_styles?: boolean;\n    language?: string;\n    language_load?: boolean;\n    language_url?: string;\n    line_height_formats?: string;\n    max_height?: number;\n    max_width?: number;\n    menu?: Record<string, {\n        title: string;\n        items: string;\n    }>;\n    menubar?: boolean | string;\n    min_height?: number;\n    min_width?: number;\n    model?: string;\n    model_url?: string;\n    newdocument_content?: string;\n    newline_behavior?: 'block' | 'linebreak' | 'invert' | 'default';\n    no_newline_selector?: string;\n    noneditable_class?: string;\n    noneditable_regexp?: RegExp | RegExp[];\n    nowrap?: boolean;\n    object_resizing?: boolean | string;\n    pad_empty_with_br?: boolean;\n    paste_as_text?: boolean;\n    paste_block_drop?: boolean;\n    paste_data_images?: boolean;\n    paste_merge_formats?: boolean;\n    paste_postprocess?: PastePostProcessFn;\n    paste_preprocess?: PastePreProcessFn;\n    paste_remove_styles_if_webkit?: boolean;\n    paste_tab_spaces?: number;\n    paste_webkit_styles?: string;\n    placeholder?: string;\n    preserve_cdata?: boolean;\n    preview_styles?: false | string;\n    promotion?: boolean;\n    protect?: RegExp[];\n    readonly?: boolean;\n    referrer_policy?: ReferrerPolicy;\n    relative_urls?: boolean;\n    remove_script_host?: boolean;\n    remove_trailing_brs?: boolean;\n    removed_menuitems?: string;\n    resize?: boolean | 'both';\n    resize_img_proportional?: boolean;\n    root_name?: string;\n    sandbox_iframes?: boolean;\n    sandbox_iframes_exclusions?: string[];\n    schema?: SchemaType;\n    selector?: string;\n    setup?: SetupCallback;\n    sidebar_show?: string;\n    skin?: boolean | string;\n    skin_url?: string;\n    smart_paste?: boolean;\n    statusbar?: boolean;\n    style_formats?: AllowedFormat[];\n    style_formats_autohide?: boolean;\n    style_formats_merge?: boolean;\n    submit_patch?: boolean;\n    suffix?: string;\n    table_tab_navigation?: boolean;\n    target?: HTMLElement;\n    text_patterns?: RawPattern[] | false;\n    text_patterns_lookup?: RawDynamicPatternsLookup;\n    theme?: string | ThemeInitFunc | false;\n    theme_url?: string;\n    toolbar?: boolean | string | string[] | Array<ToolbarGroup>;\n    toolbar1?: string;\n    toolbar2?: string;\n    toolbar3?: string;\n    toolbar4?: string;\n    toolbar5?: string;\n    toolbar6?: string;\n    toolbar7?: string;\n    toolbar8?: string;\n    toolbar9?: string;\n    toolbar_groups?: Record<string, GroupToolbarButtonSpec>;\n    toolbar_location?: ToolbarLocation;\n    toolbar_mode?: ToolbarMode;\n    toolbar_sticky?: boolean;\n    toolbar_sticky_offset?: number;\n    typeahead_urls?: boolean;\n    ui_mode?: 'combined' | 'split';\n    url_converter?: URLConverter;\n    url_converter_scope?: any;\n    urlconverter_callback?: URLConverterCallback;\n    valid_children?: string;\n    valid_classes?: string | Record<string, string>;\n    valid_elements?: string;\n    valid_styles?: string | Record<string, string>;\n    verify_html?: boolean;\n    visual?: boolean;\n    visual_anchor_class?: string;\n    visual_table_class?: string;\n    width?: number | string;\n    xss_sanitization?: boolean;\n    license_key?: string;\n    disable_nodechange?: boolean;\n    forced_plugins?: string | string[];\n    plugin_base_urls?: Record<string, string>;\n    service_message?: string;\n    [key: string]: any;\n}\ninterface RawEditorOptions extends BaseEditorOptions {\n    external_plugins?: Record<string, string>;\n    mobile?: RawEditorOptions;\n    plugins?: string | string[];\n}\ninterface NormalizedEditorOptions extends BaseEditorOptions {\n    external_plugins: Record<string, string>;\n    forced_plugins: string[];\n    plugins: string[];\n}\ninterface EditorOptions extends NormalizedEditorOptions {\n    a11y_advanced_options: boolean;\n    allow_unsafe_link_target: boolean;\n    anchor_bottom: string;\n    anchor_top: string;\n    automatic_uploads: boolean;\n    block_formats: string;\n    body_class: string;\n    body_id: string;\n    br_newline_selector: string;\n    color_map: string[];\n    color_cols: number;\n    color_cols_foreground: number;\n    color_cols_background: number;\n    color_default_background: string;\n    color_default_foreground: string;\n    content_css: string[];\n    contextmenu: string[];\n    convert_unsafe_embeds: boolean;\n    custom_colors: boolean;\n    default_font_stack: string[];\n    document_base_url: string;\n    init_content_sync: boolean;\n    draggable_modal: boolean;\n    editable_class: string;\n    editable_root: boolean;\n    font_css: string[];\n    font_family_formats: string;\n    font_size_classes: string;\n    font_size_formats: string;\n    font_size_input_default_unit: string;\n    font_size_legacy_values: string;\n    font_size_style_values: string;\n    forced_root_block: string;\n    forced_root_block_attrs: Record<string, string>;\n    format_noneditable_selector: string;\n    height: number | string;\n    highlight_on_focus: boolean;\n    iframe_attrs: Record<string, string>;\n    images_file_types: string;\n    images_upload_base_path: string;\n    images_upload_credentials: boolean;\n    images_upload_url: string;\n    indent_use_margin: boolean;\n    indentation: string;\n    inline: boolean;\n    inline_boundaries_selector: string;\n    language: string;\n    language_load: boolean;\n    language_url: string;\n    line_height_formats: string;\n    menu: Record<string, {\n        title: string;\n        items: string;\n    }>;\n    menubar: boolean | string;\n    model: string;\n    newdocument_content: string;\n    no_newline_selector: string;\n    noneditable_class: string;\n    noneditable_regexp: RegExp[];\n    object_resizing: string;\n    pad_empty_with_br: boolean;\n    paste_as_text: boolean;\n    preview_styles: string;\n    promotion: boolean;\n    readonly: boolean;\n    removed_menuitems: string;\n    sandbox_iframes: boolean;\n    sandbox_iframes_exclusions: string[];\n    toolbar: boolean | string | string[] | Array<ToolbarGroup>;\n    toolbar_groups: Record<string, GroupToolbarButtonSpec>;\n    toolbar_location: ToolbarLocation;\n    toolbar_mode: ToolbarMode;\n    toolbar_persist: boolean;\n    toolbar_sticky: boolean;\n    toolbar_sticky_offset: number;\n    text_patterns: Pattern[];\n    text_patterns_lookup: DynamicPatternsLookup;\n    visual: boolean;\n    visual_anchor_class: string;\n    visual_table_class: string;\n    width: number | string;\n    xss_sanitization: boolean;\n}\ntype StyleMap = Record<string, string | number>;\ninterface StylesSettings {\n    allow_script_urls?: boolean;\n    allow_svg_data_urls?: boolean;\n    url_converter?: URLConverter;\n    url_converter_scope?: any;\n}\ninterface Styles {\n    parse: (css: string | undefined) => Record<string, string>;\n    serialize: (styles: StyleMap, elementName?: string) => string;\n}\ntype EventUtilsCallback<T> = (event: EventUtilsEvent<T>) => void | boolean;\ntype EventUtilsEvent<T> = NormalizedEvent<T> & {\n    metaKey: boolean;\n};\ninterface Callback$1<T> {\n    func: EventUtilsCallback<T>;\n    scope: any;\n}\ninterface CallbackList<T> extends Array<Callback$1<T>> {\n    fakeName: string | false;\n    capture: boolean;\n    nativeHandler: EventListener;\n}\ninterface EventUtilsConstructor {\n    readonly prototype: EventUtils;\n    new (): EventUtils;\n    Event: EventUtils;\n}\ndeclare class EventUtils {\n    static Event: EventUtils;\n    domLoaded: boolean;\n    events: Record<number, Record<string, CallbackList<any>>>;\n    private readonly expando;\n    private hasFocusIn;\n    private count;\n    constructor();\n    bind<K extends keyof HTMLElementEventMap>(target: any, name: K, callback: EventUtilsCallback<HTMLElementEventMap[K]>, scope?: any): EventUtilsCallback<HTMLElementEventMap[K]>;\n    bind<T = any>(target: any, names: string, callback: EventUtilsCallback<T>, scope?: any): EventUtilsCallback<T>;\n    unbind<K extends keyof HTMLElementEventMap>(target: any, name: K, callback?: EventUtilsCallback<HTMLElementEventMap[K]>): this;\n    unbind<T = any>(target: any, names: string, callback?: EventUtilsCallback<T>): this;\n    unbind(target: any): this;\n    fire(target: any, name: string, args?: {}): this;\n    dispatch(target: any, name: string, args?: {}): this;\n    clean(target: any): this;\n    destroy(): void;\n    cancel<T>(e: EventUtilsEvent<T>): boolean;\n    private executeHandlers;\n}\ninterface SetAttribEvent {\n    attrElm: HTMLElement;\n    attrName: string;\n    attrValue: string | boolean | number | null;\n}\ninterface DOMUtilsSettings {\n    schema: Schema;\n    url_converter: URLConverter;\n    url_converter_scope: any;\n    ownEvents: boolean;\n    keep_values: boolean;\n    update_styles: boolean;\n    root_element: HTMLElement | null;\n    collect: boolean;\n    onSetAttrib: (event: SetAttribEvent) => void;\n    contentCssCors: boolean;\n    referrerPolicy: ReferrerPolicy;\n}\ntype Target = Node | Window;\ntype RunArguments<T extends Node = Node> = string | T | Array<string | T> | null;\ntype BoundEvent = [\n    Target,\n    string,\n    EventUtilsCallback<any>,\n    any\n];\ntype Callback<K extends string> = EventUtilsCallback<MappedEvent<HTMLElementEventMap, K>>;\ntype RunResult<T, R> = T extends Array<any> ? R[] : false | R;\ninterface DOMUtils {\n    doc: Document;\n    settings: Partial<DOMUtilsSettings>;\n    win: Window;\n    files: Record<string, boolean>;\n    stdMode: boolean;\n    boxModel: boolean;\n    styleSheetLoader: StyleSheetLoader;\n    boundEvents: BoundEvent[];\n    styles: Styles;\n    schema: Schema;\n    events: EventUtils;\n    root: Node | null;\n    isBlock: {\n        (node: Node | null): node is HTMLElement;\n        (node: string): boolean;\n    };\n    clone: (node: Node, deep: boolean) => Node;\n    getRoot: () => HTMLElement;\n    getViewPort: (argWin?: Window) => GeomRect;\n    getRect: (elm: string | HTMLElement) => GeomRect;\n    getSize: (elm: string | HTMLElement) => {\n        w: number;\n        h: number;\n    };\n    getParent: {\n        <K extends keyof HTMLElementTagNameMap>(node: string | Node | null, selector: K, root?: Node): HTMLElementTagNameMap[K] | null;\n        <T extends Element>(node: string | Node | null, selector: string | ((node: Node) => node is T), root?: Node): T | null;\n        (node: string | Node | null, selector?: string | ((node: Node) => boolean | void), root?: Node): Node | null;\n    };\n    getParents: {\n        <K extends keyof HTMLElementTagNameMap>(elm: string | HTMLElementTagNameMap[K] | null, selector: K, root?: Node, collect?: boolean): Array<HTMLElementTagNameMap[K]>;\n        <T extends Element>(node: string | Node | null, selector: string | ((node: Node) => node is T), root?: Node, collect?: boolean): T[];\n        (elm: string | Node | null, selector?: string | ((node: Node) => boolean | void), root?: Node, collect?: boolean): Node[];\n    };\n    get: {\n        <T extends Node>(elm: T): T;\n        (elm: string): HTMLElement | null;\n    };\n    getNext: (node: Node | null, selector: string | ((node: Node) => boolean)) => Node | null;\n    getPrev: (node: Node | null, selector: string | ((node: Node) => boolean)) => Node | null;\n    select: {\n        <K extends keyof HTMLElementTagNameMap>(selector: K, scope?: string | Node): Array<HTMLElementTagNameMap[K]>;\n        <T extends HTMLElement = HTMLElement>(selector: string, scope?: string | Node): T[];\n    };\n    is: {\n        <T extends Element>(elm: Node | Node[] | null, selector: string): elm is T;\n        (elm: Node | Node[] | null, selector: string): boolean;\n    };\n    add: (parentElm: RunArguments, name: string | Element, attrs?: Record<string, string | boolean | number | null>, html?: string | Node | null, create?: boolean) => HTMLElement;\n    create: {\n        <K extends keyof HTMLElementTagNameMap>(name: K, attrs?: Record<string, string | boolean | number | null>, html?: string | Node | null): HTMLElementTagNameMap[K];\n        (name: string, attrs?: Record<string, string | boolean | number | null>, html?: string | Node | null): HTMLElement;\n    };\n    createHTML: (name: string, attrs?: Record<string, string | null>, html?: string) => string;\n    createFragment: (html?: string) => DocumentFragment;\n    remove: {\n        <T extends Node>(node: T | T[], keepChildren?: boolean): typeof node extends Array<any> ? T[] : T;\n        <T extends Node>(node: string, keepChildren?: boolean): T | false;\n    };\n    getStyle: {\n        (elm: Element, name: string, computed: true): string;\n        (elm: string | Element | null, name: string, computed?: boolean): string | undefined;\n    };\n    setStyle: (elm: string | Element | Element[], name: string, value: string | number | null) => void;\n    setStyles: (elm: string | Element | Element[], stylesArg: StyleMap) => void;\n    removeAllAttribs: (e: RunArguments<Element>) => void;\n    setAttrib: (elm: RunArguments<Element>, name: string, value: string | boolean | number | null) => void;\n    setAttribs: (elm: RunArguments<Element>, attrs: Record<string, string | boolean | number | null>) => void;\n    getAttrib: (elm: string | Element | null, name: string, defaultVal?: string) => string;\n    getAttribs: (elm: string | Element) => NamedNodeMap | Attr[];\n    getPos: (elm: string | Element, rootElm?: Node) => {\n        x: number;\n        y: number;\n    };\n    parseStyle: (cssText: string) => Record<string, string>;\n    serializeStyle: (stylesArg: StyleMap, name?: string) => string;\n    addStyle: (cssText: string) => void;\n    loadCSS: (url: string) => void;\n    hasClass: (elm: string | Element, cls: string) => boolean;\n    addClass: (elm: RunArguments<Element>, cls: string) => void;\n    removeClass: (elm: RunArguments<Element>, cls: string) => void;\n    toggleClass: (elm: RunArguments<Element>, cls: string, state?: boolean) => void;\n    show: (elm: string | Node | Node[]) => void;\n    hide: (elm: string | Node | Node[]) => void;\n    isHidden: (elm: string | Node) => boolean;\n    uniqueId: (prefix?: string) => string;\n    setHTML: (elm: RunArguments<Element>, html: string) => void;\n    getOuterHTML: (elm: string | Node) => string;\n    setOuterHTML: (elm: string | Node | Node[], html: string) => void;\n    decode: (text: string) => string;\n    encode: (text: string) => string;\n    insertAfter: {\n        <T extends Node>(node: T | T[], reference: string | Node): T;\n        <T extends Node>(node: RunArguments<T>, reference: string | Node): RunResult<typeof node, T>;\n    };\n    replace: {\n        <T extends Node>(newElm: Node, oldElm: T | T[], keepChildren?: boolean): T;\n        <T extends Node>(newElm: Node, oldElm: RunArguments<T>, keepChildren?: boolean): false | T;\n    };\n    rename: {\n        <K extends keyof HTMLElementTagNameMap>(elm: Element, name: K): HTMLElementTagNameMap[K];\n        (elm: Element, name: string): Element;\n    };\n    findCommonAncestor: (a: Node, b: Node) => Node | null;\n    run<R, T extends Node>(this: DOMUtils, elm: T | T[], func: (node: T) => R, scope?: any): typeof elm extends Array<any> ? R[] : R;\n    run<R, T extends Node>(this: DOMUtils, elm: RunArguments<T>, func: (node: T) => R, scope?: any): RunResult<typeof elm, R>;\n    isEmpty: (node: Node, elements?: Record<string, any>, options?: IsEmptyOptions) => boolean;\n    createRng: () => Range;\n    nodeIndex: (node: Node, normalized?: boolean) => number;\n    split: {\n        <T extends Node>(parentElm: Node, splitElm: Node, replacementElm: T): T | undefined;\n        <T extends Node>(parentElm: Node, splitElm: T): T | undefined;\n    };\n    bind: {\n        <K extends string>(target: Target, name: K, func: Callback<K>, scope?: any): Callback<K>;\n        <K extends string>(target: Target[], name: K, func: Callback<K>, scope?: any): Callback<K>[];\n    };\n    unbind: {\n        <K extends string>(target: Target, name?: K, func?: EventUtilsCallback<MappedEvent<HTMLElementEventMap, K>>): EventUtils;\n        <K extends string>(target: Target[], name?: K, func?: EventUtilsCallback<MappedEvent<HTMLElementEventMap, K>>): EventUtils[];\n    };\n    fire: (target: Node | Window, name: string, evt?: {}) => EventUtils;\n    dispatch: (target: Node | Window, name: string, evt?: {}) => EventUtils;\n    getContentEditable: (node: Node) => string | null;\n    getContentEditableParent: (node: Node) => string | null;\n    isEditable: (node: Node | null | undefined) => boolean;\n    destroy: () => void;\n    isChildOf: (node: Node, parent: Node) => boolean;\n    dumpRng: (r: Range) => string;\n}\ninterface ClientRect {\n    left: number;\n    top: number;\n    bottom: number;\n    right: number;\n    width: number;\n    height: number;\n}\ninterface BookmarkManager {\n    getBookmark: (type?: number, normalized?: boolean) => Bookmark;\n    moveToBookmark: (bookmark: Bookmark) => void;\n}\ninterface ControlSelection {\n    isResizable: (elm: Element) => boolean;\n    showResizeRect: (elm: HTMLElement) => void;\n    hideResizeRect: () => void;\n    updateResizeRect: (evt: EditorEvent<any>) => void;\n    destroy: () => void;\n}\ninterface WriterSettings {\n    element_format?: 'xhtml' | 'html';\n    entities?: string;\n    entity_encoding?: EntityEncoding;\n    indent?: boolean;\n    indent_after?: string;\n    indent_before?: string;\n}\ntype Attributes = Array<{\n    name: string;\n    value: string;\n}>;\ninterface Writer {\n    cdata: (text: string) => void;\n    comment: (text: string) => void;\n    doctype: (text: string) => void;\n    end: (name: string) => void;\n    getContent: () => string;\n    pi: (name: string, text?: string) => void;\n    reset: () => void;\n    start: (name: string, attrs?: Attributes | null, empty?: boolean) => void;\n    text: (text: string, raw?: boolean) => void;\n}\ninterface HtmlSerializerSettings extends WriterSettings {\n    inner?: boolean;\n    validate?: boolean;\n}\ninterface HtmlSerializer {\n    serialize: (node: AstNode) => string;\n}\ninterface DomSerializerSettings extends DomParserSettings, WriterSettings, SchemaSettings, HtmlSerializerSettings {\n    remove_trailing_brs?: boolean;\n    url_converter?: URLConverter;\n    url_converter_scope?: {};\n}\ninterface DomSerializerImpl {\n    schema: Schema;\n    addNodeFilter: (name: string, callback: ParserFilterCallback) => void;\n    addAttributeFilter: (name: string, callback: ParserFilterCallback) => void;\n    getNodeFilters: () => ParserFilter[];\n    getAttributeFilters: () => ParserFilter[];\n    removeNodeFilter: (name: string, callback?: ParserFilterCallback) => void;\n    removeAttributeFilter: (name: string, callback?: ParserFilterCallback) => void;\n    serialize: {\n        (node: Element, parserArgs: {\n            format: 'tree';\n        } & ParserArgs): AstNode;\n        (node: Element, parserArgs?: ParserArgs): string;\n    };\n    addRules: (rules: string) => void;\n    setRules: (rules: string) => void;\n    addTempAttr: (name: string) => void;\n    getTempAttrs: () => string[];\n}\ninterface DomSerializer extends DomSerializerImpl {\n}\ninterface EditorSelection {\n    bookmarkManager: BookmarkManager;\n    controlSelection: ControlSelection;\n    dom: DOMUtils;\n    win: Window;\n    serializer: DomSerializer;\n    editor: Editor;\n    collapse: (toStart?: boolean) => void;\n    setCursorLocation: {\n        (node: Node, offset: number): void;\n        (): void;\n    };\n    getContent: {\n        (args: {\n            format: 'tree';\n        } & Partial<GetSelectionContentArgs>): AstNode;\n        (args?: Partial<GetSelectionContentArgs>): string;\n    };\n    setContent: (content: string, args?: Partial<SetSelectionContentArgs>) => void;\n    getBookmark: (type?: number, normalized?: boolean) => Bookmark;\n    moveToBookmark: (bookmark: Bookmark) => void;\n    select: (node: Node, content?: boolean) => Node;\n    isCollapsed: () => boolean;\n    isEditable: () => boolean;\n    isForward: () => boolean;\n    setNode: (elm: Element) => Element;\n    getNode: () => HTMLElement;\n    getSel: () => Selection | null;\n    setRng: (rng: Range, forward?: boolean) => void;\n    getRng: () => Range;\n    getStart: (real?: boolean) => Element;\n    getEnd: (real?: boolean) => Element;\n    getSelectedBlocks: (startElm?: Element, endElm?: Element) => Element[];\n    normalize: () => Range;\n    selectorChanged: (selector: string, callback: (active: boolean, args: {\n        node: Node;\n        selector: String;\n        parents: Node[];\n    }) => void) => EditorSelection;\n    selectorChangedWithUnbind: (selector: string, callback: (active: boolean, args: {\n        node: Node;\n        selector: String;\n        parents: Node[];\n    }) => void) => {\n        unbind: () => void;\n    };\n    getScrollContainer: () => HTMLElement | undefined;\n    scrollIntoView: (elm?: HTMLElement, alignToTop?: boolean) => void;\n    placeCaretAt: (clientX: number, clientY: number) => void;\n    getBoundingClientRect: () => ClientRect | DOMRect;\n    destroy: () => void;\n    expand: (options?: {\n        type: 'word';\n    }) => void;\n}\ntype EditorCommandCallback<S> = (this: S, ui: boolean, value: any) => void;\ntype EditorCommandsCallback = (command: string, ui: boolean, value?: any) => void;\ninterface Commands {\n    state: Record<string, (command: string) => boolean>;\n    exec: Record<string, EditorCommandsCallback>;\n    value: Record<string, (command: string) => string>;\n}\ninterface ExecCommandArgs {\n    skip_focus?: boolean;\n}\ninterface EditorCommandsConstructor {\n    readonly prototype: EditorCommands;\n    new (editor: Editor): EditorCommands;\n}\ndeclare class EditorCommands {\n    private readonly editor;\n    private commands;\n    constructor(editor: Editor);\n    execCommand(command: string, ui?: boolean, value?: any, args?: ExecCommandArgs): boolean;\n    queryCommandState(command: string): boolean;\n    queryCommandValue(command: string): string;\n    addCommands<K extends keyof Commands>(commandList: Commands[K], type: K): void;\n    addCommands(commandList: Record<string, EditorCommandsCallback>): void;\n    addCommand<S>(command: string, callback: EditorCommandCallback<S>, scope: S): void;\n    addCommand(command: string, callback: EditorCommandCallback<Editor>): void;\n    queryCommandSupported(command: string): boolean;\n    addQueryStateHandler<S>(command: string, callback: (this: S) => boolean, scope: S): void;\n    addQueryStateHandler(command: string, callback: (this: Editor) => boolean): void;\n    addQueryValueHandler<S>(command: string, callback: (this: S) => string, scope: S): void;\n    addQueryValueHandler(command: string, callback: (this: Editor) => string): void;\n}\ninterface RawString {\n    raw: string;\n}\ntype Primitive = string | number | boolean | Record<string | number, any> | Function;\ntype TokenisedString = [\n    string,\n    ...Primitive[]\n];\ntype Untranslated = Primitive | TokenisedString | RawString | null | undefined;\ntype TranslatedString = string;\ninterface I18n {\n    getData: () => Record<string, Record<string, string>>;\n    setCode: (newCode: string) => void;\n    getCode: () => string;\n    add: (code: string, items: Record<string, string>) => void;\n    translate: (text: Untranslated) => TranslatedString;\n    isRtl: () => boolean;\n    hasCode: (code: string) => boolean;\n}\ninterface Observable<T extends {}> {\n    fire<K extends string, U extends MappedEvent<T, K>>(name: K, args?: U, bubble?: boolean): EditorEvent<U>;\n    dispatch<K extends string, U extends MappedEvent<T, K>>(name: K, args?: U, bubble?: boolean): EditorEvent<U>;\n    on<K extends string>(name: K, callback: (event: EditorEvent<MappedEvent<T, K>>) => void, prepend?: boolean): EventDispatcher<T>;\n    off<K extends string>(name?: K, callback?: (event: EditorEvent<MappedEvent<T, K>>) => void): EventDispatcher<T>;\n    once<K extends string>(name: K, callback: (event: EditorEvent<MappedEvent<T, K>>) => void): EventDispatcher<T>;\n    hasEventListeners(name: string): boolean;\n}\ninterface URISettings {\n    base_uri?: URI;\n}\ninterface URIConstructor {\n    readonly prototype: URI;\n    new (url: string, settings?: URISettings): URI;\n    getDocumentBaseUrl: (loc: {\n        protocol: string;\n        host?: string;\n        href?: string;\n        pathname?: string;\n    }) => string;\n    parseDataUri: (uri: string) => {\n        type: string;\n        data: string;\n    };\n}\ninterface SafeUriOptions {\n    readonly allow_html_data_urls?: boolean;\n    readonly allow_script_urls?: boolean;\n    readonly allow_svg_data_urls?: boolean;\n}\ndeclare class URI {\n    static parseDataUri(uri: string): {\n        type: string | undefined;\n        data: string;\n    };\n    static isDomSafe(uri: string, context?: string, options?: SafeUriOptions): boolean;\n    static getDocumentBaseUrl(loc: {\n        protocol: string;\n        host?: string;\n        href?: string;\n        pathname?: string;\n    }): string;\n    source: string;\n    protocol: string | undefined;\n    authority: string | undefined;\n    userInfo: string | undefined;\n    user: string | undefined;\n    password: string | undefined;\n    host: string | undefined;\n    port: string | undefined;\n    relative: string | undefined;\n    path: string;\n    directory: string;\n    file: string | undefined;\n    query: string | undefined;\n    anchor: string | undefined;\n    settings: URISettings;\n    constructor(url: string, settings?: URISettings);\n    setPath(path: string): void;\n    toRelative(uri: string): string;\n    toAbsolute(uri: string, noHost?: boolean): string;\n    isSameOrigin(uri: URI): boolean;\n    toRelPath(base: string, path: string): string;\n    toAbsPath(base: string, path: string): string;\n    getURI(noProtoHost?: boolean): string;\n}\ninterface EditorManager extends Observable<EditorManagerEventMap> {\n    defaultOptions: RawEditorOptions;\n    majorVersion: string;\n    minorVersion: string;\n    releaseDate: string;\n    activeEditor: Editor | null;\n    focusedEditor: Editor | null;\n    baseURI: URI;\n    baseURL: string;\n    documentBaseURL: string;\n    i18n: I18n;\n    suffix: string;\n    add(this: EditorManager, editor: Editor): Editor;\n    addI18n: (code: string, item: Record<string, string>) => void;\n    createEditor(this: EditorManager, id: string, options: RawEditorOptions): Editor;\n    execCommand(this: EditorManager, cmd: string, ui: boolean, value: any): boolean;\n    get(this: EditorManager): Editor[];\n    get(this: EditorManager, id: number | string): Editor | null;\n    init(this: EditorManager, options: RawEditorOptions): Promise<Editor[]>;\n    overrideDefaults(this: EditorManager, defaultOptions: Partial<RawEditorOptions>): void;\n    remove(this: EditorManager): void;\n    remove(this: EditorManager, selector: string): void;\n    remove(this: EditorManager, editor: Editor): Editor | null;\n    setActive(this: EditorManager, editor: Editor): void;\n    setup(this: EditorManager): void;\n    translate: (text: Untranslated) => TranslatedString;\n    triggerSave: () => void;\n    _setBaseUrl(this: EditorManager, baseUrl: string): void;\n}\ninterface EditorObservable extends Observable<EditorEventMap> {\n    bindPendingEventDelegates(this: Editor): void;\n    toggleNativeEvent(this: Editor, name: string, state: boolean): void;\n    unbindAllNativeEvents(this: Editor): void;\n}\ninterface ProcessorSuccess<T> {\n    valid: true;\n    value: T;\n}\ninterface ProcessorError {\n    valid: false;\n    message: string;\n}\ntype SimpleProcessor = (value: unknown) => boolean;\ntype Processor<T> = (value: unknown) => ProcessorSuccess<T> | ProcessorError;\ninterface BuiltInOptionTypeMap {\n    'string': string;\n    'number': number;\n    'boolean': boolean;\n    'array': any[];\n    'function': Function;\n    'object': any;\n    'string[]': string[];\n    'object[]': any[];\n    'regexp': RegExp;\n}\ntype BuiltInOptionType = keyof BuiltInOptionTypeMap;\ninterface BaseOptionSpec {\n    immutable?: boolean;\n    deprecated?: boolean;\n    docsUrl?: string;\n}\ninterface BuiltInOptionSpec<K extends BuiltInOptionType> extends BaseOptionSpec {\n    processor: K;\n    default?: BuiltInOptionTypeMap[K];\n}\ninterface SimpleOptionSpec<T> extends BaseOptionSpec {\n    processor: SimpleProcessor;\n    default?: T;\n}\ninterface OptionSpec<T, U> extends BaseOptionSpec {\n    processor: Processor<U>;\n    default?: T;\n}\ninterface Options {\n    register: {\n        <K extends BuiltInOptionType>(name: string, spec: BuiltInOptionSpec<K>): void;\n        <K extends keyof NormalizedEditorOptions>(name: K, spec: OptionSpec<NormalizedEditorOptions[K], EditorOptions[K]> | SimpleOptionSpec<NormalizedEditorOptions[K]>): void;\n        <T, U>(name: string, spec: OptionSpec<T, U>): void;\n        <T>(name: string, spec: SimpleOptionSpec<T>): void;\n    };\n    isRegistered: (name: string) => boolean;\n    get: {\n        <K extends keyof EditorOptions>(name: K): EditorOptions[K];\n        <T>(name: string): T | undefined;\n    };\n    set: <K extends string, T>(name: K, value: K extends keyof NormalizedEditorOptions ? NormalizedEditorOptions[K] : T) => boolean;\n    unset: (name: string) => boolean;\n    isSet: (name: string) => boolean;\n    debug: () => void;\n}\ninterface UploadResult$1 {\n    element: HTMLImageElement;\n    status: boolean;\n    blobInfo: BlobInfo;\n    uploadUri: string;\n    removed: boolean;\n}\ninterface EditorUpload {\n    blobCache: BlobCache;\n    addFilter: (filter: (img: HTMLImageElement) => boolean) => void;\n    uploadImages: () => Promise<UploadResult$1[]>;\n    uploadImagesAuto: () => Promise<UploadResult$1[]>;\n    scanForImages: () => Promise<BlobInfoImagePair[]>;\n    destroy: () => void;\n}\ntype FormatChangeCallback = (state: boolean, data: {\n    node: Node;\n    format: string;\n    parents: Element[];\n}) => void;\ninterface FormatRegistry {\n    get: {\n        (name: string): Format[] | undefined;\n        (): Record<string, Format[]>;\n    };\n    has: (name: string) => boolean;\n    register: (name: string | Formats, format?: Format[] | Format) => void;\n    unregister: (name: string) => Formats;\n}\ninterface Formatter extends FormatRegistry {\n    apply: (name: string, vars?: FormatVars, node?: Node | RangeLikeObject | null) => void;\n    remove: (name: string, vars?: FormatVars, node?: Node | Range, similar?: boolean) => void;\n    toggle: (name: string, vars?: FormatVars, node?: Node) => void;\n    match: (name: string, vars?: FormatVars, node?: Node, similar?: boolean) => boolean;\n    closest: (names: string[]) => string | null;\n    matchAll: (names: string[], vars?: FormatVars) => string[];\n    matchNode: (node: Node | null, name: string, vars?: FormatVars, similar?: boolean) => Format | undefined;\n    canApply: (name: string) => boolean;\n    formatChanged: (names: string, callback: FormatChangeCallback, similar?: boolean, vars?: FormatVars) => {\n        unbind: () => void;\n    };\n    getCssText: (format: string | ApplyFormat) => string;\n}\ninterface EditorMode {\n    isReadOnly: () => boolean;\n    set: (mode: string) => void;\n    get: () => string;\n    register: (mode: string, api: EditorModeApi) => void;\n}\ninterface EditorModeApi {\n    activate: () => void;\n    deactivate: () => void;\n    editorReadOnly: boolean;\n}\ninterface Model {\n    readonly table: {\n        readonly getSelectedCells: () => HTMLTableCellElement[];\n        readonly clearSelectedCells: (container: Node) => void;\n    };\n}\ntype ModelManager = AddOnManager<Model>;\ninterface Plugin {\n    getMetadata?: () => {\n        name: string;\n        url: string;\n    };\n    init?: (editor: Editor, url: string) => void;\n    [key: string]: any;\n}\ntype PluginManager = AddOnManager<void | Plugin>;\ninterface ShortcutsConstructor {\n    readonly prototype: Shortcuts;\n    new (editor: Editor): Shortcuts;\n}\ntype CommandFunc = string | [\n    string,\n    boolean,\n    any\n] | (() => void);\ndeclare class Shortcuts {\n    private readonly editor;\n    private readonly shortcuts;\n    private pendingPatterns;\n    constructor(editor: Editor);\n    add(pattern: string, desc: string | null, cmdFunc: CommandFunc, scope?: any): boolean;\n    remove(pattern: string): boolean;\n    private normalizeCommandFunc;\n    private createShortcut;\n    private hasModifier;\n    private isFunctionKey;\n    private matchShortcut;\n    private executeShortcutAction;\n}\ninterface RenderResult {\n    iframeContainer?: HTMLElement;\n    editorContainer: HTMLElement;\n    api?: Partial<EditorUiApi>;\n}\ninterface Theme {\n    ui?: any;\n    inline?: any;\n    execCommand?: (command: string, ui?: boolean, value?: any) => boolean;\n    destroy?: () => void;\n    init?: (editor: Editor, url: string) => void;\n    renderUI?: () => Promise<RenderResult> | RenderResult;\n    getNotificationManagerImpl?: () => NotificationManagerImpl;\n    getWindowManagerImpl?: () => WindowManagerImpl;\n}\ntype ThemeManager = AddOnManager<void | Theme>;\ninterface EditorConstructor {\n    readonly prototype: Editor;\n    new (id: string, options: RawEditorOptions, editorManager: EditorManager): Editor;\n}\ndeclare class Editor implements EditorObservable {\n    documentBaseUrl: string;\n    baseUri: URI;\n    id: string;\n    plugins: Record<string, Plugin>;\n    documentBaseURI: URI;\n    baseURI: URI;\n    contentCSS: string[];\n    contentStyles: string[];\n    ui: EditorUi;\n    mode: EditorMode;\n    options: Options;\n    editorUpload: EditorUpload;\n    shortcuts: Shortcuts;\n    loadedCSS: Record<string, any>;\n    editorCommands: EditorCommands;\n    suffix: string;\n    editorManager: EditorManager;\n    hidden: boolean;\n    inline: boolean;\n    hasVisual: boolean;\n    isNotDirty: boolean;\n    annotator: Annotator;\n    bodyElement: HTMLElement | undefined;\n    bookmark: any;\n    composing: boolean;\n    container: HTMLElement;\n    contentAreaContainer: HTMLElement;\n    contentDocument: Document;\n    contentWindow: Window;\n    delegates: Record<string, EventUtilsCallback<any>> | undefined;\n    destroyed: boolean;\n    dom: DOMUtils;\n    editorContainer: HTMLElement;\n    eventRoot: Element | undefined;\n    formatter: Formatter;\n    formElement: HTMLElement | undefined;\n    formEventDelegate: ((e: Event) => void) | undefined;\n    hasHiddenInput: boolean;\n    iframeElement: HTMLIFrameElement | null;\n    iframeHTML: string | undefined;\n    initialized: boolean;\n    notificationManager: NotificationManager;\n    orgDisplay: string;\n    orgVisibility: string | undefined;\n    parser: DomParser;\n    quirks: Quirks;\n    readonly: boolean;\n    removed: boolean;\n    schema: Schema;\n    selection: EditorSelection;\n    serializer: DomSerializer;\n    startContent: string;\n    targetElm: HTMLElement;\n    theme: Theme;\n    model: Model;\n    undoManager: UndoManager;\n    windowManager: WindowManager;\n    _beforeUnload: (() => void) | undefined;\n    _eventDispatcher: EventDispatcher<NativeEventMap> | undefined;\n    _nodeChangeDispatcher: NodeChange;\n    _pendingNativeEvents: string[];\n    _selectionOverrides: SelectionOverrides;\n    _skinLoaded: boolean;\n    _editableRoot: boolean;\n    bindPendingEventDelegates: EditorObservable['bindPendingEventDelegates'];\n    toggleNativeEvent: EditorObservable['toggleNativeEvent'];\n    unbindAllNativeEvents: EditorObservable['unbindAllNativeEvents'];\n    fire: EditorObservable['fire'];\n    dispatch: EditorObservable['dispatch'];\n    on: EditorObservable['on'];\n    off: EditorObservable['off'];\n    once: EditorObservable['once'];\n    hasEventListeners: EditorObservable['hasEventListeners'];\n    constructor(id: string, options: RawEditorOptions, editorManager: EditorManager);\n    render(): void;\n    focus(skipFocus?: boolean): void;\n    hasFocus(): boolean;\n    translate(text: Untranslated): TranslatedString;\n    getParam<K extends BuiltInOptionType>(name: string, defaultVal: BuiltInOptionTypeMap[K], type: K): BuiltInOptionTypeMap[K];\n    getParam<K extends keyof NormalizedEditorOptions>(name: K, defaultVal?: NormalizedEditorOptions[K], type?: BuiltInOptionType): NormalizedEditorOptions[K];\n    getParam<T>(name: string, defaultVal: T, type?: BuiltInOptionType): T;\n    hasPlugin(name: string, loaded?: boolean): boolean;\n    nodeChanged(args?: any): void;\n    addCommand<S>(name: string, callback: EditorCommandCallback<S>, scope: S): void;\n    addCommand(name: string, callback: EditorCommandCallback<Editor>): void;\n    addQueryStateHandler<S>(name: string, callback: (this: S) => boolean, scope?: S): void;\n    addQueryStateHandler(name: string, callback: (this: Editor) => boolean): void;\n    addQueryValueHandler<S>(name: string, callback: (this: S) => string, scope: S): void;\n    addQueryValueHandler(name: string, callback: (this: Editor) => string): void;\n    addShortcut(pattern: string, desc: string, cmdFunc: string | [\n        string,\n        boolean,\n        any\n    ] | (() => void), scope?: any): void;\n    execCommand(cmd: string, ui?: boolean, value?: any, args?: ExecCommandArgs): boolean;\n    queryCommandState(cmd: string): boolean;\n    queryCommandValue(cmd: string): string;\n    queryCommandSupported(cmd: string): boolean;\n    show(): void;\n    hide(): void;\n    isHidden(): boolean;\n    setProgressState(state: boolean, time?: number): void;\n    load(args?: Partial<SetContentArgs>): string;\n    save(args?: Partial<GetContentArgs>): string;\n    setContent(content: string, args?: Partial<SetContentArgs>): string;\n    setContent(content: AstNode, args?: Partial<SetContentArgs>): AstNode;\n    setContent(content: Content, args?: Partial<SetContentArgs>): Content;\n    getContent(args: {\n        format: 'tree';\n    } & Partial<GetContentArgs>): AstNode;\n    getContent(args?: Partial<GetContentArgs>): string;\n    insertContent(content: string, args?: any): void;\n    resetContent(initialContent?: string): void;\n    isDirty(): boolean;\n    setDirty(state: boolean): void;\n    getContainer(): HTMLElement;\n    getContentAreaContainer(): HTMLElement;\n    getElement(): HTMLElement;\n    getWin(): Window;\n    getDoc(): Document;\n    getBody(): HTMLElement;\n    convertURL(url: string, name: string, elm?: string | Element): string;\n    addVisual(elm?: HTMLElement): void;\n    setEditableRoot(state: boolean): void;\n    hasEditableRoot(): boolean;\n    remove(): void;\n    destroy(automatic?: boolean): void;\n    uploadImages(): Promise<UploadResult$1[]>;\n    _scanForImages(): Promise<BlobInfoImagePair[]>;\n}\ninterface UrlObject {\n    prefix: string;\n    resource: string;\n    suffix: string;\n}\ntype WaitState = 'added' | 'loaded';\ntype AddOnConstructor<T> = (editor: Editor, url: string) => T;\ninterface AddOnManager<T> {\n    items: AddOnConstructor<T>[];\n    urls: Record<string, string>;\n    lookup: Record<string, {\n        instance: AddOnConstructor<T>;\n    }>;\n    get: (name: string) => AddOnConstructor<T> | undefined;\n    requireLangPack: (name: string, languages?: string) => void;\n    add: (id: string, addOn: AddOnConstructor<T>) => AddOnConstructor<T>;\n    remove: (name: string) => void;\n    createUrl: (baseUrl: UrlObject, dep: string | UrlObject) => UrlObject;\n    load: (name: string, addOnUrl: string | UrlObject) => Promise<void>;\n    waitFor: (name: string, state?: WaitState) => Promise<void>;\n}\ninterface RangeUtils {\n    walk: (rng: Range, callback: (nodes: Node[]) => void) => void;\n    split: (rng: Range) => RangeLikeObject;\n    normalize: (rng: Range) => boolean;\n    expand: (rng: Range, options?: {\n        type: 'word';\n    }) => Range;\n}\ninterface ScriptLoaderSettings {\n    referrerPolicy?: ReferrerPolicy;\n}\ninterface ScriptLoaderConstructor {\n    readonly prototype: ScriptLoader;\n    new (): ScriptLoader;\n    ScriptLoader: ScriptLoader;\n}\ndeclare class ScriptLoader {\n    static ScriptLoader: ScriptLoader;\n    private settings;\n    private states;\n    private queue;\n    private scriptLoadedCallbacks;\n    private queueLoadedCallbacks;\n    private loading;\n    constructor(settings?: ScriptLoaderSettings);\n    _setReferrerPolicy(referrerPolicy: ReferrerPolicy): void;\n    loadScript(url: string): Promise<void>;\n    isDone(url: string): boolean;\n    markDone(url: string): void;\n    add(url: string): Promise<void>;\n    load(url: string): Promise<void>;\n    remove(url: string): void;\n    loadQueue(): Promise<void>;\n    loadScripts(scripts: string[]): Promise<void>;\n}\ntype TextProcessCallback = (node: Text, offset: number, text: string) => number;\ninterface Spot {\n    container: Text;\n    offset: number;\n}\ninterface TextSeeker {\n    backwards: (node: Node, offset: number, process: TextProcessCallback, root?: Node) => Spot | null;\n    forwards: (node: Node, offset: number, process: TextProcessCallback, root?: Node) => Spot | null;\n}\ninterface DomTreeWalkerConstructor {\n    readonly prototype: DomTreeWalker;\n    new (startNode: Node, rootNode: Node): DomTreeWalker;\n}\ndeclare class DomTreeWalker {\n    private readonly rootNode;\n    private node;\n    constructor(startNode: Node, rootNode: Node);\n    current(): Node | null | undefined;\n    next(shallow?: boolean): Node | null | undefined;\n    prev(shallow?: boolean): Node | null | undefined;\n    prev2(shallow?: boolean): Node | null | undefined;\n    private findSibling;\n    private findPreviousNode;\n}\ninterface Version {\n    major: number;\n    minor: number;\n}\ninterface Env {\n    transparentSrc: string;\n    documentMode: number;\n    cacheSuffix: any;\n    container: any;\n    canHaveCSP: boolean;\n    windowsPhone: boolean;\n    browser: {\n        current: string | undefined;\n        version: Version;\n        isEdge: () => boolean;\n        isChromium: () => boolean;\n        isIE: () => boolean;\n        isOpera: () => boolean;\n        isFirefox: () => boolean;\n        isSafari: () => boolean;\n    };\n    os: {\n        current: string | undefined;\n        version: Version;\n        isWindows: () => boolean;\n        isiOS: () => boolean;\n        isAndroid: () => boolean;\n        isMacOS: () => boolean;\n        isLinux: () => boolean;\n        isSolaris: () => boolean;\n        isFreeBSD: () => boolean;\n        isChromeOS: () => boolean;\n    };\n    deviceType: {\n        isiPad: () => boolean;\n        isiPhone: () => boolean;\n        isTablet: () => boolean;\n        isPhone: () => boolean;\n        isTouch: () => boolean;\n        isWebView: () => boolean;\n        isDesktop: () => boolean;\n    };\n}\ninterface FakeClipboardItem {\n    readonly items: Record<string, any>;\n    readonly types: ReadonlyArray<string>;\n    readonly getType: <D = any>(type: string) => D | undefined;\n}\ninterface FakeClipboard {\n    readonly FakeClipboardItem: (items: Record<string, any>) => FakeClipboardItem;\n    readonly write: (data: FakeClipboardItem[]) => void;\n    readonly read: () => FakeClipboardItem[] | undefined;\n    readonly clear: () => void;\n}\ninterface FocusManager {\n    isEditorUIElement: (elm: Element) => boolean;\n}\ninterface EntitiesMap {\n    [name: string]: string;\n}\ninterface Entities {\n    encodeRaw: (text: string, attr?: boolean) => string;\n    encodeAllRaw: (text: string) => string;\n    encodeNumeric: (text: string, attr?: boolean) => string;\n    encodeNamed: (text: string, attr?: boolean, entities?: EntitiesMap) => string;\n    getEncodeFunc: (name: string, entities?: string) => (text: string, attr?: boolean) => string;\n    decode: (text: string) => string;\n}\ninterface IconPack {\n    icons: Record<string, string>;\n}\ninterface IconManager {\n    add: (id: string, iconPack: IconPack) => void;\n    get: (id: string) => IconPack;\n    has: (id: string) => boolean;\n}\ninterface Resource {\n    load: <T = any>(id: string, url: string) => Promise<T>;\n    add: (id: string, data: any) => void;\n    has: (id: string) => boolean;\n    get: (id: string) => any;\n    unload: (id: string) => void;\n}\ntype TextPatterns_d_Pattern = Pattern;\ntype TextPatterns_d_RawPattern = RawPattern;\ntype TextPatterns_d_DynamicPatternsLookup = DynamicPatternsLookup;\ntype TextPatterns_d_RawDynamicPatternsLookup = RawDynamicPatternsLookup;\ntype TextPatterns_d_DynamicPatternContext = DynamicPatternContext;\ntype TextPatterns_d_BlockCmdPattern = BlockCmdPattern;\ntype TextPatterns_d_BlockPattern = BlockPattern;\ntype TextPatterns_d_BlockFormatPattern = BlockFormatPattern;\ntype TextPatterns_d_InlineCmdPattern = InlineCmdPattern;\ntype TextPatterns_d_InlinePattern = InlinePattern;\ntype TextPatterns_d_InlineFormatPattern = InlineFormatPattern;\ndeclare namespace TextPatterns_d {\n    export { TextPatterns_d_Pattern as Pattern, TextPatterns_d_RawPattern as RawPattern, TextPatterns_d_DynamicPatternsLookup as DynamicPatternsLookup, TextPatterns_d_RawDynamicPatternsLookup as RawDynamicPatternsLookup, TextPatterns_d_DynamicPatternContext as DynamicPatternContext, TextPatterns_d_BlockCmdPattern as BlockCmdPattern, TextPatterns_d_BlockPattern as BlockPattern, TextPatterns_d_BlockFormatPattern as BlockFormatPattern, TextPatterns_d_InlineCmdPattern as InlineCmdPattern, TextPatterns_d_InlinePattern as InlinePattern, TextPatterns_d_InlineFormatPattern as InlineFormatPattern, };\n}\ninterface Delay {\n    setEditorInterval: (editor: Editor, callback: () => void, time?: number) => number;\n    setEditorTimeout: (editor: Editor, callback: () => void, time?: number) => number;\n}\ntype UploadResult = UploadResult$2;\ninterface ImageUploader {\n    upload: (blobInfos: BlobInfo[], showNotification?: boolean) => Promise<UploadResult[]>;\n}\ntype ArrayCallback$1<T, R> = (this: any, x: T, i: number, xs: ArrayLike<T>) => R;\ntype ObjCallback$1<T, R> = (this: any, value: T, key: string, obj: Record<string, T>) => R;\ntype ArrayCallback<T, R> = ArrayCallback$1<T, R>;\ntype ObjCallback<T, R> = ObjCallback$1<T, R>;\ntype WalkCallback<T> = (this: any, o: T, i: string, n: keyof T | undefined) => boolean | void;\ninterface Tools {\n    is: (obj: any, type?: string) => boolean;\n    isArray: <T>(arr: any) => arr is Array<T>;\n    inArray: <T>(arr: ArrayLike<T>, value: T) => number;\n    grep: {\n        <T>(arr: ArrayLike<T> | null | undefined, pred?: ArrayCallback<T, boolean>): T[];\n        <T>(arr: Record<string, T> | null | undefined, pred?: ObjCallback<T, boolean>): T[];\n    };\n    trim: (str: string | null | undefined) => string;\n    toArray: <T>(obj: ArrayLike<T>) => T[];\n    hasOwn: (obj: any, name: string) => boolean;\n    makeMap: (items: ArrayLike<string> | string | undefined, delim?: string | RegExp, map?: Record<string, {}>) => Record<string, {}>;\n    each: {\n        <T>(arr: ArrayLike<T> | null | undefined, cb: ArrayCallback<T, void | boolean>, scope?: any): boolean;\n        <T>(obj: Record<string, T> | null | undefined, cb: ObjCallback<T, void | boolean>, scope?: any): boolean;\n    };\n    map: {\n        <T, R>(arr: ArrayLike<T> | null | undefined, cb: ArrayCallback<T, R>): R[];\n        <T, R>(obj: Record<string, T> | null | undefined, cb: ObjCallback<T, R>): R[];\n    };\n    extend: (obj: Object, ext: Object, ...objs: Object[]) => any;\n    walk: <T extends Record<string, any>>(obj: T, f: WalkCallback<T>, n?: keyof T, scope?: any) => void;\n    resolve: (path: string, o?: Object) => any;\n    explode: (s: string | string[], d?: string | RegExp) => string[];\n    _addCacheSuffix: (url: string) => string;\n}\ninterface KeyboardLikeEvent {\n    shiftKey: boolean;\n    ctrlKey: boolean;\n    altKey: boolean;\n    metaKey: boolean;\n}\ninterface VK {\n    BACKSPACE: number;\n    DELETE: number;\n    DOWN: number;\n    ENTER: number;\n    ESC: number;\n    LEFT: number;\n    RIGHT: number;\n    SPACEBAR: number;\n    TAB: number;\n    UP: number;\n    PAGE_UP: number;\n    PAGE_DOWN: number;\n    END: number;\n    HOME: number;\n    modifierPressed: (e: KeyboardLikeEvent) => boolean;\n    metaKeyPressed: (e: KeyboardLikeEvent) => boolean;\n}\ninterface DOMUtilsNamespace {\n    (doc: Document, settings: Partial<DOMUtilsSettings>): DOMUtils;\n    DOM: DOMUtils;\n    nodeIndex: (node: Node, normalized?: boolean) => number;\n}\ninterface RangeUtilsNamespace {\n    (dom: DOMUtils): RangeUtils;\n    compareRanges: (rng1: RangeLikeObject, rng2: RangeLikeObject) => boolean;\n    getCaretRangeFromPoint: (clientX: number, clientY: number, doc: Document) => Range;\n    getSelectedNode: (range: Range) => Node;\n    getNode: (container: Node, offset: number) => Node;\n}\ninterface AddOnManagerNamespace {\n    <T>(): AddOnManager<T>;\n    language: string | undefined;\n    languageLoad: boolean;\n    baseURL: string;\n    PluginManager: PluginManager;\n    ThemeManager: ThemeManager;\n    ModelManager: ModelManager;\n}\ninterface BookmarkManagerNamespace {\n    (selection: EditorSelection): BookmarkManager;\n    isBookmarkNode: (node: Node) => boolean;\n}\ninterface TinyMCE extends EditorManager {\n    geom: {\n        Rect: Rect;\n    };\n    util: {\n        Delay: Delay;\n        Tools: Tools;\n        VK: VK;\n        URI: URIConstructor;\n        EventDispatcher: EventDispatcherConstructor<any>;\n        Observable: Observable<any>;\n        I18n: I18n;\n        LocalStorage: Storage;\n        ImageUploader: ImageUploader;\n    };\n    dom: {\n        EventUtils: EventUtilsConstructor;\n        TreeWalker: DomTreeWalkerConstructor;\n        TextSeeker: (dom: DOMUtils, isBlockBoundary?: (node: Node) => boolean) => TextSeeker;\n        DOMUtils: DOMUtilsNamespace;\n        ScriptLoader: ScriptLoaderConstructor;\n        RangeUtils: RangeUtilsNamespace;\n        Serializer: (settings: DomSerializerSettings, editor?: Editor) => DomSerializer;\n        ControlSelection: (selection: EditorSelection, editor: Editor) => ControlSelection;\n        BookmarkManager: BookmarkManagerNamespace;\n        Selection: (dom: DOMUtils, win: Window, serializer: DomSerializer, editor: Editor) => EditorSelection;\n        StyleSheetLoader: (documentOrShadowRoot: Document | ShadowRoot, settings: StyleSheetLoaderSettings) => StyleSheetLoader;\n        Event: EventUtils;\n    };\n    html: {\n        Styles: (settings?: StylesSettings, schema?: Schema) => Styles;\n        Entities: Entities;\n        Node: AstNodeConstructor;\n        Schema: (settings?: SchemaSettings) => Schema;\n        DomParser: (settings?: DomParserSettings, schema?: Schema) => DomParser;\n        Writer: (settings?: WriterSettings) => Writer;\n        Serializer: (settings?: HtmlSerializerSettings, schema?: Schema) => HtmlSerializer;\n    };\n    AddOnManager: AddOnManagerNamespace;\n    Annotator: (editor: Editor) => Annotator;\n    Editor: EditorConstructor;\n    EditorCommands: EditorCommandsConstructor;\n    EditorManager: EditorManager;\n    EditorObservable: EditorObservable;\n    Env: Env;\n    FocusManager: FocusManager;\n    Formatter: (editor: Editor) => Formatter;\n    NotificationManager: (editor: Editor) => NotificationManager;\n    Shortcuts: ShortcutsConstructor;\n    UndoManager: (editor: Editor) => UndoManager;\n    WindowManager: (editor: Editor) => WindowManager;\n    DOM: DOMUtils;\n    ScriptLoader: ScriptLoader;\n    PluginManager: PluginManager;\n    ThemeManager: ThemeManager;\n    ModelManager: ModelManager;\n    IconManager: IconManager;\n    Resource: Resource;\n    FakeClipboard: FakeClipboard;\n    trim: Tools['trim'];\n    isArray: Tools['isArray'];\n    is: Tools['is'];\n    toArray: Tools['toArray'];\n    makeMap: Tools['makeMap'];\n    each: Tools['each'];\n    map: Tools['map'];\n    grep: Tools['grep'];\n    inArray: Tools['inArray'];\n    extend: Tools['extend'];\n    walk: Tools['walk'];\n    resolve: Tools['resolve'];\n    explode: Tools['explode'];\n    _addCacheSuffix: Tools['_addCacheSuffix'];\n}\ndeclare const tinymce: TinyMCE;\nexport { AddOnManager, Annotator, AstNode, Bookmark, BookmarkManager, ControlSelection, DOMUtils, Delay, DomParser, DomParserSettings, DomSerializer, DomSerializerSettings, DomTreeWalker, Editor, EditorCommands, EditorEvent, EditorManager, EditorModeApi, EditorObservable, EditorOptions, EditorSelection, Entities, Env, EventDispatcher, EventUtils, EventTypes_d as Events, FakeClipboard, FocusManager, Format_d as Formats, Formatter, GeomRect, HtmlSerializer, HtmlSerializerSettings, I18n, IconManager, Model, ModelManager, NotificationApi, NotificationManager, NotificationSpec, Observable, Plugin, PluginManager, RangeUtils, RawEditorOptions, Rect, Resource, Schema, SchemaSettings, ScriptLoader, Shortcuts, StyleSheetLoader, Styles, TextPatterns_d as TextPatterns, TextSeeker, Theme, ThemeManager, TinyMCE, Tools, URI, Ui_d as Ui, UndoManager, VK, WindowManager, Writer, WriterSettings, tinymce as default };\n"
  },
  {
    "path": "apps/web-antd/src/adapter/component/index.ts",
    "content": "/**\n * 通用组件共同的使用的基础组件，原先放在 adapter/form 内部，限制了使用范围，这里提取出来，方便其他地方使用\n * 可用于 vben-form、vben-modal、vben-drawer 等组件使用,\n */\n\nimport type { Component } from 'vue';\n\nimport type { BaseFormComponentType } from '@vben/common-ui';\nimport type { Recordable } from '@vben/types';\n\nimport { computed, defineAsyncComponent, defineComponent, h, ref } from 'vue';\n\nimport { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui';\nimport { $t } from '@vben/locales';\n\nimport { notification } from 'ant-design-vue';\n\nimport { FileUploadOld, ImageUploadOld } from '#/components/upload-old';\n\nconst RichTextarea = defineAsyncComponent(() =>\n  import('#/components/tinymce/index').then((res) => res.Tinymce),\n);\n\nconst FileUpload = defineAsyncComponent(() =>\n  import('#/components/upload').then((res) => res.FileUpload),\n);\n\nconst ImageUpload = defineAsyncComponent(() =>\n  import('#/components/upload').then((res) => res.ImageUpload),\n);\n\nconst AutoComplete = defineAsyncComponent(\n  () => import('ant-design-vue/es/auto-complete'),\n);\nconst Button = defineAsyncComponent(() => import('ant-design-vue/es/button'));\nconst Cascader = defineAsyncComponent(\n  () => import('ant-design-vue/es/cascader'),\n);\nconst Checkbox = defineAsyncComponent(\n  () => import('ant-design-vue/es/checkbox'),\n);\nconst CheckboxGroup = defineAsyncComponent(() =>\n  import('ant-design-vue/es/checkbox').then((res) => res.CheckboxGroup),\n);\nconst DatePicker = defineAsyncComponent(\n  () => import('ant-design-vue/es/date-picker'),\n);\nconst Divider = defineAsyncComponent(() => import('ant-design-vue/es/divider'));\nconst Input = defineAsyncComponent(() => import('ant-design-vue/es/input'));\nconst InputNumber = defineAsyncComponent(\n  () => import('ant-design-vue/es/input-number'),\n);\nconst InputPassword = defineAsyncComponent(() =>\n  import('ant-design-vue/es/input').then((res) => res.InputPassword),\n);\nconst Mentions = defineAsyncComponent(\n  () => import('ant-design-vue/es/mentions'),\n);\nconst Radio = defineAsyncComponent(() => import('ant-design-vue/es/radio'));\nconst RadioGroup = defineAsyncComponent(() =>\n  import('ant-design-vue/es/radio').then((res) => res.RadioGroup),\n);\nconst RangePicker = defineAsyncComponent(() =>\n  import('ant-design-vue/es/date-picker').then((res) => res.RangePicker),\n);\nconst Rate = defineAsyncComponent(() => import('ant-design-vue/es/rate'));\nconst Select = defineAsyncComponent(() => import('ant-design-vue/es/select'));\nconst Space = defineAsyncComponent(() => import('ant-design-vue/es/space'));\nconst Switch = defineAsyncComponent(() => import('ant-design-vue/es/switch'));\nconst Textarea = defineAsyncComponent(() =>\n  import('ant-design-vue/es/input').then((res) => res.Textarea),\n);\nconst TimePicker = defineAsyncComponent(\n  () => import('ant-design-vue/es/time-picker'),\n);\nconst TimeRangePicker = defineAsyncComponent(() =>\n  import('ant-design-vue/es/time-picker').then((res) => res.TimeRangePicker),\n);\nconst TreeSelect = defineAsyncComponent(\n  () => import('ant-design-vue/es/tree-select'),\n);\nconst Upload = defineAsyncComponent(() => import('ant-design-vue/es/upload'));\n\nconst withDefaultPlaceholder = <T extends Component>(\n  component: T,\n  type: 'input' | 'select',\n  componentProps: Recordable<any> = {},\n) => {\n  return defineComponent({\n    name: component.name,\n    inheritAttrs: false,\n    setup: (props: any, { attrs, expose, slots }) => {\n      // 改为placeholder 解决在keepalive & 语言切换 & tab切换 显示不变的问题\n      const computedPlaceholder = computed(\n        () =>\n          props?.placeholder ||\n          attrs?.placeholder ||\n          $t(`ui.placeholder.${type}`),\n      );\n\n      // 透传组件暴露的方法\n      const innerRef = ref();\n      expose(\n        new Proxy(\n          {},\n          {\n            get: (_target, key) => innerRef.value?.[key],\n            has: (_target, key) => key in (innerRef.value || {}),\n          },\n        ),\n      );\n      return () =>\n        h(\n          component,\n          {\n            ...componentProps,\n            placeholder: computedPlaceholder.value,\n            ...props,\n            ...attrs,\n            ref: innerRef,\n          },\n          slots,\n        );\n    },\n  });\n};\n\n// 这里需要自行根据业务组件库进行适配，需要用到的组件都需要在这里类型说明\nexport type ComponentType =\n  | 'ApiSelect'\n  | 'ApiTreeSelect'\n  | 'AutoComplete'\n  | 'Cascader'\n  | 'Checkbox'\n  | 'CheckboxGroup'\n  | 'DatePicker'\n  | 'DefaultButton'\n  | 'Divider'\n  | 'FileUpload'\n  | 'FileUploadOld'\n  | 'IconPicker'\n  | 'ImageUpload'\n  | 'ImageUploadOld'\n  | 'Input'\n  | 'InputNumber'\n  | 'InputPassword'\n  | 'Mentions'\n  | 'PrimaryButton'\n  | 'Radio'\n  | 'RadioGroup'\n  | 'RangePicker'\n  | 'Rate'\n  | 'RichTextarea'\n  | 'Select'\n  | 'Space'\n  | 'Switch'\n  | 'Textarea'\n  | 'TimePicker'\n  | 'TimeRangePicker'\n  | 'TreeSelect'\n  | 'Upload'\n  | BaseFormComponentType;\n\nasync function initComponentAdapter() {\n  const components: Partial<Record<ComponentType, Component>> = {\n    // 如果你的组件体积比较大，可以使用异步加载\n    // Button: () =>\n    // import('xxx').then((res) => res.Button),\n    ApiSelect: withDefaultPlaceholder(\n      {\n        ...ApiComponent,\n        name: 'ApiSelect',\n      },\n      'select',\n      {\n        component: Select,\n        loadingSlot: 'suffixIcon',\n        visibleEvent: 'onDropdownVisibleChange',\n        modelPropName: 'value',\n      },\n    ),\n    ApiTreeSelect: withDefaultPlaceholder(\n      {\n        ...ApiComponent,\n        name: 'ApiTreeSelect',\n      },\n      'select',\n      {\n        component: TreeSelect,\n        fieldNames: { label: 'label', value: 'value', children: 'children' },\n        loadingSlot: 'suffixIcon',\n        modelPropName: 'value',\n        optionsPropName: 'treeData',\n        visibleEvent: 'onVisibleChange',\n      },\n    ),\n    AutoComplete,\n    Cascader: withDefaultPlaceholder(Cascader, 'select'),\n    Checkbox,\n    CheckboxGroup,\n    DatePicker,\n    // 自定义默认按钮\n    DefaultButton: (props, { attrs, slots }) => {\n      return h(Button, { ...props, attrs, type: 'default' }, slots);\n    },\n    Divider,\n    IconPicker: withDefaultPlaceholder(IconPicker, 'select', {\n      iconSlot: 'addonAfter',\n      inputComponent: Input,\n      modelValueProp: 'value',\n    }),\n    Input: withDefaultPlaceholder(Input, 'input'),\n    InputNumber: withDefaultPlaceholder(InputNumber, 'input'),\n    InputPassword: withDefaultPlaceholder(InputPassword, 'input'),\n    Mentions: withDefaultPlaceholder(Mentions, 'input'),\n    // 自定义主要按钮\n    PrimaryButton: (props, { attrs, slots }) => {\n      return h(Button, { ...props, attrs, type: 'primary' }, slots);\n    },\n    Radio,\n    RadioGroup,\n    RangePicker,\n    Rate,\n    Select: withDefaultPlaceholder(Select, 'select'),\n    Space,\n    Switch,\n    Textarea: withDefaultPlaceholder(Textarea, 'input'),\n    TimePicker,\n    TimeRangePicker,\n    TreeSelect: withDefaultPlaceholder(TreeSelect, 'select'),\n    Upload,\n    ImageUpload,\n    FileUpload,\n    RichTextarea,\n    ImageUploadOld,\n    FileUploadOld,\n  };\n\n  // 将组件注册到全局共享状态中\n  globalShareState.setComponents(components);\n\n  // 定义全局共享状态中的消息提示\n  globalShareState.defineMessage({\n    // 复制成功消息提示\n    copyPreferencesSuccess: (title, content) => {\n      notification.success({\n        description: content,\n        message: title,\n        placement: 'bottomRight',\n      });\n    },\n  });\n}\n\nexport { initComponentAdapter };\n"
  },
  {
    "path": "apps/web-antd/src/adapter/form.ts",
    "content": "import type {\n  VbenFormSchema as FormSchema,\n  VbenFormProps,\n} from '@vben/common-ui';\n\nimport type { ComponentType } from './component';\n\nimport { setupVbenForm, useVbenForm as useForm, z } from '@vben/common-ui';\nimport { $t } from '@vben/locales';\n\nimport { isArray } from 'lodash-es';\n\nasync function initSetupVbenForm() {\n  setupVbenForm<ComponentType>({\n    config: {\n      // ant design vue组件库默认都是 v-model:value\n      baseModelPropName: 'value',\n\n      // 一些组件是 v-model:checked 或者 v-model:fileList\n      modelPropNameMap: {\n        Checkbox: 'checked',\n        Radio: 'checked',\n        RichTextarea: 'modelValue',\n        Switch: 'checked',\n        Upload: 'fileList',\n      },\n    },\n    defineRules: {\n      // 输入项目必填国际化适配\n      required: (value, _params, ctx) => {\n        if (value === undefined || value === null || value.length === 0) {\n          return $t('ui.formRules.required', [ctx.label]);\n        }\n        return true;\n      },\n      // 选择项目必填国际化适配\n      selectRequired: (value, _params, ctx) => {\n        if (\n          [false, null, undefined].includes(value) ||\n          (isArray(value) && value.length === 0)\n        ) {\n          return $t('ui.formRules.selectRequired', [ctx.label]);\n        }\n        return true;\n      },\n    },\n  });\n}\n\nconst useVbenForm = useForm<ComponentType>;\n\nexport { initSetupVbenForm, useVbenForm, z };\n\nexport type VbenFormSchema = FormSchema<ComponentType>;\nexport type { VbenFormProps };\nexport type FormSchemaGetter = () => VbenFormSchema[];\n"
  },
  {
    "path": "apps/web-antd/src/adapter/vxe-table.ts",
    "content": "import type { VxeGridPropTypes } from '@vben/plugins/vxe-table';\n\nimport { h } from 'vue';\n\nimport { setupVbenVxeTable, useVbenVxeGrid } from '@vben/plugins/vxe-table';\n\nimport { Button, Image } from 'ant-design-vue';\n\nimport { useVbenForm } from './form';\n\nsetupVbenVxeTable({\n  configVxeTable: (vxeUI) => {\n    vxeUI.setConfig({\n      grid: {\n        align: 'center',\n        // https://vxetable.cn/#/component/table/base/border\n        border: 'inner',\n        minHeight: 180,\n        formConfig: {\n          // 全局禁用vxe-table的表单配置，使用formOptions\n          enabled: false,\n        },\n        proxyConfig: {\n          autoLoad: true,\n          response: {\n            result: 'rows',\n            total: 'total',\n            list: 'rows',\n          },\n          showActiveMsg: true,\n          showResponseMsg: false,\n        },\n        // 溢出展示形式\n        showOverflow: true,\n        pagerConfig: {\n          // 默认条数\n          pageSize: 10,\n          // 分页可选条数\n          pageSizes: [10, 20, 30, 40, 50],\n        },\n        rowConfig: {\n          // 鼠标移入行显示 hover 样式\n          isHover: true,\n          // 点击行高亮\n          isCurrent: false,\n        },\n        columnConfig: {\n          // 可拖拽列宽\n          resizable: true,\n        },\n        // 右上角工具栏\n        toolbarConfig: {\n          // 自定义列\n          custom: true,\n          customOptions: {\n            icon: 'vxe-icon-setting',\n          },\n          // 最大化\n          zoom: true,\n          // 刷新\n          refresh: true,\n          refreshOptions: {\n            // 默认为reload 修改为在当前页刷新\n            code: 'query',\n          },\n        },\n        // 圆角按钮\n        round: true,\n        // 表格尺寸\n        size: 'medium',\n        customConfig: {\n          // 表格右上角自定义列配置 是否保存到localStorage\n          // 必须存在id参数才能使用\n          storage: false,\n        },\n      },\n    });\n\n    // 表格配置项可以用 cellRender: { name: 'CellImage' },\n    vxeUI.renderer.add('CellImage', {\n      renderTableDefault(_renderOpts, params) {\n        const { column, row } = params;\n        return h(Image, { src: row[column.field] });\n      },\n    });\n\n    // 表格配置项可以用 cellRender: { name: 'CellLink' },\n    vxeUI.renderer.add('CellLink', {\n      renderTableDefault(renderOpts) {\n        const { props } = renderOpts;\n        return h(\n          Button,\n          { size: 'small', type: 'link' },\n          { default: () => props?.text },\n        );\n      },\n    });\n\n    // 这里可以自行扩展 vxe-table 的全局配置，比如自定义格式化\n    // vxeUI.formats.add\n  },\n  useVbenForm,\n});\n\nexport { useVbenVxeGrid };\n\nexport type * from '@vben/plugins/vxe-table';\n\n/**\n * 判断vxe-table的复选框是否选中\n * @param tableApi api\n * @returns boolean\n */\nexport function vxeCheckboxChecked(\n  tableApi: ReturnType<typeof useVbenVxeGrid>[1],\n) {\n  return tableApi?.grid?.getCheckboxRecords?.()?.length > 0;\n}\n\n/**\n * 通用的 排序参数添加到请求参数中\n * @param params 请求参数\n * @param sortList vxe-table的排序参数\n */\nexport function addSortParams(\n  params: Record<string, any>,\n  sortList: VxeGridPropTypes.ProxyAjaxQuerySortCheckedParams[],\n) {\n  // 这里是排序取消 length为0 就不添加参数了\n  if (sortList.length === 0) {\n    return;\n  }\n  // 支持单/多字段排序\n  const orderByColumn = sortList.map((item) => item.field).join(',');\n  const isAsc = sortList.map((item) => item.order).join(',');\n  params.orderByColumn = orderByColumn;\n  params.isAsc = isAsc;\n}\n"
  },
  {
    "path": "apps/web-antd/src/api/aiflow/API文档.md",
    "content": "# 工作流管理 API 接口文档\n\n## 1. 获取工作流详情\n\n### 基本信息\n\n- **接口URL**: `/workflow/get/{uuid}`\n- **请求方式**: `GET`\n- **接口描述**: 根据工作流UUID获取工作流的完整信息，包括节点、连线、配置等\n- **权限要求**: 需要登录，只能获取自己创建的工作流或公开的工作流\n\n### 请求参数\n\n#### 路径参数\n\n| 参数名 | 类型 | 必填 | 说明 |\n|--------|------|------|------|\n| uuid | String | 是 | 工作流的唯一标识符 |\n\n#### 请求示例\n\n```http\nGET /workflow/get/workflow-1698745623456 HTTP/1.1\nHost: api.example.com\nAuthorization: Bearer {token}\n```\n\n### 响应数据\n\n#### 响应结构\n\n```typescript\n{\n  code: number;           // 状态码，200表示成功\n  msg: string;            // 响应消息\n  data: {\n    uuid: string;         // 工作流UUID\n    title: string;        // 工作流名称\n    remark: string;       // 备注说明\n    isPublic: boolean;    // 是否公开\n    nodes: Array<{        // 节点列表\n      uuid: string;       // 节点UUID\n      title: string;      // 节点名称\n      workflowUuid: string;           // 所属工作流UUID\n      workflowComponentId: number;    // 工作流组件ID\n      wfComponent: {                  // 工作流组件信息\n        name: string;                 // 组件名称（如：Start、Answer等）\n        title: string;                // 组件显示名称\n        remark: string;               // 组件说明\n        isEnable: boolean;            // 是否启用\n      };\n      inputConfig: {                  // 输入配置\n        user_inputs: Array<any>;      // 用户输入配置\n        ref_inputs: Array<any>;       // 引用输入配置\n      };\n      nodeConfig: {                   // 节点配置（根据不同组件类型有不同字段）\n        [key: string]: any;\n      };\n      outputConfig: {                 // 输出配置\n        [key: string]: any;\n      };\n      positionX: number;              // 节点X坐标\n      positionY: number;              // 节点Y坐标\n    }>;\n    edges: Array<{        // 连线列表\n      uuid: string;       // 连线UUID\n      source: string;     // 源节点UUID\n      target: string;     // 目标节点UUID\n      sourceHandle?: string;  // 源节点连接点ID（可选）\n      targetHandle?: string;  // 目标节点连接点ID（可选）\n      label?: string;         // 连线标签（可选）\n    }>;\n    createTime: string;   // 创建时间（ISO 8601格式）\n    updateTime: string;   // 更新时间（ISO 8601格式）\n    createBy: string;     // 创建人\n    updateBy: string;     // 更新人\n  };\n}\n```\n\n#### 响应示例\n\n```json\n{\n  \"code\": 200,\n  \"msg\": \"操作成功\",\n  \"data\": {\n    \"uuid\": \"workflow-1698745623456\",\n    \"title\": \"客服问答工作流\",\n    \"remark\": \"用于处理客户常见问题\",\n    \"isPublic\": false,\n    \"nodes\": [\n      {\n        \"uuid\": \"node-start-1\",\n        \"title\": \"开始\",\n        \"workflowUuid\": \"workflow-1698745623456\",\n        \"workflowComponentId\": 1,\n        \"wfComponent\": {\n          \"name\": \"Start\",\n          \"title\": \"开始\",\n          \"remark\": \"工作流起始节点\",\n          \"isEnable\": true\n        },\n        \"inputConfig\": {\n          \"user_inputs\": [\n            {\n              \"uuid\": \"input-1\",\n              \"name\": \"question\",\n              \"title\": \"用户问题\",\n              \"type\": 1,\n              \"required\": true\n            }\n          ],\n          \"ref_inputs\": []\n        },\n        \"nodeConfig\": {\n          \"prologue\": \"您好，请问有什么可以帮您？\"\n        },\n        \"outputConfig\": {},\n        \"positionX\": 100,\n        \"positionY\": 200\n      },\n      {\n        \"uuid\": \"node-answer-1\",\n        \"title\": \"AI回答\",\n        \"workflowUuid\": \"workflow-1698745623456\",\n        \"workflowComponentId\": 3,\n        \"wfComponent\": {\n          \"name\": \"Answer\",\n          \"title\": \"回答\",\n          \"remark\": \"AI生成回答\",\n          \"isEnable\": true\n        },\n        \"inputConfig\": {\n          \"user_inputs\": [],\n          \"ref_inputs\": [\n            {\n              \"source\": \"node-start-1\",\n              \"field\": \"question\"\n            }\n          ]\n        },\n        \"nodeConfig\": {\n          \"model_name\": \"gpt-4\",\n          \"prompt\": \"请根据用户问题给出专业回答：{{question}}\"\n        },\n        \"outputConfig\": {},\n        \"positionX\": 400,\n        \"positionY\": 200\n      }\n    ],\n    \"edges\": [\n      {\n        \"uuid\": \"edge-1\",\n        \"source\": \"node-start-1\",\n        \"target\": \"node-answer-1\",\n        \"sourceHandle\": \"right\",\n        \"targetHandle\": \"left\"\n      }\n    ],\n    \"createTime\": \"2024-10-30T10:30:00Z\",\n    \"updateTime\": \"2024-10-30T15:45:00Z\",\n    \"createBy\": \"admin\",\n    \"updateBy\": \"admin\"\n  }\n}\n```\n\n#### 错误响应\n\n```json\n{\n  \"code\": 404,\n  \"msg\": \"工作流不存在\",\n  \"data\": null\n}\n```\n\n```json\n{\n  \"code\": 403,\n  \"msg\": \"无权限访问该工作流\",\n  \"data\": null\n}\n```\n\n### 注意事项\n\n1. **权限控制**: \n   - 用户只能获取自己创建的工作流\n   - 公开的工作流所有登录用户都可以获取\n   \n2. **数据完整性**: \n   - 返回的工作流包含完整的节点和连线信息\n   - 节点配置 `nodeConfig` 根据不同组件类型会有不同的字段结构\n   \n3. **坐标系统**: \n   - `positionX` 和 `positionY` 是节点在画布上的位置坐标\n   - 单位为像素（px）\n\n---\n\n## 2. 分页查询工作流列表\n\n### 基本信息\n\n- **接口URL**: `/workflow/page`\n- **请求方式**: `POST`\n- **接口描述**: 分页查询工作流列表，支持按标题搜索和类型过滤\n- **权限要求**: 需要登录\n\n### 请求参数\n\n#### Body参数（JSON格式）\n\n| 参数名 | 类型 | 必填 | 说明 |\n|--------|------|------|------|\n| pageNum | Integer | 是 | 页码，从1开始 |\n| pageSize | Integer | 是 | 每页数量，建议10-100 |\n| title | String | 否 | 工作流名称，支持模糊搜索 |\n| type | String | 否 | 类型筛选：空字符串或不传=全部，\"my\"=我的，\"public\"=公开 |\n\n#### 请求示例\n\n```http\nPOST /workflow/page HTTP/1.1\nHost: api.example.com\nContent-Type: application/json\nAuthorization: Bearer {token}\n\n{\n  \"pageNum\": 1,\n  \"pageSize\": 10,\n  \"title\": \"客服\",\n  \"type\": \"my\"\n}\n```\n\n### 响应数据\n\n#### 响应结构\n\n```typescript\n{\n  code: number;           // 状态码，200表示成功\n  msg: string;            // 响应消息\n  data: {\n    total: number;        // 总记录数\n    rows: Array<{         // 工作流列表\n      uuid: string;       // 工作流UUID\n      title: string;      // 工作流名称\n      remark: string;     // 备注说明\n      isPublic: boolean;  // 是否公开\n      nodes: Array<any>;  // 节点列表（简化版，仅用于统计数量）\n      edges: Array<any>;  // 连线列表（简化版）\n      createTime: string; // 创建时间\n      updateTime: string; // 更新时间\n      createBy: string;   // 创建人\n      updateBy: string;   // 更新人\n    }>;\n  };\n}\n```\n\n#### 响应示例\n\n```json\n{\n  \"code\": 200,\n  \"msg\": \"操作成功\",\n  \"data\": {\n    \"total\": 25,\n    \"rows\": [\n      {\n        \"uuid\": \"workflow-1698745623456\",\n        \"title\": \"客服问答工作流\",\n        \"remark\": \"用于处理客户常见问题\",\n        \"isPublic\": false,\n        \"nodes\": [\n          { \"uuid\": \"node-start-1\" },\n          { \"uuid\": \"node-answer-1\" },\n          { \"uuid\": \"node-end-1\" }\n        ],\n        \"edges\": [\n          { \"uuid\": \"edge-1\" },\n          { \"uuid\": \"edge-2\" }\n        ],\n        \"createTime\": \"2024-10-30T10:30:00Z\",\n        \"updateTime\": \"2024-10-30T15:45:00Z\",\n        \"createBy\": \"admin\",\n        \"updateBy\": \"admin\"\n      },\n      {\n        \"uuid\": \"workflow-1698745623457\",\n        \"title\": \"客服智能路由\",\n        \"remark\": \"根据问题类型自动分配\",\n        \"isPublic\": true,\n        \"nodes\": [\n          { \"uuid\": \"node-start-2\" },\n          { \"uuid\": \"node-classifier-1\" },\n          { \"uuid\": \"node-switcher-1\" }\n        ],\n        \"edges\": [\n          { \"uuid\": \"edge-3\" }\n        ],\n        \"createTime\": \"2024-10-29T14:20:00Z\",\n        \"updateTime\": \"2024-10-29T14:20:00Z\",\n        \"createBy\": \"admin\",\n        \"updateBy\": \"admin\"\n      }\n    ]\n  }\n}\n```\n\n#### 错误响应\n\n```json\n{\n  \"code\": 400,\n  \"msg\": \"参数错误：pageNum必须大于0\",\n  \"data\": null\n}\n```\n\n```json\n{\n  \"code\": 400,\n  \"msg\": \"参数错误：pageSize必须在1-100之间\",\n  \"data\": null\n}\n```\n\n### 注意事项\n\n1. **分页参数**: \n   - `pageNum` 从1开始，不是0\n   - `pageSize` 建议范围 1-100，超出范围应返回400错误\n   \n2. **搜索功能**: \n   - `title` 参数支持模糊搜索（LIKE '%title%'）\n   - 搜索不区分大小写\n   \n3. **类型过滤**: \n   - `type` 为空或不传：返回所有工作流（我的+公开）\n   - `type=\"my\"`：仅返回当前用户创建的工作流\n   - `type=\"public\"`：仅返回公开的工作流\n   \n4. **数据优化**: \n   - 列表接口中的 `nodes` 和 `edges` 为简化版，仅包含基本信息\n   - 前端会根据数组长度统计节点数量，无需额外返回 `nodeCount` 字段\n   \n5. **排序规则**: \n   - 默认按 `updateTime` 降序排列（最新修改的在前）\n   - 可根据需求添加其他排序字段\n\n---"
  },
  {
    "path": "apps/web-antd/src/api/aiflow/adapters.ts",
    "content": "import { requestClient } from '#/api/request'\n\nexport const adapters = {\n  httpGet<T = any>(url: string, config?: any) {\n    return requestClient.get<T>(url, config)\n  },\n\n  httpPost<T = any>(url: string, data?: any, config?: any) {\n    return requestClient.post<T>(url, data, config)\n  },\n\n  httpPut<T = any>(url: string, data?: any, config?: any) {\n    return requestClient.put<T>(url, data, config)\n  },\n\n  httpDelete<T = any>(url: string, config?: any) {\n    return requestClient.delete<T>(url, config)\n  }\n}\n"
  },
  {
    "path": "apps/web-antd/src/api/aiflow/index.ts",
    "content": "import { adapters } from './adapters'\n\nexport const workflowApi = {\n  workflowAdd<T = any>(data: { title: string; remark: string; isPublic: boolean }) {\n    return adapters.httpPost<T>('/workflow/add', data)\n  },\n\n  workflowCopy<T = any>(wfUuid: string) {\n    return adapters.httpPost<T>(`/workflow/copy/${wfUuid}`)\n  },\n\n  workflowUpdate<T = any>(data: Workflow.WorkflowUpdateReq) {\n    return adapters.httpPost<T>('/workflow/update', data)\n  },\n\n  workflowDel<T = any>(uuid: string) {\n    return adapters.httpPost<T>(`/workflow/del/${uuid}`)\n  },\n\n  workflowSetPublic<T = any>(uuid: string, isPublic?: boolean) {\n    return adapters.httpPost<T>(`/workflow/set-public/${uuid}?isPublic=${isPublic}`)\n  },\n\n  workflowBaseInfoUpdate<T = any>(data: { uuid: string; title: string; remark: string; isPublic: boolean }) {\n    return adapters.httpPost<T>('/workflow/base-info/update', data)\n  },\n\n//新增节点\n  addNode<T = any>(data: { name: string; title: string }) {\n    return adapters.httpPost<T>('/admin/workflow/component/addOrUpdate', data)\n  },\n\n// 上传文件\n  uploadFile<T = any>(data: File) {\n    const formData = new FormData();\n    formData.append('files', data);\n    return adapters.httpPost<T>('/resource/oss/fileUpload', formData);\n  },\n  workflowGet<T = any>(uuid: string) {\n    return adapters.httpGet<T>(`/workflow/${uuid}`)\n  },\n\n  workflowPage<T = any>(params: {\n    currentPage: number;\n    pageSize: number;\n    wfSearchReq: {\n      title?: string;\n      isEnable?: boolean;\n      isPublic?: boolean;\n    }\n  }) {\n    const { currentPage, pageSize, wfSearchReq } = params\n    return adapters.httpPost<T>('/admin/workflow/search', wfSearchReq, {\n      params: { currentPage, pageSize }\n    })\n  },\n\n  workflowComponents<T = any>() {\n    return adapters.httpGet<T>('/workflow/public/component/list')\n  },\n\n  workflowSearchMine<T = any>(keyword: string, currentPage: number, pageSize: number) {\n    const search = keyword === undefined ? '' : `keyword=${keyword}&`\n    return adapters.httpGet<T>(`/workflow/mine/search?${search}currentPage=${currentPage}&pageSize=${pageSize}`)\n  },\n\n  workflowSearchPublic<T = any>(keyword: string, currentPage: number, pageSize: number) {\n    const search = keyword === undefined ? '' : `keyword=${keyword}&`\n    return adapters.httpGet<T>(`/workflow/public/search?${search}currentPage=${currentPage}&pageSize=${pageSize}`)\n  },\n\n  workflowRuntimes<T = any>(wfUuid: string, currentPage: number, pageSize: number) {\n    return adapters.httpGet<T>(`/workflow/runtime/page?wfUuid=${wfUuid}&currentPage=${currentPage}&pageSize=${pageSize}`)\n  },\n\n  workflowRuntimeNodes<T = any>(wfRuntimeUuid: string) {\n    return adapters.httpGet<T>(`/workflow/runtime/nodes/${wfRuntimeUuid}`)\n  },\n\n  workflowRuntimesClear<T = any>() {\n    return adapters.httpPost<T>('/workflow/runtime/clear')\n  },\n\n  workflowOperators<T = any>() {\n    return adapters.httpGet<T>('/workflow/public/operators')\n  },\n\n  workflowRuntimeDelete<T = any>(wfRuntimeUuid: string) {\n    return adapters.httpGet<T>(`/workflow/runtime/del/${wfRuntimeUuid}`)\n  },\n\n  workflowRuntimeResume<T = any>(params: {\n    runtimeUuid: string\n    feedbackContent: string\n  }) {\n    return adapters.httpPost<T>(`/workflow/runtime/resume/${params.runtimeUuid}`, { ...params })\n  },\n}\n\nexport default workflowApi\n\n// 运行时能力透出（便于按包内其他接口形式统一从此处导出）\nexport * from './runtime'\n"
  },
  {
    "path": "apps/web-antd/src/api/aiflow/runtime.ts",
    "content": "// 运行时相关接口（提供注入能力，避免与应用层 API 耦合）\nimport { adapters } from './adapters'\nimport { useAccessStore } from '@vben/stores'\n\nexport interface WorkflowRunParams {\n  options: { uuid: string; inputs: any[] }\n  signal?: AbortSignal\n  startCallback?: (wfRuntimeJson: string) => void\n  thinkingDataReceived?: (chunk: string) => void\n  messageReceived?: (chunk: string, event?: string) => void\n  doneCallback?: (finalChunk: string) => void\n  errorCallback?: (error: string) => void\n}\n\nexport interface WorkflowResumeParams {\n  runtimeUuid: string\n  feedbackContent: string\n}\n\nlet workflowRunImpl: ((p: WorkflowRunParams) => Promise<void>) | null = null\nlet workflowResumeImpl: ((p: WorkflowResumeParams) => Promise<void>) | null = null\n\nexport function setWorkflowRunImpl(fn: (p: WorkflowRunParams) => Promise<void>) {\n  workflowRunImpl = fn\n}\n\nexport function setWorkflowResumeImpl(fn: (p: WorkflowResumeParams) => Promise<void>) {\n  workflowResumeImpl = fn\n}\n\nexport async function workflowRun(p: WorkflowRunParams) {\n  if (!workflowRunImpl) {\n    // 默认回退到真实接口（后端改为 body 传 uuid/inputs，URL 不再携带 uuid）\n    return commonSseProcess(`/api/workflow/run`, {\n      options: p.options,\n      signal: p.signal,\n      startCallback: p.startCallback || (() => {}),\n      messageReceived: p.messageReceived || (() => {}),\n      thinkingDataReceived: p.thinkingDataReceived || (() => {}),\n      doneCallback: p.doneCallback || (() => {}),\n      errorCallback: p.errorCallback || (() => {}),\n    })\n  }\n  return workflowRunImpl(p)\n}\n\nexport async function workflowRuntimeResume(p: WorkflowResumeParams) {\n  if (!workflowResumeImpl) {\n    return adapters.httpPost(`/workflow/runtime/resume/${p.runtimeUuid}`, { ...p })\n  }\n  return workflowResumeImpl(p)\n}\n\nlet uploadAction = '/api/file/upload'\nexport function setUploadAction(url: string) { uploadAction = url }\nexport function getUploadAction() { return uploadAction }\n\n// 标准化的 SSE 运行器（不包含任何权限处理）\nexport async function commonSseProcess(\n  url: string,\n  params: {\n    options: any\n    signal?: AbortSignal\n    startCallback: (chunk: string) => void\n    thinkingDataReceived: (chunk: string) => void\n    messageReceived: (chunk: string, eventName: string) => void\n    audioDataReceived?: (chunk: string) => void\n    stateChanged?: (state: string) => void\n    doneCallback: (chunk: string) => void\n    errorCallback: (error: string) => void\n  },\n) {\n  try {\n    // 使用项目的请求客户端来获取正确的配置\n    const accessStore = useAccessStore()\n    const token = accessStore.accessToken ? `Bearer ${accessStore.accessToken}` : ''\n    \n    console.log('SSE 请求配置:', {\n      url,\n      token: token ? '已设置' : '未设置',\n      options: params.options\n    })\n    \n    const res = await fetch(url, {\n      method: 'POST',\n      headers: { \n        'Content-Type': 'application/json',\n        'Authorization': token,\n        'Accept-Language': 'zh_CN',\n        'Content-Language': 'zh_CN',\n        'clientId': 'web'\n      },\n      body: JSON.stringify({ ...params.options }),\n      signal: params.signal,\n    })\n    \n    console.log('SSE 响应状态:', {\n      status: res.status,\n      statusText: res.statusText,\n      contentType: res.headers.get('content-type'),\n      ok: res.ok\n    })\n    \n    const contentType = res.headers.get('content-type') || ''\n    if (!res.ok || !contentType.includes('text/event-stream')) {\n      throw new Error(`SSE open failed: ${res.status} ${res.statusText}`)\n    }\n\n    const reader = res.body?.getReader()\n    if (!reader) { throw new Error('ReadableStream not supported') }\n    const decoder = new TextDecoder('utf-8')\n    let buffer = ''\n\n    while (true) {\n      const { done, value } = await reader.read()\n      if (done) break\n      buffer += decoder.decode(value, { stream: true })\n\n      // 按事件块分割（以空行分隔）\n      const parts = buffer.split('\\n\\n')\n      // 最后一段可能是不完整，保留在 buffer\n      buffer = parts.pop() || ''\n      for (const part of parts) {\n        // 解析 sse 行：event: xxx / data: yyy\n        const lines = part.split('\\n')\n        let eventName = ''\n        const dataLines: string[] = []\n        for (const line of lines) {\n          if (line.startsWith('event:')) eventName = line.slice(6).trim()\n          else if (line.startsWith('data:')) dataLines.push(line.slice(5))\n        }\n        let data = dataLines.join('\\n')\n        if (data.indexOf('-_wrap_-') === 0) data = data.replace('-_wrap_-', '\\n')\n\n        // 分发内置事件\n        if (eventName === '[START]') { params.startCallback(data); continue }\n        if (eventName === '[ERROR]') { params.errorCallback(data); continue }\n        if (eventName === '[DONE]') { params.doneCallback(data); continue }\n        if (eventName === '[AUDIO]') { params.audioDataReceived && params.audioDataReceived(data); continue }\n        if (eventName === '[THINKING]') { params.thinkingDataReceived && params.thinkingDataReceived(data); continue }\n        if (eventName === '[STATE_CHANGED]') { params.stateChanged && params.stateChanged(data); continue }\n        params.messageReceived(data, eventName)\n      }\n    }\n  } catch (e: any) {\n    params.errorCallback(e?.message || String(e))\n    throw e\n  }\n}\n\n// 运行记录相关\nexport function workflowRuntimes<T = any>(wfUuid: string, currentPage: number, pageSize: number) {\n  return adapters.httpGet<T>(`/workflow/runtime/page?wfUuid=${wfUuid}&currentPage=${currentPage}&pageSize=${pageSize}`)\n}\n\nexport function workflowRuntimeNodes<T = any>(wfRuntimeUuid: string) {\n  return adapters.httpGet<T>(`/workflow/runtime/nodes/${wfRuntimeUuid}`)\n}\n\nexport function workflowRuntimesClear<T = any>() {\n  return adapters.httpPost<T>('/workflow/runtime/clear')\n}\n\nexport function workflowRuntimeDelete<T = any>(uuid: string) {\n  return adapters.httpPost<T>(`/workflow/runtime/del/${uuid}`)\n}\n\n"
  },
  {
    "path": "apps/web-antd/src/api/aiflow/types.d.ts",
    "content": "declare namespace Workflow {\n  interface WorkflowUpdateReq {\n    uuid: string\n    title: string\n    remark: string\n    isPublic: boolean\n    nodes: any[]\n    edges: any[]\n  }\n\n  interface WorkflowInfo {\n    uuid: string\n    title: string\n    remark: string\n    isPublic: boolean\n    nodes: WorkflowNode[]\n    edges: WorkflowEdge[]\n  }\n\n  interface WorkflowNode {\n    uuid: string\n    title: string\n    workflowUuid: string\n    wfComponent: WorkflowComponent\n    inputConfig: any\n    nodeConfig: any\n    outputConfig: any\n    positionX: number\n    positionY: number\n  }\n\n  interface WorkflowEdge {\n    id?: string\n    uuid: string\n    workflowUuid: string\n    sourceNodeUuid: string\n    sourceHandle?: string\n    targetNodeUuid: string\n  }\n\n  interface WorkflowComponent {\n    name: string\n    title: string\n    remark?: string\n    isEnable?: boolean\n  }\n\n  interface WorkflowRuntime {\n    uuid: string\n    workflowUuid: string\n    status: string\n    createTime: string\n    updateTime: string\n  }\n\n  interface WorkflowRuntimeNode {\n    uuid: string\n    runtimeUuid: string\n    nodeUuid: string\n    status: string\n    inputData: any\n    outputData: any\n    errorMessage?: string\n  }\n}\n"
  },
  {
    "path": "apps/web-antd/src/api/chat/chatconfig/index.ts",
    "content": "import type { ConfigForm, ConfigQuery, ConfigVO } from './model';\n\nimport type { ID, IDS, PageResult } from '#/api/common';\n\nimport { commonExport } from '#/api/helper';\nimport { requestClient } from '#/api/request';\n\n/**\n * 查询配置信息列表\n * @param params\n * @returns 配置信息列表\n */\nexport function configList(params?: ConfigQuery) {\n  return requestClient.get<PageResult<ConfigVO>>('/system/config/list', {\n    params,\n  });\n}\n\n/**\n * 导出配置信息列表\n * @param params\n * @returns 配置信息列表\n */\nexport function configExport(params?: ConfigQuery) {\n  return commonExport('/system/config/export', params ?? {});\n}\n\n/**\n * 查询配置信息详情\n * @param id id\n * @returns 配置信息详情\n */\nexport function configInfo(id: ID) {\n  return requestClient.get<ConfigVO>(`/system/config/${id}`);\n}\n\n/**\n * 新增配置信息\n * @param data\n * @returns void\n */\nexport function configAdd(data: ConfigForm) {\n  return requestClient.postWithMsg<void>('/system/config', data);\n}\n\n/**\n * 更新配置信息\n * @param data\n * @returns void\n */\nexport function configUpdate(data: ConfigForm) {\n  return requestClient.putWithMsg<void>('/system/config', data);\n}\n\n/**\n * 删除配置信息\n * @param id id\n * @returns void\n */\nexport function configRemove(id: ID | IDS) {\n  return requestClient.deleteWithMsg<void>(`/system/config/${id}`);\n}\n"
  },
  {
    "path": "apps/web-antd/src/api/chat/chatconfig/model.d.ts",
    "content": "import type { BaseEntity, PageQuery } from '#/api/common';\n\nexport interface ConfigVO {\n  /**\n   * 主键\n   */\n  id: number | string;\n\n  /**\n   * 配置类型\n   */\n  category: string;\n\n  /**\n   * 配置名称\n   */\n  configName: string;\n\n  /**\n   * 配置值\n   */\n  configValue: string;\n\n  /**\n   * 说明\n   */\n  configDict: string;\n\n  /**\n   * 备注\n   */\n  remark: string;\n\n  /**\n   * 更新IP\n   */\n  updateIp: string;\n}\n\nexport interface ConfigForm extends BaseEntity {\n  /**\n   * 主键\n   */\n  id?: number | string;\n\n  /**\n   * 配置类型\n   */\n  category?: string;\n\n  /**\n   * 配置名称\n   */\n  configName?: string;\n\n  /**\n   * 配置值\n   */\n  configValue?: string;\n\n  /**\n   * 说明\n   */\n  configDict?: string;\n\n  /**\n   * 备注\n   */\n  remark?: string;\n\n  /**\n   * 更新IP\n   */\n  updateIp?: string;\n}\n\nexport interface ConfigQuery extends PageQuery {\n  /**\n   * 配置类型\n   */\n  category?: string;\n\n  /**\n   * 配置名称\n   */\n  configName?: string;\n\n  /**\n   * 配置值\n   */\n  configValue?: string;\n\n  /**\n   * 说明\n   */\n  configDict?: string;\n\n  /**\n   * 更新IP\n   */\n  updateIp?: string;\n\n  /**\n   * 日期范围参数\n   */\n  params?: any;\n}\n"
  },
  {
    "path": "apps/web-antd/src/api/chat/message/index.ts",
    "content": "import type { MessageForm, MessageQuery, MessageVO } from './model';\n\nimport type { ID, IDS, PageResult } from '#/api/common';\n\nimport { commonExport } from '#/api/helper';\nimport { requestClient } from '#/api/request';\n\n/**\n * 查询聊天消息列表\n * @param params\n * @returns 聊天消息列表\n */\nexport function messageList(params?: MessageQuery) {\n  return requestClient.get<PageResult<MessageVO>>('/system/message/list', {\n    params,\n  });\n}\n\n/**\n * 导出聊天消息列表\n * @param params\n * @returns 聊天消息列表\n */\nexport function messageExport(params?: MessageQuery) {\n  return commonExport('/system/message/export', params ?? {});\n}\n\n/**\n * 查询聊天消息详情\n * @param id id\n * @returns 聊天消息详情\n */\nexport function messageInfo(id: ID) {\n  return requestClient.get<MessageVO>(`/system/message/${id}`);\n}\n\n/**\n * 新增聊天消息\n * @param data\n * @returns void\n */\nexport function messageAdd(data: MessageForm) {\n  return requestClient.postWithMsg<void>('/system/message', data);\n}\n\n/**\n * 更新聊天消息\n * @param data\n * @returns void\n */\nexport function messageUpdate(data: MessageForm) {\n  return requestClient.putWithMsg<void>('/system/message', data);\n}\n\n/**\n * 删除聊天消息\n * @param id id\n * @returns void\n */\nexport function messageRemove(id: ID | IDS) {\n  return requestClient.deleteWithMsg<void>(`/system/message/${id}`);\n}\n"
  },
  {
    "path": "apps/web-antd/src/api/chat/message/model.d.ts",
    "content": "import type { BaseEntity, PageQuery } from '#/api/common';\n\nexport interface MessageVO {\n  /**\n   * 主键\n   */\n  id: number | string;\n\n  /**\n   * 会话id\n   */\n  sessionId: number | string;\n\n  /**\n   * 用户id\n   */\n  userId: number | string;\n\n  /**\n   * 消息内容\n   */\n  content: string;\n\n  /**\n   * 对话角色\n   */\n  role: string;\n\n  /**\n   * 累计 Tokens\n   */\n  totalTokens: number;\n\n  /**\n   * 模型名称\n   */\n  modelName: string;\n\n  /**\n   * 备注\n   */\n  remark: string;\n}\n\nexport interface MessageForm extends BaseEntity {\n  /**\n   * 主键\n   */\n  id?: number | string;\n\n  /**\n   * 会话id\n   */\n  sessionId?: number | string;\n\n  /**\n   * 用户id\n   */\n  userId?: number | string;\n\n  /**\n   * 消息内容\n   */\n  content?: string;\n\n  /**\n   * 对话角色\n   */\n  role?: string;\n\n  /**\n   * 累计 Tokens\n   */\n  totalTokens?: number;\n\n  /**\n   * 模型名称\n   */\n  modelName?: string;\n\n  /**\n   * 备注\n   */\n  remark?: string;\n}\n\nexport interface MessageQuery extends PageQuery {\n  /**\n   * 会话id\n   */\n  sessionId?: number | string;\n\n  /**\n   * 用户id\n   */\n  userId?: number | string;\n\n  /**\n   * 消息内容\n   */\n  content?: string;\n\n  /**\n   * 对话角色\n   */\n  role?: string;\n\n  /**\n   * 累计 Tokens\n   */\n  totalTokens?: number;\n\n  /**\n   * 模型名称\n   */\n  modelName?: string;\n\n  /**\n   * 日期范围参数\n   */\n  params?: any;\n}\n"
  },
  {
    "path": "apps/web-antd/src/api/chat/model/index.ts",
    "content": "import type { ModelForm, ModelQuery, ModelVO } from './model';\n\nimport type { ID, IDS, PageResult } from '#/api/common';\n\nimport { commonExport } from '#/api/helper';\nimport { requestClient } from '#/api/request';\n\n/**\n * 查询模型管理列表\n * @param params\n * @returns 模型管理列表\n */\nexport function modelList(params?: ModelQuery) {\n  return requestClient.get<PageResult<ModelVO>>('/system/model/list', {\n    params,\n  });\n}\n\n/**\n * 查询向量模型列表\n * @returns 向量模型列表\n */\nexport function embeddingModelList() {\n  return modelList({ category: 'vector', pageSize: 1000 });\n}\n\n/**\n * 查询重排序模型列表\n * @returns 重排序模型列表\n */\nexport function rerankModelList() {\n  return modelList({ category: 'rerank', pageSize: 1000 });\n}\n\n/**\n * 导出模型管理列表\n * @param params\n * @returns 模型管理列表\n */\nexport function modelExport(params?: ModelQuery) {\n  return commonExport('/system/model/export', params ?? {});\n}\n\n/**\n * 查询模型管理详情\n * @param id id\n * @returns 模型管理详情\n */\nexport function modelInfo(id: ID) {\n  return requestClient.get<ModelVO>(`/system/model/${id}`);\n}\n\n/**\n * 新增模型管理\n * @param data\n * @returns void\n */\nexport function modelAdd(data: ModelForm) {\n  return requestClient.postWithMsg<void>('/system/model', data);\n}\n\n/**\n * 更新模型管理\n * @param data\n * @returns void\n */\nexport function modelUpdate(data: ModelForm) {\n  return requestClient.putWithMsg<void>('/system/model', data);\n}\n\n/**\n * 删除模型管理\n * @param id id\n * @returns void\n */\nexport function modelRemove(id: ID | IDS) {\n  return requestClient.deleteWithMsg<void>(`/system/model/${id}`);\n}\n"
  },
  {
    "path": "apps/web-antd/src/api/chat/model/model.d.ts",
    "content": "import type { BaseEntity, PageQuery } from '#/api/common';\n\nexport interface ModelVO {\n  /**\n   * 主键\n   */\n  id: number | string;\n\n  /**\n   * 模型分类\n   */\n  category: string;\n\n  /**\n   * 模型名称\n   */\n  modelName: string;\n\n  /**\n   * 模型供应商\n   */\n  providerCode: number | string;\n\n  /**\n   * 模型描述\n   */\n  modelDescribe: string;\n\n  /**\n   * 是否显示\n   */\n  modelShow: string;\n\n  /**\n   * 模型维度\n   */\n  modelDimension: number;\n\n  /**\n   * 请求地址\n   */\n  apiHost: string;\n\n  /**\n   * 密钥\n   */\n  apiKey: string;\n\n  /**\n   * 备注\n   */\n  remark: string;\n}\n\nexport interface ModelForm extends BaseEntity {\n  /**\n   * 主键\n   */\n  id?: number | string;\n\n  /**\n   * 模型分类\n   */\n  category?: string;\n\n  /**\n   * 模型名称\n   */\n  modelName?: string;\n\n  /**\n   * 模型供应商\n   */\n  providerCode?: number | string;\n\n  /**\n   * 模型描述\n   */\n  modelDescribe?: string;\n\n  /**\n   * 是否显示\n   */\n  modelShow?: string;\n\n  /**\n   * 模型维度\n   */\n  modelDimension?: number;\n\n  /**\n   * 请求地址\n   */\n  apiHost?: string;\n\n  /**\n   * 密钥\n   */\n  apiKey?: string;\n\n  /**\n   * 备注\n   */\n  remark?: string;\n}\n\nexport interface ModelQuery extends PageQuery {\n  /**\n   * 模型分类\n   */\n  category?: string;\n\n  /**\n   * 模型名称\n   */\n  modelName?: string;\n\n  /**\n   * 模型供应商\n   */\n  providerCode?: number | string;\n\n  /**\n   * 模型描述\n   */\n  modelDescribe?: string;\n\n  /**\n   * 是否显示\n   */\n  modelShow?: string;\n\n  /**\n   * 模型维度\n   */\n  modelDimension?: number;\n\n  /**\n   * 请求地址\n   */\n  apiHost?: string;\n\n  /**\n   * 密钥\n   */\n  apiKey?: string;\n\n  /**\n   * 日期范围参数\n   */\n  params?: any;\n}\n"
  },
  {
    "path": "apps/web-antd/src/api/chat/provider/index.ts",
    "content": "import type { ProviderForm, ProviderQuery, ProviderVO } from './model';\n\nimport type { ID, IDS, PageResult } from '#/api/common';\n\nimport { commonExport } from '#/api/helper';\nimport { requestClient } from '#/api/request';\n\n/**\n * 查询厂商管理列表\n * @param params\n * @returns 厂商管理列表\n */\nexport function providerList(params?: ProviderQuery) {\n  return requestClient.get<PageResult<ProviderVO>>('/system/provider/list', {\n    params,\n  });\n}\n\n/**\n * 导出厂商管理列表\n * @param params\n * @returns 厂商管理列表\n */\nexport function providerExport(params?: ProviderQuery) {\n  return commonExport('/system/provider/export', params ?? {});\n}\n\n/**\n * 查询厂商管理详情\n * @param id id\n * @returns 厂商管理详情\n */\nexport function providerInfo(id: ID) {\n  return requestClient.get<ProviderVO>(`/system/provider/${id}`);\n}\n\n/**\n * 新增厂商管理\n * @param data\n * @returns void\n */\nexport function providerAdd(data: ProviderForm) {\n  return requestClient.postWithMsg<void>('/system/provider', data);\n}\n\n/**\n * 更新厂商管理\n * @param data\n * @returns void\n */\nexport function providerUpdate(data: ProviderForm) {\n  return requestClient.putWithMsg<void>('/system/provider', data);\n}\n\n/**\n * 删除厂商管理\n * @param id id\n * @returns void\n */\nexport function providerRemove(id: ID | IDS) {\n  return requestClient.deleteWithMsg<void>(`/system/provider/${id}`);\n}\n"
  },
  {
    "path": "apps/web-antd/src/api/chat/provider/model.d.ts",
    "content": "import type { BaseEntity, PageQuery } from '#/api/common';\n\nexport interface ProviderVO {\n  /**\n   * 主键\n   */\n  id: number | string;\n\n  /**\n   * 厂商名称\n   */\n  providerName: number | string;\n\n  /**\n   * 厂商编码\n   */\n  providerCode: number | string;\n\n  /**\n   * 厂商图标\n   */\n  providerIcon: number | string;\n\n  /**\n   * 厂商描述\n   */\n  providerDesc: number | string;\n\n  /**\n   * API地址\n   */\n  apiHost: string;\n\n  /**\n   * 状态（0正常 1停用）\n   */\n  status: string;\n\n  /**\n   * 排序\n   */\n  sortOrder: number;\n\n  /**\n   * 备注\n   */\n  remark: string;\n\n  /**\n   * 更新IP\n   */\n  updateIp: string;\n}\n\nexport interface ProviderForm extends BaseEntity {\n  /**\n   * 主键\n   */\n  id?: number | string;\n\n  /**\n   * 厂商名称\n   */\n  providerName?: number | string;\n\n  /**\n   * 厂商编码\n   */\n  providerCode?: number | string;\n\n  /**\n   * 厂商图标\n   */\n  providerIcon?: number | string;\n\n  /**\n   * 厂商描述\n   */\n  providerDesc?: number | string;\n\n  /**\n   * API地址\n   */\n  apiHost?: string;\n\n  /**\n   * 状态（0正常 1停用）\n   */\n  status?: string;\n\n  /**\n   * 排序\n   */\n  sortOrder?: number;\n\n  /**\n   * 备注\n   */\n  remark?: string;\n\n  /**\n   * 更新IP\n   */\n  updateIp?: string;\n}\n\nexport interface ProviderQuery extends PageQuery {\n  /**\n   * 厂商名称\n   */\n  providerName?: number | string;\n\n  /**\n   * 厂商编码\n   */\n  providerCode?: number | string;\n\n  /**\n   * 厂商图标\n   */\n  providerIcon?: number | string;\n\n  /**\n   * 厂商描述\n   */\n  providerDesc?: number | string;\n\n  /**\n   * API地址\n   */\n  apiHost?: string;\n\n  /**\n   * 状态（0正常 1停用）\n   */\n  status?: string;\n\n  /**\n   * 排序\n   */\n  sortOrder?: number;\n\n  /**\n   * 更新IP\n   */\n  updateIp?: string;\n\n  /**\n   * 日期范围参数\n   */\n  params?: any;\n}\n"
  },
  {
    "path": "apps/web-antd/src/api/common.d.ts",
    "content": "export type ID = number | string;\nexport type IDS = (number | string)[];\n\nexport interface BaseEntity {\n  createBy?: string;\n  createDept?: string;\n  createTime?: string;\n  updateBy?: string;\n  updateTime?: string;\n}\n\n/**\n * 分页信息\n * @param rows 结果集\n * @param total 总数\n */\nexport interface PageResult<T = any> {\n  rows: T[];\n  total: number;\n}\n\n/**\n * 分页查询参数\n *\n * 排序支持的用法如下:\n * {isAsc:\"asc\",orderByColumn:\"id\"} order by id asc\n * {isAsc:\"asc\",orderByColumn:\"id,createTime\"} order by id asc,create_time asc\n * {isAsc:\"desc\",orderByColumn:\"id,createTime\"} order by id desc,create_time desc\n * {isAsc:\"asc,desc\",orderByColumn:\"id,createTime\"} order by id asc,create_time desc\n *\n * @param pageNum 当前页\n * @param pageSize 每页大小\n * @param orderByColumn 排序字段\n * @param isAsc 是否升序\n */\nexport interface PageQuery {\n  isAsc?: string;\n  orderByColumn?: string;\n  pageNum?: number;\n  pageSize?: number;\n  [key: string]: any;\n}\n"
  },
  {
    "path": "apps/web-antd/src/api/core/auth.ts",
    "content": "import type { GrantType } from '@vben/common-ui';\nimport type { HttpResponse } from '@vben/request';\n\nimport { useAppConfig } from '@vben/hooks';\n\nimport { requestClient } from '#/api/request';\n\nconst { clientId, sseEnable } = useAppConfig(\n  import.meta.env,\n  import.meta.env.PROD,\n);\n\nexport namespace AuthApi {\n  /**\n   * @description: 所有登录类型都需要用到的\n   * @param clientId 客户端ID 这里为必填项 但是在loginApi内部处理了 所以为可选\n   * @param grantType 授权/登录类型\n   * @param tenantId 租户id\n   */\n  export interface BaseLoginParams {\n    clientId?: string;\n    grantType: GrantType;\n    tenantId: string;\n  }\n\n  /**\n   * @description: oauth登录需要用到的参数\n   * @param socialCode 第三方参数\n   * @param socialState 第三方参数\n   * @param source 与后端的 justauth.type.xxx的回调地址的source对应\n   */\n  export interface OAuthLoginParams extends BaseLoginParams {\n    socialCode: string;\n    socialState: string;\n    source: string;\n  }\n\n  /**\n   * @description: 验证码登录需要用到的参数\n   * @param code 验证码 可选(未开启验证码情况)\n   * @param uuid 验证码ID 可选(未开启验证码情况)\n   * @param username 用户名\n   * @param password 密码\n   */\n  export interface SimpleLoginParams extends BaseLoginParams {\n    code?: string;\n    uuid?: string;\n    username: string;\n    password: string;\n  }\n\n  export type LoginParams = OAuthLoginParams | SimpleLoginParams;\n\n  // /** 登录接口参数 */\n  // export interface LoginParams {\n  //   code?: string;\n  //   grantType: string;\n  //   password: string;\n  //   tenantId: string;\n  //   username: string;\n  //   uuid?: string;\n  // }\n\n  /** 登录接口返回值 */\n  export interface LoginResult {\n    access_token: string;\n    client_id: string;\n    expire_in: number;\n  }\n\n  export interface RefreshTokenResult {\n    data: string;\n    status: number;\n  }\n}\n\n/**\n * 登录\n */\nexport async function loginApi(data: AuthApi.LoginParams) {\n  return requestClient.post<AuthApi.LoginResult>(\n    '/auth/login',\n    { ...data, clientId },\n    {\n      encrypt: true,\n    },\n  );\n}\n\n/**\n * 用户登出\n * @returns void\n */\nexport function doLogout() {\n  return requestClient.post<HttpResponse<void>>('/auth/logout');\n}\n\n/**\n * 关闭sse连接\n * @returns void\n */\nexport function seeConnectionClose() {\n  /**\n   * 未开启sse 不需要处理\n   */\n  if (!sseEnable) {\n    return;\n  }\n  return requestClient.get<void>('/resource/sse/close');\n}\n\n/**\n * @param companyName 租户/公司名称\n * @param domain 绑定域名(不带http(s)://) 可选\n * @param tenantId 租户id\n */\nexport interface TenantOption {\n  companyName: string;\n  domain?: string;\n  tenantId: string;\n}\n\n/**\n * @param tenantEnabled 是否启用租户\n * @param voList 租户列表\n */\nexport interface TenantResp {\n  tenantEnabled: boolean;\n  voList: TenantOption[];\n}\n\n/**\n * 获取租户列表 下拉框使用\n */\nexport function tenantList() {\n  return requestClient.get<TenantResp>('/auth/tenant/list');\n}\n\n/**\n * vben的 先不删除\n * @returns string[]\n */\nexport async function getAccessCodesApi() {\n  return requestClient.get<string[]>('/auth/codes');\n}\n\n/**\n * 绑定第三方账号\n * @param source 绑定的来源\n * @returns 跳转url\n */\nexport function authBinding(source: string, tenantId: string) {\n  return requestClient.get<string>(`/auth/binding/${source}`, {\n    params: {\n      domain: window.location.host,\n      tenantId,\n    },\n  });\n}\n\n/**\n * 取消绑定\n * @param id id\n */\nexport function authUnbinding(id: string) {\n  return requestClient.deleteWithMsg<void>(`/auth/unlock/${id}`);\n}\n\n/**\n * oauth授权回调\n * @param data oauth授权\n * @returns void\n */\nexport function authCallback(data: AuthApi.OAuthLoginParams) {\n  return requestClient.post<void>('/auth/social/callback', data);\n}\n"
  },
  {
    "path": "apps/web-antd/src/api/core/captcha.ts",
    "content": "import { requestClient } from '#/api/request';\n\n/**\n * 发送短信验证码\n * @param phonenumber 手机号\n * @returns void\n */\nexport function sendSmsCode(phonenumber: string) {\n  return requestClient.get<void>('/resource/sms/code', {\n    params: { phonenumber },\n  });\n}\n\n/**\n * 发送邮件验证码\n * @param email 邮箱\n * @returns void\n */\nexport function sendEmailCode(email: string) {\n  return requestClient.get<void>('/resource/email/code', {\n    params: { email },\n  });\n}\n\n/**\n * @param img 图片验证码 需要和base64拼接\n * @param captchaEnabled 是否开启\n * @param uuid 验证码ID\n */\nexport interface CaptchaResponse {\n  captchaEnabled: boolean;\n  img: string;\n  uuid: string;\n}\n\n/**\n * 图片验证码\n * @returns resp\n */\nexport function captchaImage() {\n  return requestClient.get<CaptchaResponse>('/auth/code');\n}\n"
  },
  {
    "path": "apps/web-antd/src/api/core/index.ts",
    "content": "export * from './auth';\nexport * from './menu';\nexport * from './upload';\nexport * from './user';\n"
  },
  {
    "path": "apps/web-antd/src/api/core/menu.ts",
    "content": "import { requestClient } from '#/api/request';\n\n/**\n * @description: 菜单meta\n * @param title 菜单名\n * @param icon 菜单图标\n * @param noCache 是否不缓存\n * @param link 外链链接\n */\nexport interface MenuMeta {\n  icon: string;\n  link?: string;\n  noCache: boolean;\n  title: string;\n}\n\n/**\n * @description: 菜单\n * @param name 菜单名\n * @param path 菜单路径\n * @param hidden 是否隐藏\n * @param component 组件名称 Layout\n * @param alwaysShow 总是显示\n * @param query 路由参数(json形式)\n * @param meta 路由信息\n * @param children 子路由信息\n */\nexport interface Menu {\n  alwaysShow?: boolean;\n  children: Menu[];\n  component: string;\n  hidden: boolean;\n  meta: MenuMeta;\n  name: string;\n  path: string;\n  query?: string;\n  redirect?: string;\n}\n\n/**\n * 获取用户所有菜单\n */\nexport async function getAllMenusApi() {\n  return requestClient.get<Menu[]>('/system/menu/getRouters');\n}\n"
  },
  {
    "path": "apps/web-antd/src/api/core/upload.ts",
    "content": "import type { AxiosRequestConfig } from '@vben/request';\n\nimport { requestClient } from '#/api/request';\n\n/**\n * Axios上传进度事件\n */\nexport type AxiosProgressEvent = AxiosRequestConfig['onUploadProgress'];\n\n/**\n * 默认上传结果\n */\nexport interface UploadResult {\n  url: string;\n  fileName: string;\n  ossId: string;\n}\n\n/**\n * 通过单文件上传接口\n * @param file 上传的文件\n * @param options 一些配置项\n * @param options.onUploadProgress 上传进度事件\n * @param options.signal 上传取消信号\n * @param options.otherData 其他请求参数 后端拓展可能会用到\n * @returns 上传结果\n */\nexport function uploadApi(\n  file: Blob | File,\n  options?: {\n    onUploadProgress?: AxiosProgressEvent;\n    otherData?: Record<string, any>;\n    signal?: AbortSignal;\n  },\n) {\n  const { onUploadProgress, signal, otherData = {} } = options ?? {};\n  return requestClient.upload<UploadResult>(\n    '/resource/oss/upload',\n    { file, ...otherData },\n    { onUploadProgress, signal, timeout: 60_000 },\n  );\n}\n\n/**\n * 上传api type\n */\nexport type UploadApi = typeof uploadApi;\n"
  },
  {
    "path": "apps/web-antd/src/api/core/user.ts",
    "content": "import { requestClient } from '#/api/request';\n\nexport interface Role {\n  dataScope: string;\n  flag: boolean;\n  roleId: number;\n  roleKey: string;\n  roleName: string;\n  roleSort: number;\n  status: string;\n  superAdmin: boolean;\n}\n\nexport interface User {\n  avatar: string;\n  createTime: string;\n  deptId: number;\n  deptName: string;\n  email: string;\n  loginDate: string;\n  loginIp: string;\n  nickName: string;\n  phonenumber: string;\n  remark: string;\n  roles: Role[];\n  sex: string;\n  status: string;\n  tenantId: string;\n  userId: number;\n  userName: string;\n  userType: string;\n}\n\nexport interface UserInfoResp {\n  permissions: string[];\n  roles: string[];\n  user: User;\n}\n\n/**\n * 获取用户信息\n * 存在返回null的情况(401) 不会抛出异常 需要手动抛异常\n */\nexport async function getUserInfoApi() {\n  return requestClient.get<null | UserInfoResp>('/system/user/getInfo');\n}\n"
  },
  {
    "path": "apps/web-antd/src/api/graph/index.ts",
    "content": "import { requestClient } from '#/api/request';\nimport type {\n  ExtractParams,\n  GraphData,\n  GraphInstance,\n  GraphStats,\n  IngestParams,\n  NeighborQueryParams,\n  PathQueryParams,\n  RetrieveParams,\n  SearchParams,\n  ExtractionResult,\n  GraphRetrievalResult,\n  GraphPath,\n  GraphNode,\n  GraphBuildTask,\n} from './model';\n\n/**\n * 知识图谱API接口\n */\n\n// ==================== 图谱实例管理 ====================\n\n/**\n * 获取图谱实例列表\n */\nexport function graphInstanceList(params?: any) {\n  return requestClient.get<any>('/graph/instance/list', { params });\n}\n\n/**\n * 创建图谱实例\n */\nexport function graphInstanceAdd(data: Partial<GraphInstance>) {\n  return requestClient.post<any>('/graph/instance', data);\n}\n\n/**\n * 更新图谱实例\n */\nexport function graphInstanceUpdate(data: Partial<GraphInstance>) {\n  return requestClient.put<any>('/graph/instance', data);\n}\n\n/**\n * 删除图谱实例\n */\nexport function graphInstanceRemove(id: string | string[]) {\n  if (Array.isArray(id)) {\n    return requestClient.delete<any>('/graph/instance/batch', { data: { ids: id } });\n  }\n  return requestClient.delete<any>(`/graph/instance/${id}`);\n}\n\n/**\n * 获取图谱实例详情\n */\nexport function graphInstanceInfo(id: string) {\n  return requestClient.get<GraphInstance>(`/graph/instance/${id}`);\n}\n\n/**\n * 构建图谱\n */\nexport function graphInstanceBuild(id: string) {\n  return requestClient.post<any>(`/graph/instance/build/${id}`);\n}\n\n/**\n * 重建图谱\n */\nexport function graphInstanceRebuild(id: string) {\n  return requestClient.post<any>(`/graph/instance/rebuild/${id}`);\n}\n\n/**\n * 获取构建状态\n */\nexport function graphInstanceStatus(id: string) {\n  return requestClient.get<any>(`/graph/instance/status/${id}`);\n}\n\n/**\n * 导出图谱实例数据\n */\nexport function graphInstanceExport(params?: any) {\n  return requestClient.post<Blob>('/graph/instance/export', params, {\n    responseType: 'blob',\n  });\n}\n\n// ==================== 图谱查询 ====================\n\n/**\n * 获取知识库的图谱数据\n */\nexport function graphQueryByKnowledge(knowledgeId: string, limit?: number) {\n  return requestClient.get<GraphData>(`/graph/query/knowledge/${knowledgeId}`, {\n    params: { limit },\n  });\n}\n\n/**\n * 搜索实体\n */\nexport function graphSearchEntity(params: SearchParams) {\n  return requestClient.get<GraphNode[]>('/graph/query/search/entity', { params });  // ⭐ 修复路径\n}\n\n/**\n * 获取邻居节点\n */\nexport function graphGetNeighbors(params: NeighborQueryParams) {\n  return requestClient.get<GraphData>('/graph/query/neighbors', { params });\n}\n\n/**\n * 查找路径\n */\nexport function graphFindPath(params: PathQueryParams) {\n  return requestClient.get<GraphPath[]>('/graph/query/path', { params });\n}\n\n/**\n * 获取图谱统计信息\n */\nexport function graphGetStats(knowledgeId: string) {\n  return requestClient.get<GraphStats>(`/graph/query/stats/${knowledgeId}`);\n}\n\n/**\n * 删除图谱数据\n */\nexport function graphDeleteData(knowledgeId: string) {\n  return requestClient.delete<any>(`/graph/query/delete/${knowledgeId}`);\n}\n\n// ==================== 图谱RAG ====================\n\n/**\n * 实体抽取\n */\nexport function graphExtractEntities(data: ExtractParams) {\n  return requestClient.post<ExtractionResult>('/graph/query/extract', data);\n}\n\n/**\n * 文本入库\n */\nexport function graphIngestText(data: IngestParams) {\n  return requestClient.post<ExtractionResult>('/graph/query/ingest', data);\n}\n\n/**\n * 图谱检索\n */\nexport function graphRetrieve(data: RetrieveParams) {\n  return requestClient.post<GraphRetrievalResult>('/graph/query/retrieve', data);\n}\n\n"
  },
  {
    "path": "apps/web-antd/src/api/graph/model.d.ts",
    "content": "/**\n * 知识图谱相关类型定义\n */\n\n// 图谱实例\nexport interface GraphInstance {\n  id?: string;\n  instanceName: string;\n  knowledgeId: string;\n  knowledgeName?: string;\n  modelName?: string;\n  status?: string; // NOT_BUILT, BUILDING, COMPLETED, FAILED\n  nodeCount?: number;\n  edgeCount?: number;\n  entityTypes?: string;\n  relationTypes?: string;\n  createTime?: string;\n  updateTime?: string;\n  remark?: string;\n}\n\n// 图谱构建任务\nexport interface GraphBuildTask {\n  id?: string;\n  instanceId?: string;\n  taskType?: string; // FULL_BUILD, INCREMENTAL_BUILD, DOCUMENT_BUILD\n  status?: string; // PENDING, RUNNING, COMPLETED, FAILED, CANCELLED\n  taskStatus?: number; // 任务状态码（后端枚举值）\n  graphStatus?: number; // 图谱状态码（后端枚举值）\n  progress?: number;\n  totalDocuments?: number;\n  processedDocuments?: number;\n  nodeCount?: number;\n  relationshipCount?: number;\n  errorMessage?: string;\n  startTime?: string;\n  endTime?: string;\n  createTime?: string;\n}\n\n// 图谱节点\nexport interface GraphNode {\n  id?: string;\n  nodeId: string;\n  name: string;\n  label: string; // 实体类型\n  description?: string;\n  confidence?: number;\n  properties?: string; // JSON字符串\n  knowledgeId?: string;\n}\n\n// 图谱边\nexport interface GraphEdge {\n  id?: string;\n  edgeId: string;\n  sourceNodeId: string;\n  targetNodeId: string;\n  label: string; // 关系类型\n  confidence?: number;\n  weight?: number;\n  properties?: string; // JSON字符串\n  knowledgeId?: string;\n}\n\n// 图谱数据\nexport interface GraphData {\n  vertices: GraphNode[];\n  edges: GraphEdge[];\n}\n\n// 图谱统计\nexport interface GraphStats {\n  totalNodes: number;\n  totalEdges: number;\n  entityTypes: Record<string, number>;\n  relationTypes: Record<string, number>;\n}\n\n// 实体抽取结果\nexport interface ExtractedEntity {\n  name: string;\n  type: string;\n  description?: string;\n}\n\nexport interface ExtractedRelation {\n  source: string;\n  target: string;\n  type: string;\n  description?: string;\n}\n\nexport interface ExtractionResult {\n  entities: ExtractedEntity[];\n  relations: ExtractedRelation[];\n}\n\n// 图谱检索结果\nexport interface GraphRetrievalResult {\n  content: string;\n  relevantEntities: GraphNode[];\n  relevantRelations: GraphEdge[];\n}\n\n// 路径查询结果\nexport interface GraphPath {\n  nodes: GraphNode[];\n  edges: GraphEdge[];\n  length: number;\n}\n\n// 邻居查询参数\nexport interface NeighborQueryParams {\n  nodeId: string;\n  knowledgeId: string;\n  depth?: number;\n}\n\n// 路径查询参数\nexport interface PathQueryParams {\n  startNodeId: string;\n  endNodeId: string;\n  knowledgeId: string;\n  maxDepth?: number;\n}\n\n// 搜索参数\nexport interface SearchParams {\n  keyword: string;\n  knowledgeId: string;\n  limit?: number;\n}\n\n// 实体抽取参数\nexport interface ExtractParams {\n  text: string;\n  modelName?: string;\n}\n\n// 文本入库参数\nexport interface IngestParams {\n  text: string;\n  knowledgeId: string;\n  modelName?: string;\n  metadata?: Record<string, any>;\n}\n\n// 检索参数\nexport interface RetrieveParams {\n  query: string;\n  knowledgeId: string;\n  topK?: number;\n}\n\n// 图谱实例表单\nexport interface GraphInstanceForm {\n  id?: string;\n  instanceName: string;\n  knowledgeId: string;\n  modelName?: string;\n  entityTypes?: string[];\n  relationTypes?: string[];\n  remark?: string;\n}\n\n"
  },
  {
    "path": "apps/web-antd/src/api/helper.ts",
    "content": "import { $t } from '@vben/locales';\n\nimport { message, Modal } from 'ant-design-vue';\n\nimport { useAuthStore } from '#/store';\n\nimport { requestClient } from './request';\n\n/**\n * @description:  contentType\n */\nexport const ContentTypeEnum = {\n  // form-data  upload\n  FORM_DATA: 'multipart/form-data;charset=UTF-8',\n  // form-data qs\n  FORM_URLENCODED: 'application/x-www-form-urlencoded;charset=UTF-8',\n  // json\n  JSON: 'application/json;charset=UTF-8',\n} as const;\n\n/**\n * 通用下载接口 封装一层\n * @param url 请求地址\n * @param data  请求参数\n * @returns blob二进制\n */\nexport function commonExport(url: string, data: Record<string, any>) {\n  return requestClient.post<Blob>(url, data, {\n    data,\n    headers: { 'Content-Type': ContentTypeEnum.FORM_URLENCODED },\n    isTransformResponse: false,\n    responseType: 'blob',\n  });\n}\n\n/**\n * 定义一个401专用异常 用于可能会用到的区分场景?\n */\nexport class UnauthorizedException extends Error {}\n\n/**\n * logout这种接口都返回401 抛出这个异常\n */\nexport class ImpossibleReturn401Exception extends Error {}\n\n/**\n * 是否已经处在登出过程中了 一个标志位\n * 主要是防止一个页面会请求多个api 都401 会导致登出执行多次\n */\nlet isLogoutProcessing = false;\n/**\n * 防止 调用logout接口 logout又返回401 然后又走到Logout逻辑死循环\n */\nlet lockLogoutRequest = false;\n\n/**\n * 登出逻辑 两个地方用到 提取出来\n * @throws UnauthorizedException 抛出特定的异常\n */\nexport function handleUnauthorizedLogout() {\n  const timeoutMsg = $t('http.loginTimeout');\n  /**\n   * lock 不再请求logout接口\n   * 这里已经算异常情况了\n   */\n  if (lockLogoutRequest) {\n    throw new UnauthorizedException(timeoutMsg);\n  }\n  // 已经在登出过程中 不再执行\n  if (isLogoutProcessing) {\n    throw new UnauthorizedException(timeoutMsg);\n  }\n  isLogoutProcessing = true;\n  const userStore = useAuthStore();\n  userStore\n    .logout()\n    .catch((error) => {\n      /**\n       * logout接口返回了401\n       * 做Lock处理 且 该标志位不会复位(因为这种场景出现 系统已经算故障了)\n       * 因为这已经不符合正常的逻辑了\n       */\n      if (error instanceof ImpossibleReturn401Exception) {\n        lockLogoutRequest = true;\n        if (import.meta.env.DEV) {\n          Modal.error({\n            title: '提示',\n            centered: true,\n            content:\n              '检测到你的logout接口返回了401, 去检查你的后端配置 这已经不符合正常逻辑(该提示不会在非dev环境弹出)',\n          });\n        }\n      }\n    })\n    .finally(() => {\n      message.error(timeoutMsg);\n      isLogoutProcessing = false;\n    });\n  // 不再执行下面逻辑\n  throw new UnauthorizedException(timeoutMsg);\n}\n"
  },
  {
    "path": "apps/web-antd/src/api/index.ts",
    "content": "export * from './core';\n"
  },
  {
    "path": "apps/web-antd/src/api/knowledge/attach/index.ts",
    "content": "import type { AttachVO, AttachForm, AttachQuery } from './model';\n\nimport type { ID, IDS } from '#/api/common';\nimport type { PageResult } from '#/api/common';\n\nimport { commonExport } from '#/api/helper';\nimport { requestClient } from '#/api/request';\n\n/**\n* 查询知识库附件列表\n* @param params\n* @returns 知识库附件列表\n*/\nexport function attachList(params?: AttachQuery) {\n  return requestClient.get<PageResult<AttachVO>>('/system/attach/list', { params });\n}\n\n/**\n * 导出知识库附件列表\n * @param params\n * @returns 知识库附件列表\n */\nexport function attachExport(params?: AttachQuery) {\n  return commonExport('/system/attach/export', params ?? {});\n}\n\n/**\n * 查询知识库附件详情\n * @param id id\n * @returns 知识库附件详情\n */\nexport function attachInfo(id: ID) {\n  return requestClient.get<AttachVO>(`/system/attach/${id}`);\n}\n\n/**\n * 新增知识库附件\n * @param data\n * @returns void\n */\nexport function attachAdd(data: AttachForm) {\n  return requestClient.postWithMsg<void>('/system/attach', data);\n}\n\n/**\n * 更新知识库附件\n * @param data\n * @returns void\n */\nexport function attachUpdate(data: AttachForm) {\n  return requestClient.putWithMsg<void>('/system/attach', data);\n}\n\n/**\n * 删除知识库附件\n * @param id id\n * @returns void\n */\nexport function attachRemove(id: ID | IDS) {\n  return requestClient.deleteWithMsg<void>(`/system/attach/${id}`);\n}\n\n/**\n * 手动解析附件内容\n * @param id id\n */\nexport function attachParse(id: ID) {\n  return requestClient.postWithMsg<void>(`/system/attach/parse/${id}`);\n}\n"
  },
  {
    "path": "apps/web-antd/src/api/knowledge/attach/model.d.ts",
    "content": "import type { PageQuery, BaseEntity } from '#/api/common';\n\nexport interface AttachVO {\n  /**\n   * \n   */\n  id: string | number;\n\n  /**\n   * 知识库ID\n   */\n  knowledgeId: string | number;\n\n  /**\n   * 附件名称\n   */\n  name: string;\n\n  /**\n   * 附件类型\n   */\n  type: string;\n\n  /**\n   * 对象存储ID\n   */\n  ossId: string | number;\n\n  /**\n   * 文档内容\n   */\n  content: string;\n\n  /**\n   * 备注\n   */\n  remark: string;\n\n  /**\n   * 解析状态\n   */\n  status: number;\n\n}\n\nexport interface AttachForm extends BaseEntity {\n  /**\n   * \n   */\n  id?: string | number;\n\n  /**\n   * 知识库ID\n   */\n  knowledgeId?: string | number;\n\n  /**\n   * 附件名称\n   */\n  name?: string;\n\n  /**\n   * 附件类型\n   */\n  type?: string;\n\n  /**\n   * 对象存储ID\n   */\n  ossId?: string | number;\n\n  /**\n   * 文档内容\n   */\n  content?: string;\n\n  /**\n   * 备注\n   */\n  remark?: string;\n\n}\n\nexport interface AttachQuery extends PageQuery {\n  /**\n   * 知识库ID\n   */\n  knowledgeId?: string | number;\n\n  /**\n   * 附件名称\n   */\n  name?: string;\n\n  /**\n   * 附件类型\n   */\n  type?: string;\n\n  /**\n   * 对象存储ID\n   */\n  ossId?: string | number;\n\n  /**\n   * 文档内容\n   */\n  content?: string;\n\n  /**\n    * 日期范围参数\n    */\n  params?: any;\n}\n"
  },
  {
    "path": "apps/web-antd/src/api/knowledge/fragment/index.ts",
    "content": "import type { FragmentVO, FragmentForm, FragmentQuery } from './model';\n\nimport type { ID, IDS } from '#/api/common';\nimport type { PageResult } from '#/api/common';\n\nimport { commonExport } from '#/api/helper';\nimport { requestClient } from '#/api/request';\n\n/**\n* 查询知识片段列表\n* @param params\n* @returns 知识片段列表\n*/\nexport function fragmentList(params?: FragmentQuery) {\n  return requestClient.get<PageResult<FragmentVO>>('/system/fragment/list', { params });\n}\n\n/**\n * 导出知识片段列表\n * @param params\n * @returns 知识片段列表\n */\nexport function fragmentExport(params?: FragmentQuery) {\n  return commonExport('/system/fragment/export', params ?? {});\n}\n\n/**\n * 查询知识片段详情\n * @param id id\n * @returns 知识片段详情\n */\nexport function fragmentInfo(id: ID) {\n  return requestClient.get<FragmentVO>(`/system/fragment/${id}`);\n}\n\n/**\n * 新增知识片段\n * @param data\n * @returns void\n */\nexport function fragmentAdd(data: FragmentForm) {\n  return requestClient.postWithMsg<void>('/system/fragment', data);\n}\n\n/**\n * 更新知识片段\n * @param data\n * @returns void\n */\nexport function fragmentUpdate(data: FragmentForm) {\n  return requestClient.putWithMsg<void>('/system/fragment', data);\n}\n\n/**\n * 删除知识片段\n * @param id id\n * @returns void\n */\nexport function fragmentRemove(id: ID | IDS) {\n  return requestClient.deleteWithMsg<void>(`/system/fragment/${id}`);\n}\n"
  },
  {
    "path": "apps/web-antd/src/api/knowledge/fragment/model.d.ts",
    "content": "import type { PageQuery, BaseEntity } from '#/api/common';\n\nexport interface FragmentVO {\n  /**\n   * \n   */\n  id: string | number;\n\n  /**\n   * 附件ID\n   */\n  attachId: string | number;\n\n  /**\n   * 文档ID\n   */\n  docId: string;\n\n  /**\n   * 片段索引下标\n   */\n  idx: string | number;\n\n  /**\n   * 文档内容\n   */\n  content: string;\n\n  /**\n   * 备注\n   */\n  remark: string;\n\n}\n\nexport interface FragmentForm extends BaseEntity {\n  /**\n   * \n   */\n  id?: string | number;\n\n  /**\n   * 附件ID\n   */\n  attachId?: string | number;\n\n  /**\n   * 文档ID\n   */\n  docId?: string;\n\n  /**\n   * 片段索引下标\n   */\n  idx?: string | number;\n\n  /**\n   * 文档内容\n   */\n  content?: string;\n\n  /**\n   * 备注\n   */\n  remark?: string;\n\n}\n\nexport interface FragmentQuery extends PageQuery {\n  /**\n   * 附件ID\n   */\n  attachId?: string | number;\n\n  /**\n   * 文档ID\n   */\n  docId?: string;\n\n  /**\n   * 片段索引下标\n   */\n  idx?: string | number;\n\n  /**\n   * 文档内容\n   */\n  content?: string;\n\n  /**\n    * 日期范围参数\n    */\n  params?: any;\n}\n"
  },
  {
    "path": "apps/web-antd/src/api/knowledge/info/index.ts",
    "content": "import type { InfoVO, InfoForm, InfoQuery } from './model';\n\nimport type { ID, IDS } from '#/api/common';\nimport type { PageResult } from '#/api/common';\n\nimport { commonExport } from '#/api/helper';\nimport { requestClient } from '#/api/request';\n\n/**\n* 查询知识库列表\n* @param params\n* @returns 知识库列表\n*/\nexport function infoList(params?: InfoQuery) {\n  return requestClient.get<PageResult<InfoVO>>('/system/info/list', { params });\n}\n\n/**\n * 导出知识库列表\n * @param params\n * @returns 知识库列表\n */\nexport function infoExport(params?: InfoQuery) {\n  return commonExport('/system/info/export', params ?? {});\n}\n\n/**\n * 查询知识库详情\n * @param id id\n * @returns 知识库详情\n */\nexport function infoInfo(id: ID) {\n  return requestClient.get<InfoVO>(`/system/info/${id}`);\n}\n\n/**\n * 新增知识库\n * @param data\n * @returns void\n */\nexport function infoAdd(data: InfoForm) {\n  return requestClient.postWithMsg<void>('/system/info', data);\n}\n\n/**\n * 更新知识库\n * @param data\n * @returns void\n */\nexport function infoUpdate(data: InfoForm) {\n  return requestClient.putWithMsg<void>('/system/info', data);\n}\n\n/**\n * 删除知识库\n * @param id id\n * @returns void\n */\nexport function infoRemove(id: ID | IDS) {\n  return requestClient.deleteWithMsg<void>(`/system/info/${id}`);\n}\n\n/**\n * 检索测试\n * @param data\n * @returns 检索结果\n */\nexport function knowledgeRetrieval(data: any) {\n  return requestClient.post<any[]>('/system/fragment/retrieval', data);\n}\n"
  },
  {
    "path": "apps/web-antd/src/api/knowledge/info/model.d.ts",
    "content": "import type { PageQuery, BaseEntity } from '#/api/common';\n\nexport interface InfoVO {\n  /**\n   * 主键\n   */\n  id: string | number;\n\n  /**\n   * 用户ID\n   */\n  userId: string | number;\n\n  /**\n   * 知识库名称\n   */\n  name: string;\n\n  /**\n   * 是否公开知识库（0 否 1是）\n   */\n  share: number;\n\n  /**\n   * 知识库描述\n   */\n  description: string;\n\n  /**\n   * 知识分隔符\n   */\n  separator: string;\n\n  /**\n   * 重叠字符数\n   */\n  overlapChar: number;\n\n  /**\n   * 知识库中检索的条数\n   */\n  retrieveLimit: number;\n\n  /**\n   * 文本块大小\n   */\n  textBlockSize: number;\n\n  /**\n   * 向量库\n   */\n  vectorModel: string;\n\n  /**\n   * 向量模型\n   */\n  embeddingModel: string;\n\n  /**\n   * 是否启用重排序（0 否 1是）\n   */\n  enableRerank: number;\n\n  /**\n   * 重排序模型名称\n   */\n  rerankModel: string;\n\n  /**\n   * 重排序后返回数量\n   */\n  rerankTopN: number;\n\n  /**\n   * 重排序分数阈值（0-1）\n   */\n  rerankScoreThreshold: number;\n\n  /**\n   * 是否启用混合检索（0 否 1 是）\n   */\n  enableHybrid: number;\n\n  /**\n   * 混合检索权重比例 (0.0-1.0)\n   */\n  hybridAlpha: number;\n\n  /**\n   * 备注\n   */\n  remark: string;\n}\n\nexport interface InfoForm extends BaseEntity {\n  /**\n   * 主键\n   */\n  id?: string | number;\n\n  /**\n   * 用户ID\n   */\n  userId?: string | number;\n\n  /**\n   * 知识库名称\n   */\n  name?: string;\n\n  /**\n   * 是否公开知识库（0 否 1是）\n   */\n  share?: number;\n\n  /**\n   * 知识库描述\n   */\n  description?: string;\n\n  /**\n   * 知识分隔符\n   */\n  separator?: string;\n\n  /**\n   * 重叠字符数\n   */\n  overlapChar?: number;\n\n  /**\n   * 知识库中检索的条数\n   */\n  retrieveLimit?: number;\n\n  /**\n   * 文本块大小\n   */\n  textBlockSize?: number;\n\n  /**\n   * 向量库\n   */\n  vectorModel?: string;\n\n  /**\n   * 向量模型\n   */\n  embeddingModel?: string;\n\n  /**\n   * 是否启用重排序（0 否 1是）\n   */\n  enableRerank?: number;\n\n  /**\n   * 重排序模型名称\n   */\n  rerankModel?: string;\n\n  /**\n   * 重排序后返回数量\n   */\n  rerankTopN?: number;\n\n  /**\n   * 重排序分数阈值（0-1）\n   */\n  rerankScoreThreshold?: number;\n\n  /**\n   * 是否启用混合检索（0 否 1 是）\n   */\n  enableHybrid?: number;\n\n  /**\n   * 混合检索权重比例 (0.0-1.0)\n   */\n  hybridAlpha?: number;\n\n  /**\n   * 备注\n   */\n  remark?: string;\n}\n\nexport interface InfoQuery extends PageQuery {\n  /**\n   * 用户ID\n   */\n  userId?: string | number;\n\n  /**\n   * 知识库名称\n   */\n  name?: string;\n\n  /**\n   * 是否公开知识库（0 否 1是）\n   */\n  share?: number;\n\n  /**\n   * 知识库描述\n   */\n  description?: string;\n\n  /**\n   * 知识分隔符\n   */\n  separator?: string;\n\n  /**\n   * 重叠字符数\n   */\n  overlapChar?: number;\n\n  /**\n   * 知识库中检索的条数\n   */\n  retrieveLimit?: number;\n\n  /**\n   * 文本块大小\n   */\n  textBlockSize?: number;\n\n  /**\n   * 向量库\n   */\n  vectorModel?: string;\n\n  /**\n   * 向量模型\n   */\n  embeddingModel?: string;\n\n  /**\n    * 日期范围参数\n    */\n  params?: any;\n}\n"
  },
  {
    "path": "apps/web-antd/src/api/mcp/market/index.ts",
    "content": "import type { McpMarket, McpMarketRefreshResult, McpMarketTool } from './model';\n\nimport type { ID, IDS, PageQuery, PageResult } from '#/api/common';\n\nimport { commonExport } from '#/api/helper';\nimport { requestClient } from '#/api/request';\n\nenum Api {\n  mcpMarketExport = '/mcp/market/export',\n  mcpMarketList = '/mcp/market/list',\n  mcpMarketAll = '/mcp/market/all',\n  root = '/mcp/market',\n}\n\n/**\n * 查询MCP市场分页列表\n * @param params 请求参数\n * @returns 列表\n */\nexport function mcpMarketList(params?: PageQuery) {\n  return requestClient.get<PageResult<McpMarket>>(Api.mcpMarketList, { params });\n}\n\n/**\n * 查询所有MCP市场（不分页）\n * @returns 列表\n */\nexport function mcpMarketAll() {\n  return requestClient.get<McpMarket[]>(Api.mcpMarketAll);\n}\n\n/**\n * 导出MCP市场excel\n * @param data 请求参数\n */\nexport function mcpMarketExport(data: Partial<McpMarket>) {\n  return commonExport(Api.mcpMarketExport, data);\n}\n\n/**\n * MCP市场详情\n * @param id id\n * @returns 详情\n */\nexport function mcpMarketInfo(id: ID) {\n  return requestClient.get<McpMarket>(`${Api.root}/${id}`);\n}\n\n/**\n * MCP市场新增\n * @param data 参数\n */\nexport function mcpMarketAdd(data: Partial<McpMarket>) {\n  return requestClient.postWithMsg<void>(Api.root, data);\n}\n\n/**\n * MCP市场修改\n * @param data 参数\n */\nexport function mcpMarketUpdate(data: Partial<McpMarket>) {\n  return requestClient.putWithMsg<void>(Api.root, data);\n}\n\n/**\n * MCP市场状态修改\n * @param data 状态\n */\nexport function mcpMarketChangeStatus(data: any) {\n  return requestClient.putWithMsg<void>(\n    `${Api.root}/${data.id}/status?status=${data.status}`,\n  );\n}\n\n/**\n * MCP市场删除\n * @param ids id集合\n */\nexport function mcpMarketRemove(ids: IDS) {\n  return requestClient.deleteWithMsg<void>(`${Api.root}/${ids}`);\n}\n\n/**\n * 刷新市场工具列表\n * @param marketId 市场ID\n */\nexport function mcpMarketRefresh(marketId: ID) {\n  return requestClient.postWithMsg<McpMarketRefreshResult>(`${Api.root}/${marketId}/refresh`);\n}\n\n/**\n * 加载单个工具到本地\n * @param toolId 工具ID\n */\nexport function mcpMarketLoadTool(toolId: ID) {\n  return requestClient.postWithMsg<void>(`${Api.root}/tools/${toolId}/load`);\n}\n\n/**\n * 批量加载工具到本地\n * @param toolIds 工具ID列表\n */\nexport function mcpMarketBatchLoadTools(toolIds: ID[]) {\n  return requestClient.postWithMsg<void>(`${Api.root}/tools/batch-load`, toolIds);\n}\n\n/**\n * 获取市场工具列表\n * @param marketId 市场ID\n */\nexport function mcpMarketToolList(marketId: ID) {\n  return requestClient.get<McpMarketTool[]>(`${Api.root}/${marketId}/tools`);\n}\n"
  },
  {
    "path": "apps/web-antd/src/api/mcp/market/model.d.ts",
    "content": "export interface McpMarket {\n  id: number;\n  name: string;\n  url: string;\n  description: string;\n  authConfig: string;\n  status: string;\n  createTime: string;\n  updateTime: string;\n}\n\nexport interface McpMarketTool {\n  id: number;\n  marketId: number;\n  toolName: string;\n  toolDescription: string;\n  toolVersion: string;\n  toolMetadata: string;\n  isLoaded: boolean;\n  localToolId: number;\n}\n\nexport interface McpMarketRefreshResult {\n  success: boolean;\n  message: string;\n  addedCount: number;\n  updatedCount: number;\n}\n"
  },
  {
    "path": "apps/web-antd/src/api/mcp/tool/index.ts",
    "content": "import type { McpTool, McpToolTestResult } from './model';\n\nimport type { ID, IDS, PageQuery, PageResult } from '#/api/common';\n\nimport { commonExport } from '#/api/helper';\nimport { requestClient } from '#/api/request';\n\nenum Api {\n  mcpToolTest = '/mcp/tool',\n  mcpToolExport = '/mcp/tool/export',\n  mcpToolList = '/mcp/tool/list',\n  mcpToolAll = '/mcp/tool/all',\n  root = '/mcp/tool',\n}\n\n/**\n * 查询MCP工具分页列表\n * @param params 请求参数\n * @returns 列表\n */\nexport function mcpToolList(params?: PageQuery) {\n  return requestClient.get<PageResult<McpTool>>(Api.mcpToolList, { params });\n}\n\n/**\n * 查询所有MCP工具（不分页）\n * @returns 列表\n */\nexport function mcpToolAll() {\n  return requestClient.get<McpTool[]>(Api.mcpToolAll);\n}\n\n/**\n * 导出MCP工具excel\n * @param data 请求参数\n */\nexport function mcpToolExport(data: Partial<McpTool>) {\n  return commonExport(Api.mcpToolExport, data);\n}\n\n/**\n * MCP工具详情\n * @param id id\n * @returns 详情\n */\nexport function mcpToolInfo(id: ID) {\n  return requestClient.get<McpTool>(`${Api.root}/${id}`);\n}\n\n/**\n * MCP工具新增\n * @param data 参数\n */\nexport function mcpToolAdd(data: Partial<McpTool>) {\n  return requestClient.postWithMsg<void>(Api.root, data);\n}\n\n/**\n * MCP工具修改\n * @param data 参数\n */\nexport function mcpToolUpdate(data: Partial<McpTool>) {\n  return requestClient.putWithMsg<void>(Api.root, data);\n}\n\n/**\n * MCP工具状态修改\n * @param data 状态\n */\nexport function mcpToolChangeStatus(data: any) {\n  return requestClient.putWithMsg<void>(\n    `${Api.root}/${data.id}/status?status=${data.status}`,\n  );\n}\n\n/**\n * MCP工具删除\n * @param ids id集合\n */\nexport function mcpToolRemove(ids: IDS) {\n  return requestClient.deleteWithMsg<void>(`${Api.root}/${ids}`);\n}\n\n/**\n * MCP工具测试\n * @param id 工具ID\n */\nexport function mcpToolTest(id: ID) {\n  return requestClient.post<McpToolTestResult>(`${Api.root}/${id}/test`);\n}\n"
  },
  {
    "path": "apps/web-antd/src/api/mcp/tool/model.d.ts",
    "content": "export interface McpTool {\n  id: number;\n  name: string;\n  description: string;\n  type: string;\n  status: string;\n  configJson: string;\n  createTime: string;\n  updateTime: string;\n}\n\nexport interface McpToolTestResult {\n  success: boolean;\n  message: string;\n  data?: any;\n}\n"
  },
  {
    "path": "apps/web-antd/src/api/monitor/cache/index.ts",
    "content": "import { requestClient } from '#/api/request';\n\nexport interface CommandStats {\n  name: string;\n  value: string;\n}\n\nexport interface RedisInfo {\n  [key: string]: string;\n}\n\nexport interface CacheInfo {\n  commandStats: CommandStats[];\n  dbSize: number;\n  info: RedisInfo;\n}\n\n/**\n *\n * @returns redis信息\n */\nexport function redisCacheInfo() {\n  return requestClient.get<CacheInfo>('/monitor/cache');\n}\n"
  },
  {
    "path": "apps/web-antd/src/api/monitor/logininfo/index.ts",
    "content": "import type { LoginLog } from './model';\n\nimport type { IDS, PageQuery, PageResult } from '#/api/common';\n\nimport { commonExport } from '#/api/helper';\nimport { requestClient } from '#/api/request';\n\nenum Api {\n  loginInfoClean = '/monitor/logininfor/clean',\n  loginInfoExport = '/monitor/logininfor/export',\n  loginInfoList = '/monitor/logininfor/list',\n  root = '/monitor/logininfor',\n  userUnlock = '/monitor/logininfor/unlock',\n}\n\n/**\n * 登录日志列表\n * @param params 查询参数\n * @returns list[]\n */\nexport function loginInfoList(params?: PageQuery) {\n  return requestClient.get<PageResult<LoginLog>>(Api.loginInfoList, { params });\n}\n\n/**\n * 导出登录日志\n * @param data 表单参数\n * @returns excel\n */\nexport function loginInfoExport(data: any) {\n  return commonExport(Api.loginInfoExport, data);\n}\n\n/**\n * 移除登录日志\n * @param infoIds 登录日志id数组\n * @returns void\n */\nexport function loginInfoRemove(infoIds: IDS) {\n  return requestClient.deleteWithMsg<void>(`${Api.root}/${infoIds}`);\n}\n\n/**\n * 账号解锁\n * @param username 用户名(账号)\n * @returns void\n */\nexport function userUnlock(username: string) {\n  return requestClient.get<void>(`${Api.userUnlock}/${username}`, {\n    successMessageMode: 'message',\n  });\n}\n\n/**\n * 清空全部登录日志\n * @returns void\n */\nexport function loginInfoClean() {\n  return requestClient.deleteWithMsg<void>(Api.loginInfoClean);\n}\n"
  },
  {
    "path": "apps/web-antd/src/api/monitor/logininfo/model.d.ts",
    "content": "export interface LoginLog {\n  infoId: string;\n  tenantId: string;\n  userName: string;\n  status: string;\n  ipaddr: string;\n  loginLocation: string;\n  browser: string;\n  os: string;\n  msg: string;\n  loginTime: string;\n  clientKey: string;\n}\n"
  },
  {
    "path": "apps/web-antd/src/api/monitor/online/index.ts",
    "content": "import type { OnlineUser } from './model';\n\nimport type { PageQuery, PageResult } from '#/api/common';\n\nimport { requestClient } from '#/api/request';\n\nenum Api {\n  onlineList = '/monitor/online/list',\n  root = '/monitor/online',\n}\n\n/**\n * 当前账号的在线设备 个人中心使用\n * @returns OnlineUser[]\n */\nexport function onlineDeviceList() {\n  return requestClient.get<PageResult<OnlineUser>>(Api.root);\n}\n\n/**\n * 这里的分页参数无效 返回的是全部的分页\n * @param params 请求参数\n * @returns 结果\n */\nexport function onlineList(params?: PageQuery) {\n  return requestClient.get<PageResult<OnlineUser>>(Api.onlineList, { params });\n}\n\n/**\n * 强制下线\n * @param tokenId 用户token\n * @returns void\n */\nexport function forceLogout(tokenId: string) {\n  return requestClient.deleteWithMsg<void>(`${Api.root}/${tokenId}`);\n}\n\n/**\n * 个人中心用的 跟上面的不同是用的Post\n * @param tokenId 用户token\n * @returns void\n */\nexport function forceLogout2(tokenId: string) {\n  return requestClient.deleteWithMsg<void>(`${Api.root}/myself/${tokenId}`);\n}\n"
  },
  {
    "path": "apps/web-antd/src/api/monitor/online/model.d.ts",
    "content": "export interface OnlineUser {\n  tokenId: string;\n  deptName: string;\n  userName: string;\n  ipaddr: string;\n  loginLocation: string;\n  browser: string;\n  os: string;\n  loginTime: number;\n  deviceType: string;\n}\n"
  },
  {
    "path": "apps/web-antd/src/api/monitor/operlog/index.ts",
    "content": "import type { OperationLog } from './model';\n\nimport type { IDS, PageQuery, PageResult } from '#/api/common';\n\nimport { commonExport } from '#/api/helper';\nimport { requestClient } from '#/api/request';\n\nenum Api {\n  operLogClean = '/monitor/operlog/clean',\n  operLogExport = '/monitor/operlog/export',\n  operLogList = '/monitor/operlog/list',\n  root = '/monitor/operlog',\n}\n\n/**\n * 操作日志分页\n * @param params 查询参数\n * @returns 分页结果\n */\nexport function operLogList(params?: PageQuery) {\n  return requestClient.get<PageResult<OperationLog>>(Api.operLogList, {\n    params,\n  });\n}\n\n/**\n * 删除操作日志\n * @param operIds id/ids\n */\nexport function operLogDelete(operIds: IDS) {\n  return requestClient.deleteWithMsg<void>(`${Api.root}/${operIds}`);\n}\n\n/**\n * 清空全部分页日志\n */\nexport function operLogClean() {\n  return requestClient.deleteWithMsg<void>(Api.operLogClean);\n}\n\n/**\n * 导出操作日志\n * @param data 查询参数\n */\nexport function operLogExport(data: Partial<OperationLog>) {\n  return commonExport(Api.operLogExport, data);\n}\n"
  },
  {
    "path": "apps/web-antd/src/api/monitor/operlog/model.d.ts",
    "content": "export interface OperationLog {\n  operId: string;\n  tenantId: string;\n  title: string;\n  businessType: string;\n  businessTypes?: any;\n  method: string;\n  requestMethod: string;\n  operatorType: number;\n  operName: string;\n  deptName: string;\n  operUrl: string;\n  operIp: string;\n  operLocation: string;\n  operParam: string;\n  jsonResult: string;\n  status: string;\n  errorMsg: string;\n  operTime: string;\n  costTime: number;\n}\n"
  },
  {
    "path": "apps/web-antd/src/api/operator/configurationManage/index.ts",
    "content": "import { requestClient } from '#/api/request';\n\nenum Api {\n  addConfig = '/chat/config/saveOrUpdate',\n  listConfig = '/chat/config/list',\n}\n\nexport function listConfig() {\n  return requestClient.get<any>(Api.listConfig);\n}\n\nexport function addConfig(data: any) {\n  return requestClient.post<any>(Api.addConfig, data);\n}\n"
  },
  {
    "path": "apps/web-antd/src/api/request.ts",
    "content": "/**\n * 该文件可自行根据业务逻辑进行调整\n */\n\nimport type { HttpResponse } from '@vben/request';\nimport type {\n  BaseAsymmetricEncryption,\n  BaseSymmetricEncryption,\n} from '@vben/utils';\n\nimport { BUSINESS_SUCCESS_CODE, UNAUTHORIZED_CODE } from '@vben/constants';\nimport { useAppConfig } from '@vben/hooks';\nimport { $t } from '@vben/locales';\nimport { preferences } from '@vben/preferences';\nimport {\n  authenticateResponseInterceptor,\n  errorMessageResponseInterceptor,\n  RequestClient,\n  stringify,\n} from '@vben/request';\nimport { useAccessStore } from '@vben/stores';\nimport {\n  AesEncryption,\n  decodeBase64,\n  encodeBase64,\n  randomStr,\n  RsaEncryption,\n} from '@vben/utils';\n\nimport { message, Modal } from 'ant-design-vue';\nimport { isEmpty, isNull } from 'lodash-es';\n\nimport { useAuthStore } from '#/store';\n\nimport { handleUnauthorizedLogout } from './helper';\n\nconst { apiURL, clientId, enableEncrypt, rsaPublicKey, rsaPrivateKey } =\n  useAppConfig(import.meta.env, import.meta.env.PROD);\n\n/**\n * 使用非对称加密的实现 前端已经实现RSA/SM2\n *\n * 你可以使用Sm2Encryption来替换 后端也需要同步替换公私钥对\n *\n * 后端文件位置: ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/filter/DecryptRequestBodyWrapper.java\n *\n * 注意前端sm-crypto库只能支持04开头的公钥! 否则加密会有问题 你可以使用前端的import { logSm2KeyPair } from '@vben/utils';方法来生成\n * 如果你生成的公钥开头不是04 那么不能正常加密\n * 或者使用这个网站来生成: https://tool.hiofd.com/sm2-key-gen/\n */\nconst asymmetricEncryption: BaseAsymmetricEncryption = new RsaEncryption({\n  publicKey: rsaPublicKey,\n  privateKey: rsaPrivateKey,\n});\n\n/**\n * 对称加密的实现 AES/SM4\n */\nconst symmetricEncryption: BaseSymmetricEncryption = new AesEncryption();\n\nfunction createRequestClient(baseURL: string) {\n  const client = new RequestClient({\n    // 后端地址\n    baseURL,\n    // 消息提示类型\n    errorMessageMode: 'message',\n    // 是否返回原生响应 比如：需要获取响应头时使用该属性\n    isReturnNativeResponse: false,\n    // 需要对返回数据进行处理\n    isTransformResponse: true,\n  });\n\n  /**\n   * 重新认证逻辑\n   */\n  async function doReAuthenticate() {\n    console.warn('Access token or refresh token is invalid or expired. ');\n    const accessStore = useAccessStore();\n    const authStore = useAuthStore();\n    accessStore.setAccessToken(null);\n    if (\n      preferences.app.loginExpiredMode === 'modal' &&\n      accessStore.isAccessChecked\n    ) {\n      accessStore.setLoginExpired(true);\n    } else {\n      await authStore.logout();\n    }\n  }\n\n  /**\n   * 刷新token逻辑\n   */\n  async function doRefreshToken() {\n    // 不需要\n    // 保留此方法只是为了合并方便\n    return '';\n  }\n\n  function formatToken(token: null | string) {\n    return token ? `Bearer ${token}` : null;\n  }\n\n  client.addRequestInterceptor({\n    fulfilled: (config) => {\n      const accessStore = useAccessStore();\n      // 添加token\n      config.headers.Authorization = formatToken(accessStore.accessToken);\n\n      const isFileUpload =\n        config.data instanceof FormData ||\n        config.data instanceof File ||\n        config.data instanceof Blob;\n\n      if (isFileUpload) {\n        delete config.headers['Content-Type'];\n      }\n      /**\n       * locale跟后台不一致 需要转换\n       */\n      const language = preferences.app.locale.replace('-', '_');\n      config.headers['Accept-Language'] = language;\n      config.headers['Content-Language'] = language;\n      /**\n       * 添加全局clientId\n       * 关于header的clientId被错误绑定到实体类\n       * https://gitee.com/dapppp/ruoyi-plus-vben5/issues/IC0BDS\n       */\n      config.headers.ClientID = clientId;\n      /**\n       * 格式化get/delete参数\n       * 如果包含自定义的paramsSerializer则不走此逻辑\n       */\n      if (\n        ['DELETE', 'GET'].includes(config.method?.toUpperCase() || '') &&\n        config.params &&\n        !config.paramsSerializer\n      ) {\n        /**\n         * 1. 格式化参数 微服务在传递区间时间选择(后端的params Map类型参数)需要格式化key 否则接收不到\n         * 2. 数组参数需要格式化 后端才能正常接收 会变成arr=1&arr=2&arr=3的格式来接收\n         */\n        config.paramsSerializer = (params) =>\n          stringify(params, { arrayFormat: 'repeat' });\n      }\n\n      const { encrypt } = config;\n      // 全局开启请求加密功能 && 该请求开启 && 是post/put请求\n      if (\n        !isFileUpload &&\n        enableEncrypt &&\n        encrypt &&\n        ['POST', 'PUT'].includes(config.method?.toUpperCase() || '')\n      ) {\n        // sm4这里改为randomStr(16)\n        const key = randomStr(32);\n        const keyWithBase64 = encodeBase64(key);\n        config.headers['encrypt-key'] =\n          asymmetricEncryption.encrypt(keyWithBase64);\n        /**\n         * axios会默认给字符串前后加上引号 RSA可以正常解密(加不加都能解密) 但是SM2不行(大坑!!!)\n         * 这里通过transformRequest强制返回原始内容\n         */\n        config.transformRequest = (data) => data;\n\n        config.data =\n          typeof config.data === 'object'\n            ? symmetricEncryption.encrypt(JSON.stringify(config.data), key)\n            : symmetricEncryption.encrypt(config.data, key);\n      }\n      return config;\n    },\n  });\n\n  // 通用的错误处理, 如果没有进入上面的错误处理逻辑，就会进入这里\n  // 主要处理http状态码不为200(如网络异常/离线)的情况 必须放在在下面的响应拦截器之前\n  client.addResponseInterceptor(\n    errorMessageResponseInterceptor((msg: string) => message.error(msg)),\n  );\n\n  client.addResponseInterceptor<HttpResponse>({\n    fulfilled: async (response) => {\n      const encryptKey = (response.headers ?? {})['encrypt-key'];\n      if (encryptKey) {\n        /** RSA私钥解密 拿到解密秘钥的base64 */\n        const base64Str = asymmetricEncryption.decrypt(encryptKey);\n        /** base64 解码 得到请求头的 AES 秘钥 */\n        const secret = decodeBase64(base64Str);\n        /** 使用aesKey解密 responseData */\n        const decryptData = symmetricEncryption.decrypt(\n          response.data as unknown as string,\n          secret,\n        );\n        /** 赋值 需要转为对象 */\n        response.data = JSON.parse(decryptData);\n      }\n\n      const { isReturnNativeResponse, isTransformResponse } = response.config;\n      // 是否返回原生响应 比如：需要获取响应时使用该属性\n      if (isReturnNativeResponse) {\n        return response;\n      }\n      // 不进行任何处理，直接返回\n      // 用于页面代码可能需要直接获取code，data，message这些信息时开启\n      if (!isTransformResponse) {\n        /**\n         * @warning 注意 微服务版本在401(网关)会返回text/plain的头 所以这里代码会无效\n         * 我建议你改后端而不是前端来做兼容\n         */\n        // json数据的判断\n        if (response.headers['content-type']?.includes?.('application/json')) {\n          /**\n           * 需要判断是否登录超时/401\n           * 执行登出操作\n           */\n          const resp = response.data as unknown as HttpResponse;\n          // 抛出异常 不再执行\n          if (\n            typeof resp === 'object' &&\n            Reflect.has(resp, 'code') &&\n            resp.code === UNAUTHORIZED_CODE\n          ) {\n            handleUnauthorizedLogout();\n          }\n\n          /**\n           * 需要判断下载二进制的情况 正常是返回二进制 报错会返回json\n           * 当type为blob且content-type为application/json时 则判断已经下载出错\n           */\n          if (response.config.responseType === 'blob') {\n            // 这时候的data为blob类型\n            const blob = response.data as unknown as Blob;\n            // 拿到字符串转json对象\n            response.data = JSON.parse(await blob.text());\n            // 然后按正常逻辑执行下面的代码(判断业务状态码)\n          } else {\n            // 其他类型数据 直接返回\n            return response.data;\n          }\n        } else {\n          // 非json数据 直接返回 不做校验\n          return response.data;\n        }\n      }\n\n      const axiosResponseData = response.data;\n      if (!axiosResponseData) {\n        throw new Error($t('http.apiRequestFailed'));\n      }\n\n      // 后端并没有采用严格的{code, msg, data}模式\n      const { code, data, msg, ...other } = axiosResponseData;\n\n      // 业务状态码为200 则请求成功\n      const hasSuccess =\n        Reflect.has(axiosResponseData, 'code') &&\n        code === BUSINESS_SUCCESS_CODE;\n      if (hasSuccess) {\n        let successMsg = msg;\n\n        if (isNull(successMsg) || isEmpty(successMsg)) {\n          successMsg = $t(`http.operationSuccess`);\n        }\n\n        if (response.config.successMessageMode === 'modal') {\n          Modal.success({\n            content: successMsg,\n            title: $t('http.successTip'),\n          });\n        } else if (response.config.successMessageMode === 'message') {\n          message.success(successMsg);\n        }\n        // 分页情况下为code msg rows total 并没有data字段\n        // 如果有data 直接返回data 没有data将剩余参数(...other)封装为data返回\n        // 需要考虑data为null的情况(比如查询为空) 所以这里直接判断undefined\n        if (data !== undefined) {\n          return data;\n        }\n        // 没有data 将其他参数包装为data\n        return other;\n      }\n      // 在此处根据自己项目的实际情况对不同的code执行不同的操作\n      // 如果不希望中断当前请求，请return数据，否则直接抛出异常即可\n      let timeoutMsg = '';\n      switch (code) {\n        // 登录超时\n        case UNAUTHORIZED_CODE: {\n          handleUnauthorizedLogout();\n          break;\n        }\n        default: {\n          if (msg) {\n            timeoutMsg = msg;\n          }\n        }\n      }\n\n      // errorMessageMode='modal'的时候会显示modal错误弹窗，而不是消息提示，用于一些比较重要的错误\n      // errorMessageMode='none' 一般是调用时明确表示不希望自动弹出错误提示\n      if (response.config.errorMessageMode === 'modal') {\n        Modal.error({\n          content: timeoutMsg,\n          title: $t('http.errorTip'),\n        });\n      } else if (response.config.errorMessageMode === 'message') {\n        message.error(timeoutMsg);\n      }\n\n      throw new Error(timeoutMsg || $t('http.apiRequestFailed'));\n    },\n  });\n\n  // token过期的处理\n  client.addResponseInterceptor(\n    authenticateResponseInterceptor({\n      client,\n      doReAuthenticate,\n      doRefreshToken,\n      enableRefreshToken: preferences.app.enableRefreshToken,\n      formatToken,\n    }),\n  );\n\n  return client;\n}\n\nexport const requestClient = createRequestClient(apiURL);\n\nexport const baseRequestClient = new RequestClient({ baseURL: apiURL });\n"
  },
  {
    "path": "apps/web-antd/src/api/system/client/index.ts",
    "content": "import type { Client } from './model';\n\nimport type { ID, IDS, PageQuery, PageResult } from '#/api/common';\n\nimport { commonExport } from '#/api/helper';\nimport { requestClient } from '#/api/request';\n\nenum Api {\n  clientChangeStatus = '/system/client/changeStatus',\n  clientExport = '/system/client/export',\n  clientList = '/system/client/list',\n  root = '/system/client',\n}\n\n/**\n * 查询客户端分页列表\n * @param params 请求参数\n * @returns 列表\n */\nexport function clientList(params?: PageQuery) {\n  return requestClient.get<PageResult<Client>>(Api.clientList, { params });\n}\n\n/**\n * 导出客户端excel\n * @param data 请求参数\n */\nexport function clientExport(data: Partial<Client>) {\n  return commonExport(Api.clientExport, data);\n}\n\n/**\n * 客户端详情\n * @param id id\n * @returns 详情\n */\nexport function clientInfo(id: ID) {\n  return requestClient.get<Client>(`${Api.root}/${id}`);\n}\n\n/**\n * 客户端新增\n * @param data 参数\n */\nexport function clientAdd(data: Partial<Client>) {\n  return requestClient.postWithMsg<void>(Api.root, data);\n}\n\n/**\n * 客户端修改\n * @param data 参数\n */\nexport function clientUpdate(data: Partial<Client>) {\n  return requestClient.putWithMsg<void>(Api.root, data);\n}\n\n/**\n * 客户端状态修改\n * @param data 状态\n */\nexport function clientChangeStatus(data: any) {\n  const requestData = {\n    clientId: data.clientId,\n    status: data.status,\n  };\n  return requestClient.putWithMsg<void>(Api.clientChangeStatus, requestData);\n}\n\n/**\n * 客户端删除\n * @param ids id集合\n */\nexport function clientRemove(ids: IDS) {\n  return requestClient.deleteWithMsg<void>(`${Api.root}/${ids}`);\n}\n"
  },
  {
    "path": "apps/web-antd/src/api/system/client/model.d.ts",
    "content": "export interface Client {\n  id: number;\n  clientId: string;\n  clientKey: string;\n  clientSecret: string;\n  grantTypeList: string[];\n  grantType: string;\n  deviceType: string;\n  activeTimeout: number;\n  timeout: number;\n  status: string;\n}\n"
  },
  {
    "path": "apps/web-antd/src/api/system/config/index.ts",
    "content": "import type { SysConfig } from './model';\n\nimport type { ID, IDS, PageQuery, PageResult } from '#/api/common';\n\nimport { commonExport } from '#/api/helper';\nimport { requestClient } from '#/api/request';\n\nenum Api {\n  configExport = '/system/config/export',\n  configInfoByKey = '/system/config/configKey',\n  configList = '/system/config/list',\n  configRefreshCache = '/system/config/refreshCache',\n  root = '/system/config',\n}\n\n/**\n * 系统参数分页列表\n * @param params 请求参数\n * @returns 列表\n */\nexport function configList(params?: PageQuery) {\n  return requestClient.get<PageResult<SysConfig>>(Api.configList, { params });\n}\n\nexport function configInfo(configId: ID) {\n  return requestClient.get<SysConfig>(`${Api.root}/${configId}`);\n}\n\n/**\n * 导出\n * @param data 参数\n */\nexport function configExport(data: Partial<SysConfig>) {\n  return commonExport(Api.configExport, data);\n}\n\n/**\n * 刷新缓存\n * @returns void\n */\nexport function configRefreshCache() {\n  return requestClient.deleteWithMsg<void>(Api.configRefreshCache);\n}\n\n/**\n * 更新系统配置\n * @param data 参数\n */\nexport function configUpdate(data: Partial<SysConfig>) {\n  return requestClient.putWithMsg<void>(Api.root, data);\n}\n\n/**\n * 新增系统配置\n * @param data 参数\n */\nexport function configAdd(data: Partial<SysConfig>) {\n  return requestClient.postWithMsg<void>(Api.root, data);\n}\n\n/**\n * 删除配置\n * @param configIds ids\n */\nexport function configRemove(configIds: IDS) {\n  return requestClient.deleteWithMsg<void>(`${Api.root}/${configIds}`);\n}\n\n/**\n * 获取配置信息\n * @param configKey configKey\n * @returns value\n */\nexport function configInfoByKey(configKey: string) {\n  return requestClient.get<string>(`${Api.configInfoByKey}/${configKey}`);\n}\n"
  },
  {
    "path": "apps/web-antd/src/api/system/config/model.d.ts",
    "content": "export interface SysConfig {\n  configId: number;\n  configName: string;\n  configKey: string;\n  configValue: string;\n  configType: string;\n  remark: string;\n  createTime: string;\n}\n"
  },
  {
    "path": "apps/web-antd/src/api/system/dept/index.ts",
    "content": "import type { Dept } from './model';\n\nimport type { ID } from '#/api/common';\n\nimport { requestClient } from '#/api/request';\n\nenum Api {\n  deptList = '/system/dept/list',\n  deptNodeInfo = '/system/dept/list/exclude',\n  root = '/system/dept',\n}\n\n/**\n * 部门列表\n * @returns list\n */\nexport function deptList(params?: { deptName?: string; status?: string }) {\n  return requestClient.get<Dept[]>(Api.deptList, { params });\n}\n\n/**\n * 查询部门列表（排除节点）\n * @param deptId 部门ID\n * @returns void\n */\nexport function deptNodeList(deptId: ID) {\n  return requestClient.get<Dept[]>(`${Api.deptNodeInfo}/${deptId}`);\n}\n\n/**\n * 部门详情\n * @param deptId 部门id\n * @returns 部门信息\n */\nexport function deptInfo(deptId: ID) {\n  return requestClient.get<Dept>(`${Api.root}/${deptId}`);\n}\n\n/**\n * 部门新增\n * @param data 参数\n */\nexport function deptAdd(data: Partial<Dept>) {\n  return requestClient.postWithMsg<void>(Api.root, data);\n}\n\n/**\n * 部门更新\n * @param data 参数\n */\nexport function deptUpdate(data: Partial<Dept>) {\n  return requestClient.putWithMsg<void>(Api.root, data);\n}\n\n/**\n * 注意这里只允许单删除\n * @param deptId ID\n * @returns void\n */\nexport function deptRemove(deptId: ID) {\n  return requestClient.deleteWithMsg<void>(`${Api.root}/${deptId}`);\n}\n"
  },
  {
    "path": "apps/web-antd/src/api/system/dept/model.d.ts",
    "content": "export interface Dept {\n  createBy: string;\n  createTime: string;\n  updateBy?: string;\n  updateTime?: string;\n  remark?: string;\n  deptId: number;\n  parentId: number;\n  ancestors: string;\n  deptName: string;\n  orderNum: number;\n  leader: string;\n  phone: string;\n  email: string;\n  status: string;\n  delFlag: string;\n  parentName?: string;\n  children?: Dept[];\n}\n"
  },
  {
    "path": "apps/web-antd/src/api/system/dict/dict-data-model.d.ts",
    "content": "export interface DictData {\n  createBy: string;\n  createTime: string;\n  cssClass: string;\n  default: boolean;\n  dictCode: number;\n  dictLabel: string;\n  dictSort: number;\n  dictType: string;\n  dictValue: string;\n  isDefault: string;\n  listClass: string;\n  remark: string;\n  status: string;\n  updateBy?: any;\n  updateTime?: any;\n}\n"
  },
  {
    "path": "apps/web-antd/src/api/system/dict/dict-data.ts",
    "content": "import type { DictData } from './dict-data-model';\n\nimport type { ID, IDS, PageQuery } from '#/api/common';\n\nimport { commonExport } from '#/api/helper';\nimport { requestClient } from '#/api/request';\n\nenum Api {\n  dictDataExport = '/system/dict/data/export',\n  dictDataList = '/system/dict/data/list',\n  root = '/system/dict/data',\n}\n\n/**\n * 主要是DictTag组件使用\n * @param dictType 字典类型\n * @returns 字典数据\n */\nexport function dictDataInfo(dictType: string) {\n  return requestClient.get<DictData[]>(`${Api.root}/type/${dictType}`);\n}\n\n/**\n * 字典数据\n * @param params 查询参数\n * @returns 字典数据列表\n */\nexport function dictDataList(params?: PageQuery) {\n  return requestClient.get<DictData[]>(Api.dictDataList, { params });\n}\n\n/**\n * 导出字典数据\n * @param data 表单参数\n * @returns blob\n */\nexport function dictDataExport(data: Partial<DictData>) {\n  return commonExport(Api.dictDataExport, data);\n}\n\n/**\n * 删除\n * @param dictIds 字典ID Array\n * @returns void\n */\nexport function dictDataRemove(dictIds: IDS) {\n  return requestClient.deleteWithMsg<void>(`${Api.root}/${dictIds}`);\n}\n\n/**\n * 新增\n * @param data 表单参数\n * @returns void\n */\nexport function dictDataAdd(data: Partial<DictData>) {\n  return requestClient.postWithMsg<void>(Api.root, data);\n}\n\n/**\n * 修改\n * @param data 表单参数\n * @returns void\n */\nexport function dictDataUpdate(data: Partial<DictData>) {\n  return requestClient.putWithMsg<void>(Api.root, data);\n}\n\n/**\n * 查询字典数据详细\n * @param dictCode 字典编码\n * @returns 字典数据\n */\nexport function dictDetailInfo(dictCode: ID) {\n  return requestClient.get<DictData>(`${Api.root}/${dictCode}`);\n}\n"
  },
  {
    "path": "apps/web-antd/src/api/system/dict/dict-type-model.d.ts",
    "content": "export interface DictType {\n  createTime: string;\n  dictId: number;\n  dictName: string;\n  dictType: string;\n  remark: string;\n  status: string;\n}\n"
  },
  {
    "path": "apps/web-antd/src/api/system/dict/dict-type.ts",
    "content": "import type { DictType } from './dict-type-model';\n\nimport type { ID, IDS, PageQuery, PageResult } from '#/api/common';\n\nimport { commonExport } from '#/api/helper';\nimport { requestClient } from '#/api/request';\n\nenum Api {\n  dictOptionSelectList = '/system/dict/type/optionselect',\n  dictTypeExport = '/system/dict/type/export',\n  dictTypeList = '/system/dict/type/list',\n  dictTypeRefreshCache = '/system/dict/type/refreshCache',\n  root = '/system/dict/type',\n}\n\n/**\n * 获取字典类型列表\n * @param params 请求参数\n * @returns list\n */\nexport function dictTypeList(params?: PageQuery) {\n  return requestClient.get<PageResult<DictType>>(Api.dictTypeList, { params });\n}\n\n/**\n * 导出字典类型列表\n * @param data 表单参数\n * @returns blob\n */\nexport function dictTypeExport(data: Partial<DictType>) {\n  return commonExport(Api.dictTypeExport, data);\n}\n\n/**\n * 删除字典类型\n * @param dictIds 字典类型id数组\n * @returns void\n */\nexport function dictTypeRemove(dictIds: IDS) {\n  return requestClient.deleteWithMsg<void>(`${Api.root}/${dictIds}`);\n}\n\n/**\n * 刷新字典缓存\n * @returns void\n */\nexport function refreshDictTypeCache() {\n  return requestClient.deleteWithMsg<void>(Api.dictTypeRefreshCache);\n}\n\n/**\n * 新增\n * @param data 表单参数\n * @returns void\n */\nexport function dictTypeAdd(data: Partial<DictType>) {\n  return requestClient.postWithMsg<void>(Api.root, data);\n}\n\n/**\n * 修改\n * @param data 表单参数\n * @returns void\n */\nexport function dictTypeUpdate(data: Partial<DictType>) {\n  return requestClient.putWithMsg<void>(Api.root, data);\n}\n\n/**\n * 查询详情\n * @param dictId 字典类型id\n * @returns 信息\n */\nexport function dictTypeInfo(dictId: ID) {\n  return requestClient.get<DictType>(`${Api.root}/${dictId}`);\n}\n\n/**\n * 这个在ele用到 v5用不上\n * 下拉框  返回值和list一样\n * @returns options\n */\nexport function dictOptionSelectList() {\n  return requestClient.get<DictType[]>(Api.dictOptionSelectList);\n}\n"
  },
  {
    "path": "apps/web-antd/src/api/system/dict/index.ts",
    "content": "import type { DictData } from './dict-data-model';\nimport type { DictType } from './dict-type-model';\nimport { dictDataInfo } from './dict-data';\n\nexport type { DictData, DictType };\n\nexport {\n  dictDataInfo,\n  dictDataList,\n  dictDataExport,\n  dictDataRemove,\n  dictDataAdd,\n  dictDataUpdate,\n  dictDetailInfo,\n} from './dict-data';\n\nexport {\n  dictTypeList,\n  dictTypeExport,\n  dictTypeRemove,\n  refreshDictTypeCache,\n  dictTypeAdd,\n  dictTypeUpdate,\n  dictTypeInfo,\n  dictOptionSelectList,\n} from './dict-type';\n\n/**\n * 获取字典数据项 (dictDataInfo的别名)\n * @param dictType 字典类型\n * @returns 字典数据\n */\nexport function getDictItems(dictType: string) {\n  return dictDataInfo(dictType);\n}"
  },
  {
    "path": "apps/web-antd/src/api/system/menu/index.ts",
    "content": "import type { Menu, MenuOption, MenuQuery, MenuResp } from './model';\n\nimport type { ID, IDS } from '#/api/common';\n\nimport { requestClient } from '#/api/request';\n\nenum Api {\n  menuList = '/system/menu/list',\n  menuTreeSelect = '/system/menu/treeselect',\n  roleMenuTree = '/system/menu/roleMenuTreeselect',\n  root = '/system/menu',\n  tenantPackageMenuTreeselect = '/system/menu/tenantPackageMenuTreeselect',\n}\n\n/**\n * 菜单列表\n * @param params 参数\n * @returns 列表\n */\nexport function menuList(params?: MenuQuery) {\n  return requestClient.get<Menu[]>(Api.menuList, { params });\n}\n\n/**\n * 菜单详情\n * @param menuId 菜单id\n * @returns 菜单详情\n */\nexport function menuInfo(menuId: ID) {\n  return requestClient.get<Menu>(`${Api.root}/${menuId}`);\n}\n\n/**\n * 菜单新增\n * @param data 参数\n */\nexport function menuAdd(data: Partial<Menu>) {\n  return requestClient.postWithMsg<void>(Api.root, data);\n}\n\n/**\n * 菜单更新\n * @param data 参数\n */\nexport function menuUpdate(data: Partial<Menu>) {\n  return requestClient.putWithMsg<void>(Api.root, data);\n}\n\n/**\n * 菜单删除\n * @param menuIds ids\n */\nexport function menuRemove(menuIds: IDS) {\n  return requestClient.deleteWithMsg<void>(`${Api.root}/${menuIds}`);\n}\n\n/**\n * 返回对应角色的菜单\n * @param roleId id\n * @returns resp\n */\nexport function roleMenuTreeSelect(roleId: ID) {\n  return requestClient.get<MenuResp>(`${Api.roleMenuTree}/${roleId}`);\n}\n\n/**\n * 下拉框使用  返回所有的菜单\n * @returns []\n */\nexport function menuTreeSelect() {\n  return requestClient.get<MenuOption[]>(Api.menuTreeSelect);\n}\n\n/**\n * 租户套餐使用\n * @param packageId packageId\n * @returns resp\n */\nexport function tenantPackageMenuTreeSelect(packageId: ID) {\n  return requestClient.get<MenuResp>(\n    `${Api.tenantPackageMenuTreeselect}/${packageId}`,\n  );\n}\n\n/**\n * 批量删除菜单\n * @param menuIds 菜单ids\n * @returns void\n */\nexport function menuCascadeRemove(menuIds: IDS) {\n  return requestClient.deleteWithMsg<void>(`${Api.root}/cascade/${menuIds}`);\n}\n"
  },
  {
    "path": "apps/web-antd/src/api/system/menu/model.d.ts",
    "content": "export interface Menu {\n  createBy?: any;\n  createTime: string;\n  updateBy?: any;\n  updateTime?: any;\n  remark?: any;\n  menuId: number;\n  menuName: string;\n  parentName?: string;\n  parentId: number;\n  orderNum: number;\n  path: string;\n  component?: string;\n  query: string;\n  isFrame: string;\n  isCache: string;\n  menuType: string;\n  visible: string;\n  status: string;\n  perms: string;\n  icon: string;\n  children: Menu[];\n}\n\n/**\n * @description 菜单信息\n * @param label 菜单名称\n */\nexport interface MenuOption {\n  id: number;\n  parentId: number;\n  label: string;\n  weight: number;\n  children: MenuOption[];\n  key: string; // 实际上不存在 ide报错\n  menuType: string;\n  icon: string;\n}\n\n/**\n * @description 菜单返回\n * @param checkedKeys 选中的菜单id\n * @param menus 菜单信息\n */\nexport interface MenuResp {\n  checkedKeys: number[];\n  menus: MenuOption[];\n}\n\n/**\n * 菜单表单查询\n */\nexport interface MenuQuery {\n  menuName?: string;\n  visible?: string;\n  status?: string;\n}\n"
  },
  {
    "path": "apps/web-antd/src/api/system/notice/index.ts",
    "content": "import type { Notice } from './model';\n\nimport type { ID, IDS, PageQuery } from '#/api/common';\n\nimport { requestClient } from '#/api/request';\n\nenum Api {\n  noticeList = '/system/notice/list',\n  root = '/system/notice',\n}\n\n/**\n * 通知公告分页\n * @param params 分页参数\n * @returns 分页结果\n */\nexport function noticeList(params?: PageQuery) {\n  return requestClient.get<Notice[]>(Api.noticeList, { params });\n}\n\n/**\n * 通知公告详情\n * @param noticeId id\n * @returns 详情\n */\nexport function noticeInfo(noticeId: ID) {\n  return requestClient.get<Notice>(`${Api.root}/${noticeId}`);\n}\n\n/**\n * 通知公告新增\n * @param data 参数\n */\nexport function noticeAdd(data: Partial<Notice>) {\n  return requestClient.postWithMsg<void>(Api.root, data);\n}\n\n/**\n * 通知公告更新\n * @param data 参数\n */\nexport function noticeUpdate(data: any) {\n  return requestClient.putWithMsg<void>(Api.root, data);\n}\n\n/**\n * 通知公告删除\n * @param noticeIds ids\n */\nexport function noticeRemove(noticeIds: IDS) {\n  return requestClient.deleteWithMsg<void>(`${Api.root}/${noticeIds}`);\n}\n"
  },
  {
    "path": "apps/web-antd/src/api/system/notice/model.d.ts",
    "content": "export interface Notice {\n  noticeId: number;\n  noticeTitle: string;\n  noticeType: string;\n  noticeContent: string;\n  status: string;\n  remark: string;\n  createBy: number;\n  createByName: string;\n  createTime: string;\n}\n"
  },
  {
    "path": "apps/web-antd/src/api/system/oss/index.ts",
    "content": "import type { AxiosRequestConfig } from '@vben/request';\n\nimport type { OssFile } from './model';\n\nimport type { ID, IDS, PageQuery, PageResult } from '#/api/common';\n\nimport { ContentTypeEnum } from '#/api/helper';\nimport { requestClient } from '#/api/request';\n\nenum Api {\n  ossDownload = '/resource/oss/download',\n  ossInfo = '/resource/oss/listByIds',\n  ossList = '/resource/oss/list',\n  ossUpload = '/resource/oss/upload',\n  root = '/resource/oss',\n}\n\n/**\n * 文件list\n * @param params 参数\n * @returns 分页\n */\nexport function ossList(params?: PageQuery) {\n  return requestClient.get<PageResult<OssFile>>(Api.ossList, { params });\n}\n\n/**\n * 查询文件信息 返回为数组\n * @param ossIds id数组\n * @returns 信息数组\n */\nexport function ossInfo(ossIds: ID | IDS) {\n  return requestClient.get<OssFile[]>(`${Api.ossInfo}/${ossIds}`);\n}\n\n/**\n * @deprecated 使用apps/web-antd/src/api/core/upload.ts uploadApi方法\n * @param file 文件\n * @returns void\n */\nexport function ossUpload(file: Blob | File) {\n  const formData = new FormData();\n  formData.append('file', file);\n  return requestClient.postWithMsg(Api.ossUpload, formData, {\n    headers: { 'Content-Type': ContentTypeEnum.FORM_DATA },\n    timeout: 30 * 1000,\n  });\n}\n\n/**\n * 下载文件  返回为二进制\n * @param ossId ossId\n * @param onDownloadProgress 下载进度(可选)\n * @returns blob\n */\nexport function ossDownload(\n  ossId: ID,\n  onDownloadProgress?: AxiosRequestConfig['onDownloadProgress'],\n) {\n  return requestClient.get<Blob>(`${Api.ossDownload}/${ossId}`, {\n    responseType: 'blob',\n    timeout: 30 * 1000,\n    isTransformResponse: false,\n    onDownloadProgress,\n  });\n}\n\n/**\n * 在使用浏览器原生下载前检测是否登录\n * 这里的方案为请求一次接口 如果登录超时会走到response的401逻辑\n * 如果没有listByIds的权限 也不会弹出无权限提示\n * 仅仅是为了检测token是否有效使用\n *\n * @returns void\n */\nexport function checkLoginBeforeDownload() {\n  return requestClient.get<OssFile[]>(`${Api.ossInfo}/1`, {\n    errorMessageMode: 'none',\n  });\n}\n\n/**\n * 删除文件\n * @param ossIds id数组\n * @returns void\n */\nexport function ossRemove(ossIds: IDS) {\n  return requestClient.deleteWithMsg<void>(`${Api.root}/${ossIds}`);\n}\n"
  },
  {
    "path": "apps/web-antd/src/api/system/oss/model.d.ts",
    "content": "export interface OssFile {\n  ossId: string;\n  fileName: string;\n  originalName: string;\n  fileSuffix: string;\n  url: string;\n  createTime: string;\n  createBy: number;\n  createByName: string;\n  service: string;\n}\n\nexport interface OssConfig {\n  ossConfigId: number;\n  configKey: string;\n  accessKey: string;\n  secretKey: string;\n  bucketName: string;\n  prefix: string;\n  endpoint: string;\n  domain: string;\n  isHttps: string;\n  region: string;\n  status: string;\n  ext1: string;\n  remark: string;\n  accessPolicy: string;\n}\n"
  },
  {
    "path": "apps/web-antd/src/api/system/oss-config/index.ts",
    "content": "import type { OssConfig } from './model';\n\nimport type { ID, IDS, PageQuery } from '#/api/common';\n\nimport { requestClient } from '#/api/request';\n\nenum Api {\n  ossConfigChangeStatus = '/resource/oss/config/changeStatus',\n  ossConfigList = '/resource/oss/config/list',\n  root = '/resource/oss/config',\n}\n\n// 获取OSS配置列表\nexport function ossConfigList(params?: PageQuery) {\n  return requestClient.get<OssConfig[]>(Api.ossConfigList, { params });\n}\n\n// 获取OSS配置的信息\nexport function ossConfigInfo(ossConfigId: ID) {\n  return requestClient.get<OssConfig>(`${Api.root}/${ossConfigId}`);\n}\n\n// 添加新的OSS配置\nexport function ossConfigAdd(data: Partial<OssConfig>) {\n  return requestClient.postWithMsg<void>(Api.root, data);\n}\n\n// 更新现有的OSS配置\nexport function ossConfigUpdate(data: Partial<OssConfig>) {\n  return requestClient.putWithMsg<void>(Api.root, data);\n}\n\n// 删除OSS配置\nexport function ossConfigRemove(ossConfigIds: IDS) {\n  return requestClient.deleteWithMsg<void>(`${Api.root}/${ossConfigIds}`);\n}\n\n// 更改OSS配置的状态\nexport function ossConfigChangeStatus(data: any) {\n  const requestData: Partial<OssConfig> = {\n    ossConfigId: data.ossConfigId,\n    status: data.status,\n    configKey: data.configKey,\n  };\n  return requestClient.putWithMsg(Api.ossConfigChangeStatus, requestData);\n}\n"
  },
  {
    "path": "apps/web-antd/src/api/system/oss-config/model.d.ts",
    "content": "export interface OssConfig {\n  ossConfigId: number;\n  configKey: string;\n  accessKey: string;\n  secretKey: string;\n  bucketName: string;\n  prefix: string;\n  endpoint: string;\n  domain: string;\n  isHttps: string;\n  region: string;\n  status: string;\n  ext1: string;\n  remark: string;\n  accessPolicy: string;\n}\n"
  },
  {
    "path": "apps/web-antd/src/api/system/post/index.ts",
    "content": "import type { DeptTree } from '../user/model';\nimport type { Post } from './model';\n\nimport type { ID, IDS, PageQuery } from '#/api/common';\n\nimport { commonExport } from '#/api/helper';\nimport { requestClient } from '#/api/request';\n\nenum Api {\n  postExport = '/system/post/export',\n  postList = '/system/post/list',\n  postSelect = '/system/post/optionselect',\n  root = '/system/post',\n}\n\n/**\n * 获取岗位列表\n * @param params 参数\n * @returns Post[]\n */\nexport function postList(params?: PageQuery) {\n  return requestClient.get<Post[]>(Api.postList, { params });\n}\n\n/**\n * 导出岗位信息\n * @param data 请求参数\n * @returns blob\n */\nexport function postExport(data: Partial<Post>) {\n  return commonExport(Api.postExport, data);\n}\n\n/**\n * 查询岗位信息\n * @param postId id\n * @returns 岗位信息\n */\nexport function postInfo(postId: ID) {\n  return requestClient.get<Post>(`${Api.root}/${postId}`);\n}\n\n/**\n * 岗位新增\n * @param data 参数\n * @returns void\n */\nexport function postAdd(data: Partial<Post>) {\n  return requestClient.postWithMsg<void>(Api.root, data);\n}\n\n/**\n * 岗位更新\n * @param data 参数\n * @returns void\n */\nexport function postUpdate(data: Partial<Post>) {\n  return requestClient.putWithMsg<void>(Api.root, data);\n}\n\n/**\n * 岗位删除\n * @param postIds ids\n * @returns void\n */\nexport function postRemove(postIds: IDS) {\n  return requestClient.deleteWithMsg<void>(`${Api.root}/${postIds}`);\n}\n\n/**\n * 根据部门id获取岗位下拉列表\n * @param deptId 部门id\n * @returns 岗位\n */\nexport function postOptionSelect(deptId: ID) {\n  return requestClient.get<Post[]>(Api.postSelect, { params: { deptId } });\n}\n\n/**\n * 岗位专用 - 获取部门树\n * @returns 部门树\n */\nexport function postDeptTreeSelect() {\n  return requestClient.get<DeptTree[]>('/system/post/deptTree');\n}\n"
  },
  {
    "path": "apps/web-antd/src/api/system/post/model.d.ts",
    "content": "/**\n * @description: Post interface\n */\nexport interface Post {\n  postId: number;\n  postCode: string;\n  postName: string;\n  postSort: number;\n  status: string;\n  remark: string;\n  createTime: string;\n}\n"
  },
  {
    "path": "apps/web-antd/src/api/system/profile/index.ts",
    "content": "import type { FileCallBack, UpdatePasswordParam, UserProfile } from './model';\n\nimport { buildUUID } from '@vben/utils';\n\nimport { requestClient } from '#/api/request';\n\nenum Api {\n  root = '/system/user/profile',\n  updateAvatar = '/system/user/profile/avatar',\n  updatePassword = '/system/user/profile/updatePwd',\n}\n\n/**\n * 用户个人主页信息\n * @returns userInformation\n */\nexport function userProfile() {\n  return requestClient.get<UserProfile>(Api.root);\n}\n\n/**\n * 更新用户个人主页信息\n * @param data\n * @returns void\n */\nexport function userProfileUpdate(data: any) {\n  return requestClient.putWithMsg<void>(Api.root, data);\n}\n\n/**\n * 用户修改密码 (需要加密)\n * @param data\n * @returns void\n */\nexport function userUpdatePassword(data: UpdatePasswordParam) {\n  return requestClient.putWithMsg<void>(Api.updatePassword, data, {\n    encrypt: true,\n  });\n}\n\n/**\n * 用户更新个人头像\n * @param fileCallback data\n * @returns void\n */\nexport function userUpdateAvatar(fileCallback: FileCallBack) {\n  /** 直接点击头像上传 filename为空 由于后台通过拓展名判断(默认文件名blob) 会上传失败 */\n  let { file } = fileCallback;\n  const { filename } = fileCallback;\n  /**\n   * Blob转File类型\n   * 1. 在直接点击确认 filename为空 取uuid作为文件名\n   * 2. 选择上传必须转为File类型 Blob类型上传后台获取文件名为空\n   */\n  file = filename\n    ? new File([file], filename)\n    : new File([file], `${buildUUID()}.png`);\n  return requestClient.post(\n    Api.updateAvatar,\n    {\n      avatarfile: file,\n    },\n    { headers: { 'Content-Type': 'multipart/form-data' } },\n  );\n}\n"
  },
  {
    "path": "apps/web-antd/src/api/system/profile/model.d.ts",
    "content": "export interface Dept {\n  deptId: number;\n  parentId: number;\n  parentName?: any;\n  ancestors: string;\n  deptName: string;\n  orderNum: number;\n  leader: string;\n  phone?: any;\n  email: string;\n  status: string;\n  createTime?: any;\n}\n\nexport interface Role {\n  roleId: number;\n  roleName: string;\n  roleKey: string;\n  roleSort: number;\n  dataScope: string;\n  menuCheckStrictly?: any;\n  deptCheckStrictly?: any;\n  status: string;\n  remark: string;\n  createTime?: any;\n  flag: boolean;\n  superAdmin: boolean;\n}\n\nexport interface User {\n  userId: number;\n  tenantId: string;\n  deptId: number;\n  userName: string;\n  nickName: string;\n  userType: string;\n  email: string;\n  phonenumber: string;\n  sex: string;\n  avatar: string;\n  status: string;\n  loginIp: string;\n  loginDate: string;\n  remark: string;\n  createTime: string;\n  dept: Dept;\n  roles: Role[];\n  roleIds?: string[];\n  postIds?: string[];\n  roleId: number;\n  deptName: string;\n}\n\n/**\n * @description 用户个人主页信息\n * @param user 用户信息\n * @param roleGroup 角色名称\n * @param postGroup 岗位名称\n */\nexport interface UserProfile {\n  user: User;\n  roleGroup: string;\n  postGroup: string;\n}\n\nexport interface UpdatePasswordParam {\n  oldPassword: string;\n  newPassword: string;\n}\n\ninterface FileCallBack {\n  name: string;\n  file: Blob;\n  filename: string;\n}\n"
  },
  {
    "path": "apps/web-antd/src/api/system/role/index.ts",
    "content": "import type { User } from '../user/model';\nimport type { DeptResp, Role } from './model';\n\nimport type { ID, IDS, PageQuery, PageResult } from '#/api/common';\n\nimport { commonExport } from '#/api/helper';\nimport { requestClient } from '#/api/request';\n\nenum Api {\n  roleAllocatedList = '/system/role/authUser/allocatedList',\n  roleAuthCancel = '/system/role/authUser/cancel',\n  roleAuthCancelAll = '/system/role/authUser/cancelAll',\n  roleAuthSelectAll = '/system/role/authUser/selectAll',\n  roleChangeStatus = '/system/role/changeStatus',\n  roleDataScope = '/system/role/dataScope',\n  roleDeptTree = '/system/role/deptTree',\n  roleExport = '/system/role/export',\n  roleList = '/system/role/list',\n  roleOptionSelect = '/system/role/optionselect',\n  roleUnallocatedList = '/system/role/authUser/unallocatedList',\n  root = '/system/role',\n}\n\n/**\n * 查询角色分页列表\n * @param params 搜索条件\n * @returns 分页列表\n */\nexport function roleList(params?: PageQuery) {\n  return requestClient.get<PageResult<Role>>(Api.roleList, { params });\n}\n\n/**\n * 导出角色信息\n * @param data 查询参数\n * @returns blob\n */\nexport function roleExport(data: Partial<Role>) {\n  return commonExport(Api.roleExport, data);\n}\n\n/**\n * 查询角色信息\n * @param roleId 角色id\n * @returns 角色信息\n */\nexport function roleInfo(roleId: ID) {\n  return requestClient.get<Role>(`${Api.root}/${roleId}`);\n}\n\n/**\n * 角色新增\n * @param data 参数\n * @returns void\n */\nexport function roleAdd(data: Partial<Role>) {\n  return requestClient.postWithMsg<void>(Api.root, data);\n}\n\n/**\n * 角色更新\n * @param data 参数\n * @returns void\n */\nexport function roleUpdate(data: Partial<Role>) {\n  return requestClient.putWithMsg<void>(Api.root, data);\n}\n\n/**\n * 修改角色状态\n * @param data 参数\n * @returns void\n */\nexport function roleChangeStatus(data: Partial<Role>) {\n  const requestData = {\n    roleId: data.roleId,\n    status: data.status,\n  };\n  return requestClient.putWithMsg<void>(Api.roleChangeStatus, requestData);\n}\n\n/**\n * 角色删除\n * @param roleIds ids\n * @returns void\n */\nexport function roleRemove(roleIds: IDS) {\n  return requestClient.deleteWithMsg<void>(`${Api.root}/${roleIds}`);\n}\n\n/**\n * 更新数据权限\n * @param data\n * @returns void\n */\nexport function roleDataScope(data: any) {\n  return requestClient.putWithMsg<void>(Api.roleDataScope, data);\n}\n\n/**\n * @deprecated 全局并没有用到这个方法\n */\nexport function roleOptionSelect(params?: any) {\n  return requestClient.get(Api.roleOptionSelect, { params });\n}\n\n/**\n * 已分配角色的用户分页\n * @param params 请求参数\n * @returns 分页\n */\nexport function roleAllocatedList(params?: PageQuery) {\n  return requestClient.get<PageResult<User>>(Api.roleAllocatedList, { params });\n}\n\n/**\n * 未授权的用户\n * @param params\n * @returns void\n */\nexport function roleUnallocatedList(params: any) {\n  return requestClient.get<PageResult<User>>(Api.roleUnallocatedList, {\n    params,\n  });\n}\n\n/**\n * 取消用户角色授权\n * @returns void\n */\nexport function roleAuthCancel(data: { roleId: ID; userId: ID }) {\n  return requestClient.putWithMsg<void>(Api.roleAuthCancel, data);\n}\n\n/**\n * 批量取消授权\n * @param roleId 角色ID\n * @param userIds 用户ID集合\n * @returns void\n */\nexport function roleAuthCancelAll(roleId: ID, userIds: IDS) {\n  return requestClient.putWithMsg<void>(\n    `${Api.roleAuthCancelAll}?roleId=${roleId}&userIds=${userIds.join(',')}`,\n  );\n}\n\n/**\n * 批量授权用户\n * @param roleId 角色ID\n * @param userIds 用户ID集合\n * @returns void\n */\nexport function roleSelectAll(roleId: ID, userIds: IDS) {\n  return requestClient.putWithMsg<void>(\n    `${Api.roleAuthSelectAll}?roleId=${roleId}&userIds=${userIds.join(',')}`,\n  );\n}\n\n/**\n * 根据角色id获取部门树\n * @param roleId 角色id\n * @returns DeptResp\n */\nexport function roleDeptTree(roleId: ID) {\n  return requestClient.get<DeptResp>(`${Api.roleDeptTree}/${roleId}`);\n}\n"
  },
  {
    "path": "apps/web-antd/src/api/system/role/model.d.ts",
    "content": "export interface Role {\n  roleId: number;\n  roleName: string;\n  roleKey: string;\n  roleSort: number;\n  dataScope: string;\n  menuCheckStrictly: boolean;\n  deptCheckStrictly: boolean;\n  status: string;\n  remark: string;\n  createTime: string;\n  // 用户是否存在此角色标识 默认不存在\n  flag: boolean;\n  superAdmin: boolean;\n}\n\nexport interface DeptOption {\n  id: number;\n  parentId: number;\n  label: string;\n  weight: number;\n  children: DeptOption[];\n  key: string; // 实际上不存在 ide报错\n}\n\nexport interface DeptResp {\n  checkedKeys: number[];\n  depts: DeptOption[];\n}\n"
  },
  {
    "path": "apps/web-antd/src/api/system/social/index.ts",
    "content": "import type { SocialInfo } from './model';\n\nimport type { ID } from '#/api/common';\n\nimport { requestClient } from '#/api/request';\n\nenum Api {\n  root = '/system/social',\n  socialList = '/system/social/list',\n}\n\n/**\n * 获取绑定的社交信息列表\n * @returns info\n */\nexport function socialList() {\n  return requestClient.get<SocialInfo[]>(Api.socialList);\n}\n\n/**\n * @deprecated 并没有用到这个方法\n */\nexport function socialInfo(id: ID) {\n  return requestClient.get(`${Api.root}/${id}`);\n}\n"
  },
  {
    "path": "apps/web-antd/src/api/system/social/model.d.ts",
    "content": "export interface SocialInfo {\n  id: string;\n  userId: number;\n  tenantId: string;\n  authId: string;\n  source: string;\n  accessToken: string;\n  expireIn: number;\n  refreshToken: string;\n  openId: string;\n  userName: string;\n  nickName: string;\n  email: string;\n  avatar: string;\n  accessCode?: any;\n  unionId?: any;\n  scope: string;\n  tokenType: string;\n  idToken?: any;\n  macAlgorithm?: any;\n  macKey?: any;\n  code?: any;\n  oauthToken?: any;\n  oauthTokenSecret?: any;\n  createTime: string;\n}\n"
  },
  {
    "path": "apps/web-antd/src/api/system/tenant/index.ts",
    "content": "import type { Tenant } from './model';\n\nimport type { ID, IDS, PageQuery } from '#/api/common';\n\nimport { commonExport } from '#/api/helper';\nimport { requestClient } from '#/api/request';\n\nenum Api {\n  dictSync = '/system/tenant/syncTenantDict',\n  root = '/system/tenant',\n  tenantDynamic = '/system/tenant/dynamic',\n  tenantDynamicClear = '/system/tenant/dynamic/clear',\n  tenantExport = '/system/tenant/export',\n  tenantList = '/system/tenant/list',\n  tenantStatus = '/system/tenant/changeStatus',\n  tenantSyncPackage = '/system/tenant/syncTenantPackage',\n}\n\n/**\n * 查询租户分页列表\n * @param params 参数\n * @returns 分页\n */\nexport function tenantList(params?: PageQuery) {\n  return requestClient.get<Tenant[]>(Api.tenantList, { params });\n}\n\n/**\n * 租户导出\n * @param data data\n * @returns void\n */\nexport function tenantExport(data: Partial<Tenant>) {\n  return commonExport(Api.tenantExport, data);\n}\n\n/**\n * 查询租户信息\n * @param id id\n * @returns 租户信息\n */\nexport function tenantInfo(id: ID) {\n  return requestClient.get<Tenant>(`${Api.root}/${id}`);\n}\n\n/**\n * 新增租户 必须开启加密\n * @param data data\n * @returns void\n */\nexport function tenantAdd(data: Partial<Tenant>) {\n  return requestClient.postWithMsg<void>(Api.root, data, { encrypt: true });\n}\n\n/**\n * 租户更新\n * @param data data\n * @returns void\n */\nexport function tenantUpdate(data: Partial<Tenant>) {\n  return requestClient.putWithMsg<void>(Api.root, data);\n}\n\n/**\n * 租户状态更新\n * @param data data\n * @returns void\n */\nexport function tenantStatusChange(data: Partial<Tenant>) {\n  const requestData = {\n    id: data.id,\n    tenantId: data.tenantId,\n    status: data.status,\n  };\n  return requestClient.putWithMsg(Api.tenantStatus, requestData);\n}\n\n/**\n * 租户删除\n * @param ids ids\n * @returns void\n */\nexport function tenantRemove(ids: IDS) {\n  return requestClient.deleteWithMsg(`${Api.root}/${ids}`);\n}\n\n/**\n * 动态切换租户\n * @param tenantId 租户ID\n * @returns void\n */\nexport function tenantDynamicToggle(tenantId: string) {\n  return requestClient.get<void>(`${Api.tenantDynamic}/${tenantId}`);\n}\n\n/**\n * 清除 动态切换租户\n * @returns void\n */\nexport function tenantDynamicClear() {\n  return requestClient.get<void>(Api.tenantDynamicClear);\n}\n\n/**\n * 租户套餐同步\n * @param tenantId 租户id\n * @param packageId 套餐id\n * @returns void\n */\nexport function tenantSyncPackage(tenantId: string, packageId: string) {\n  return requestClient.get<void>(Api.tenantSyncPackage, {\n    params: { packageId, tenantId },\n    successMessageMode: 'message',\n  });\n}\n\n/**\n * 同步租户字典\n * @param tenantId 租户ID\n * @returns void\n */\nexport function dictSyncTenant(tenantId?: string) {\n  return requestClient.get<void>(Api.dictSync, {\n    params: { tenantId },\n    successMessageMode: 'message',\n  });\n}\n\n/**\n * 同步租户配置\n * @returns void\n */\nexport function syncTenantConfig() {\n  return requestClient.get<void>('/system/tenant/syncTenantConfig', {\n    successMessageMode: 'message',\n  });\n}\n"
  },
  {
    "path": "apps/web-antd/src/api/system/tenant/model.d.ts",
    "content": "export interface Tenant {\n  accountCount: number;\n  address?: string;\n  companyName: string;\n  contactPhone: string;\n  contactUserName: string;\n  domain?: string;\n  expireTime?: string;\n  id: number;\n  intro: string;\n  licenseNumber?: any;\n  packageId: string;\n  remark?: string;\n  status: string;\n  tenantId: string;\n}\n"
  },
  {
    "path": "apps/web-antd/src/api/system/tenant-package/index.ts",
    "content": "import type { TenantPackage } from './model';\n\nimport type { ID, IDS, PageQuery, PageResult } from '#/api/common';\n\nimport { commonExport } from '#/api/helper';\nimport { requestClient } from '#/api/request';\n\nenum Api {\n  packageChangeStatus = '/system/tenant/package/changeStatus',\n  packageExport = '/system/tenant/package/export',\n  packageList = '/system/tenant/package/list',\n  packageSelectList = '/system/tenant/package/selectList',\n  root = '/system/tenant/package',\n}\n\n/**\n * 租户套餐分页列表\n * @param params 请求参数\n * @returns 分页列表\n */\nexport function packageList(params?: PageQuery) {\n  return requestClient.get<PageResult<TenantPackage>>(Api.packageList, {\n    params,\n  });\n}\n\n/**\n * 租户套餐下拉框\n * @returns 下拉框\n */\nexport function packageSelectList() {\n  return requestClient.get<TenantPackage[]>(Api.packageSelectList);\n}\n\n/**\n * 租户套餐导出\n * @param data 参数\n * @returns blob\n */\nexport function packageExport(data: Partial<TenantPackage>) {\n  return commonExport(Api.packageExport, data);\n}\n\n/**\n * 租户套餐信息\n * @param id id\n * @returns 信息\n */\nexport function packageInfo(id: ID) {\n  return requestClient.get<TenantPackage>(`${Api.root}/${id}`);\n}\n\n/**\n * 租户套餐新增\n * @param data data\n * @returns void\n */\nexport function packageAdd(data: Partial<TenantPackage>) {\n  return requestClient.postWithMsg<void>(Api.root, data);\n}\n\n/**\n * 租户套餐更新\n * @param data data\n * @returns void\n */\nexport function packageUpdate(data: Partial<TenantPackage>) {\n  return requestClient.putWithMsg<void>(Api.root, data);\n}\n\n/**\n * 租户套餐状态变更\n * @param data data\n * @returns void\n */\nexport function packageChangeStatus(data: Partial<TenantPackage>) {\n  const packageId = {\n    packageId: data.packageId,\n    status: data.status,\n  };\n  return requestClient.putWithMsg<void>(Api.packageChangeStatus, packageId);\n}\n\n/**\n * 租户套餐移除\n * @param ids ids\n * @returns void\n */\nexport function packageRemove(ids: IDS) {\n  return requestClient.deleteWithMsg<void>(`${Api.root}/${ids}`);\n}\n"
  },
  {
    "path": "apps/web-antd/src/api/system/tenant-package/model.d.ts",
    "content": "/**\n * @description 租户套餐\n * @param packageId id\n * @param packageName 名称\n * @param menuIds 菜单id  格式为[1,2,3] 返回为string 提交为数组\n * @param remark 备注\n * @param menuCheckStrictly 是否关联父节点\n * @param status 状态\n */\nexport interface TenantPackage {\n  packageId: string;\n  packageName: string;\n  menuIds: number[] | string;\n  remark: string;\n  menuCheckStrictly: boolean;\n  status: string;\n}\n"
  },
  {
    "path": "apps/web-antd/src/api/system/user/index.ts",
    "content": "import type {\n  DeptTree,\n  ResetPwdParam,\n  User,\n  UserImportParam,\n  UserInfoResponse,\n} from './model';\n\nimport type { ID, IDS, PageQuery, PageResult } from '#/api/common';\n\nimport { commonExport, ContentTypeEnum } from '#/api/helper';\nimport { requestClient } from '#/api/request';\n\nenum Api {\n  deptTree = '/system/user/deptTree',\n  listDeptUsers = '/system/user/list/dept',\n  root = '/system/user',\n  userAuthRole = '/system/user/authRole',\n  userExport = '/system/user/export',\n  userImport = '/system/user/importData',\n  userImportTemplate = '/system/user/importTemplate',\n  userList = '/system/user/list',\n  userResetPassword = '/system/user/resetPwd',\n  userStatusChange = '/system/user/changeStatus',\n}\n\n/**\n *  获取用户列表\n * @param params\n * @returns User\n */\nexport function userList(params?: PageQuery) {\n  return requestClient.get<PageResult<User>>(Api.userList, { params });\n}\n\n/**\n * 导出excel\n * @param data data\n * @returns blob\n */\nexport function userExport(data: Partial<User>) {\n  return commonExport(Api.userExport, data);\n}\n\n/**\n * 从excel导入用户\n * @param data\n * @returns void\n */\nexport function userImportData(data: UserImportParam) {\n  return requestClient.post<{ code: number; msg: string }>(\n    Api.userImport,\n    data,\n    {\n      headers: {\n        'Content-Type': ContentTypeEnum.FORM_DATA,\n      },\n      isTransformResponse: false,\n    },\n  );\n}\n\n/**\n * 下载用户导入模板\n * @returns blob\n */\nexport function downloadImportTemplate() {\n  return requestClient.post<Blob>(\n    Api.userImportTemplate,\n    {},\n    {\n      isTransformResponse: false,\n      responseType: 'blob',\n    },\n  );\n}\n\n/**\n * 可以不传ID  返回部门和角色options 需要获得原始数据\n * 不传ID时一定要带最后的/\n * @param userId 用户ID\n * @returns 用户信息\n */\nexport function findUserInfo(userId?: ID) {\n  const url = userId ? `${Api.root}/${userId}` : `${Api.root}/`;\n  return requestClient.get<UserInfoResponse>(url);\n}\n\n/**\n * 新增用户\n * @param data data\n * @returns void\n */\nexport function userAdd(data: Partial<User>) {\n  return requestClient.postWithMsg<void>(Api.root, data);\n}\n\n/**\n * 更新用户\n * @param data data\n * @returns void\n */\nexport function userUpdate(data: Partial<User>) {\n  return requestClient.putWithMsg<void>(Api.root, data);\n}\n\n/**\n * 更新用户状态\n * @param data data\n * @returns void\n */\nexport function userStatusChange(data: Partial<User>) {\n  const requestData = {\n    userId: data.userId,\n    status: data.status,\n  };\n  return requestClient.putWithMsg<void>(Api.userStatusChange, requestData);\n}\n\n/**\n * 删除用户\n * @param userIds 用户ID数组\n * @returns void\n */\nexport function userRemove(userIds: IDS) {\n  return requestClient.deleteWithMsg<void>(`${Api.root}/${userIds}`);\n}\n\n/**\n * 重置用户密码 需要加密\n * @param data\n * @returns void\n */\nexport function userResetPassword(data: ResetPwdParam) {\n  return requestClient.putWithMsg<void>(Api.userResetPassword, data, {\n    encrypt: true,\n  });\n}\n\n/**\n * 这个方法未调用过\n * @param userId\n * @returns void\n */\nexport function getUserAuthRole(userId: ID) {\n  return requestClient.get(`${Api.userAuthRole}/${userId}`);\n}\n\n/**\n * 这个方法未调用过\n * @param userId\n * @returns void\n */\nexport function userAuthRoleUpdate(userId: ID, roleIds: number[]) {\n  return requestClient.putWithMsg(Api.userAuthRole, { roleIds, userId });\n}\n\n/**\n * 获取部门树\n * @returns 部门树数组\n */\nexport function getDeptTree() {\n  return requestClient.get<DeptTree[]>(Api.deptTree);\n}\n\n/**\n * 获取部门下的所有用户信息\n */\nexport function listUserByDeptId(deptId: ID) {\n  return requestClient.get<User[]>(`${Api.listDeptUsers}/${deptId}`);\n}\n"
  },
  {
    "path": "apps/web-antd/src/api/system/user/model.d.ts",
    "content": "/**\n * @description: 用户导入\n * @param updateSupport 是否覆盖数据\n * @param file excel文件\n */\nexport interface UserImportParam {\n  updateSupport: boolean;\n  file: Blob | File;\n}\n\n/**\n * @description: 重置密码\n */\nexport interface ResetPwdParam {\n  userId: string;\n  password: string;\n}\n\nexport interface Dept {\n  deptId: number;\n  parentId: number;\n  parentName?: string;\n  ancestors: string;\n  deptName: string;\n  orderNum: number;\n  leader: string;\n  phone?: string;\n  email?: string;\n  status: string;\n  createTime?: string;\n}\n\nexport interface Role {\n  roleId: string;\n  roleName: string;\n  roleKey: string;\n  roleSort: number;\n  dataScope: string;\n  menuCheckStrictly?: boolean;\n  deptCheckStrictly?: boolean;\n  status: string;\n  remark: string;\n  createTime?: string;\n  flag: boolean;\n  superAdmin: boolean;\n}\n\nexport interface User {\n  userId: string;\n  tenantId: string;\n  deptId: number;\n  userName: string;\n  nickName: string;\n  userType: string;\n  email: string;\n  phonenumber: string;\n  sex: string;\n  avatar?: string;\n  status: string;\n  loginIp: string;\n  loginDate: string;\n  remark: string;\n  createTime: string;\n  dept: Dept;\n  roles: Role[];\n  roleIds?: string[];\n  postIds?: number[];\n  roleId: string;\n  deptName: string;\n}\n\nexport interface Post {\n  postId: number;\n  postCode: string;\n  postName: string;\n  postSort: number;\n  status: string;\n  remark: string;\n  createTime: string;\n}\n\n/**\n * @description 用户信息\n * @param user 用户个人信息\n * @param roleIds 角色IDS 不传id为空\n * @param roles 所有的角色\n * @param postIds 岗位IDS 不传id为空\n * @param posts 所有的岗位\n */\nexport interface UserInfoResponse {\n  user?: User;\n  roleIds?: string[];\n  roles: Role[];\n  postIds?: number[];\n  posts?: Post[];\n}\n\n/**\n * @description: 部门树\n */\nexport interface DeptTree {\n  id: number;\n  /**\n   * antd组件必须要这个属性 实际是没有这个属性的\n   */\n  key: string;\n  parentId: number;\n  label: string;\n  weight: number;\n  children?: DeptTree[];\n}\n\nexport interface DeptTreeData {\n  id: number;\n  label: string;\n  children?: DeptTreeData[];\n}\n"
  },
  {
    "path": "apps/web-antd/src/api/tool/gen/index.ts",
    "content": "import type { GenInfo } from './model';\n\nimport type { ID, IDS, PageQuery } from '#/api/common';\n\nimport { ContentTypeEnum } from '#/api/helper';\nimport { requestClient } from '#/api/request';\n\nenum Api {\n  batchGenCode = '/tool/gen/batchGenCode',\n  columnList = '/tool/gen/column',\n  dataSourceNames = '/tool/gen/getDataNames',\n  download = '/tool/gen/download',\n  genCode = '/tool/gen/genCode',\n  generatedList = '/tool/gen/list',\n  importTable = '/tool/gen/importTable',\n  preview = '/tool/gen/preview',\n  readyToGenList = '/tool/gen/db/list',\n  root = '/tool/gen',\n  syncDb = '/tool/gen/synchDb',\n}\n// 查询代码生成列表\nexport function generatedList(params?: PageQuery) {\n  return requestClient.get(Api.generatedList, { params });\n}\n\n// 修改代码生成业务\nexport function genInfo(tableId: ID) {\n  return requestClient.get<GenInfo>(`${Api.root}/${tableId}`);\n}\n\n// 查询数据库列表\nexport function readyToGenList(params?: PageQuery) {\n  return requestClient.get(Api.readyToGenList, { params });\n}\n\n// 查询数据表字段列表\nexport function columnList(tableId: ID) {\n  return requestClient.get(`${Api.columnList}/${tableId}`);\n}\n\n/**\n * 导入表结构（保存）\n * @param tables table名称数组 如sys_a, sys_b\n * @param dataName 数据源名称\n * @returns ret\n */\nexport function importTable(tables: string | string[], dataName: string) {\n  return requestClient.postWithMsg(\n    Api.importTable,\n    { dataName, tables },\n    {\n      headers: { 'Content-Type': ContentTypeEnum.FORM_URLENCODED },\n    },\n  );\n}\n\n// 修改保存代码生成业务\nexport function editSave(data: any) {\n  return requestClient.putWithMsg(Api.root, data);\n}\n\n// 删除代码生成\nexport function genRemove(tableIds: IDS) {\n  return requestClient.deleteWithMsg(`${Api.root}/${tableIds}`);\n}\n\n// 预览代码\nexport function previewCode(tableId: ID) {\n  return requestClient.get<{ [key: string]: string }>(\n    `${Api.preview}/${tableId}`,\n  );\n}\n\n// 生成代码（下载方式）\nexport function genDownload(tableId: ID) {\n  return requestClient.get<Blob>(`${Api.download}/${tableId}`);\n}\n\n// 生成代码（自定义路径）\nexport function genWithPath(tableId: ID) {\n  return requestClient.get<void>(`${Api.genCode}/${tableId}`);\n}\n\n// 同步数据库\nexport function syncDb(tableId: ID) {\n  return requestClient.get(`${Api.syncDb}/${tableId}`, {\n    successMessageMode: 'message',\n  });\n}\n\n// 批量生成代码\nexport function batchGenCode(tableIdStr: ID | IDS) {\n  return requestClient.get<Blob>(Api.batchGenCode, {\n    isTransformResponse: false,\n    params: { tableIdStr },\n    responseType: 'blob',\n  });\n}\n\n// 查询数据源名称列表\nexport function getDataSourceNames() {\n  return requestClient.get<string[]>(Api.dataSourceNames);\n}\n"
  },
  {
    "path": "apps/web-antd/src/api/tool/gen/model.d.ts",
    "content": "export interface Column {\n  createDept?: any;\n  createBy?: any;\n  createTime?: any;\n  updateBy?: any;\n  updateTime?: any;\n  columnId: string;\n  tableId: string;\n  columnName: string;\n  columnComment: string;\n  columnType: string;\n  javaType: string;\n  javaField: string;\n  isPk: string;\n  isIncrement: string;\n  isRequired: string;\n  isInsert?: any;\n  isEdit: string;\n  isList: string;\n  isQuery?: any;\n  queryType: string;\n  htmlType: string;\n  dictType: string;\n  sort: number;\n  list: boolean;\n  required: boolean;\n  pk: boolean;\n  insert: boolean;\n  edit: boolean;\n  usableColumn: boolean;\n  superColumn: boolean;\n  increment: boolean;\n  query: boolean;\n  capJavaField: string;\n}\n\nexport interface Table {\n  createDept?: any;\n  createBy?: any;\n  createTime?: any;\n  updateBy?: any;\n  updateTime?: any;\n  tableId: string;\n  dataName: string;\n  tableName: string;\n  tableComment: string;\n  subTableName?: any;\n  subTableFkName?: any;\n  className: string;\n  tplCategory: string;\n  packageName: string;\n  moduleName: string;\n  businessName: string;\n  functionName: string;\n  functionAuthor: string;\n  genType?: any;\n  genPath?: any;\n  pkColumn?: any;\n  columns: Column[];\n  options?: any;\n  remark?: any;\n  treeCode?: any;\n  treeParentCode?: any;\n  treeName?: any;\n  menuIds?: any;\n  parentMenuId?: any;\n  parentMenuName?: any;\n  tree: boolean;\n  crud: boolean;\n}\n\nexport interface Row {\n  createDept: number;\n  createBy: number;\n  createTime: string;\n  updateBy: number;\n  updateTime: string;\n  columnId: string;\n  tableId: string;\n  columnName: string;\n  columnComment: string;\n  columnType: string;\n  javaType: string;\n  javaField: string;\n  isPk: string;\n  isIncrement: string;\n  isRequired: string;\n  isInsert?: any;\n  isEdit: string;\n  isList: string;\n  isQuery?: any;\n  queryType: string;\n  htmlType: string;\n  dictType: string;\n  sort: number;\n  list: boolean;\n  required: boolean;\n  pk: boolean;\n  insert: boolean;\n  edit: boolean;\n  usableColumn: boolean;\n  superColumn: boolean;\n  increment: boolean;\n  query: boolean;\n  capJavaField: string;\n}\n\nexport interface Column {\n  createDept?: any;\n  createBy?: any;\n  createTime?: any;\n  updateBy?: any;\n  updateTime?: any;\n  columnId: string;\n  tableId: string;\n  columnName: string;\n  columnComment: string;\n  columnType: string;\n  javaType: string;\n  javaField: string;\n  isPk: string;\n  isIncrement: string;\n  isRequired: string;\n  isInsert?: any;\n  isEdit: string;\n  isList: string;\n  isQuery?: any;\n  queryType: string;\n  htmlType: string;\n  dictType: string;\n  sort: number;\n  list: boolean;\n  required: boolean;\n  pk: boolean;\n  insert: boolean;\n  edit: boolean;\n  usableColumn: boolean;\n  superColumn: boolean;\n  increment: boolean;\n  query: boolean;\n  capJavaField: string;\n}\n\nexport interface Info {\n  createDept?: any;\n  createBy?: any;\n  createTime?: any;\n  updateBy?: any;\n  updateTime?: any;\n  tableId: string;\n  dataName: string;\n  tableName: string;\n  tableComment: string;\n  subTableName?: any;\n  subTableFkName?: any;\n  className: string;\n  tplCategory: string;\n  packageName: string;\n  moduleName: string;\n  businessName: string;\n  functionName: string;\n  functionAuthor: string;\n  genType: string;\n  genPath: string;\n  pkColumn?: any;\n  columns: Column[];\n  options?: any;\n  remark?: any;\n  treeCode?: any;\n  treeParentCode?: any;\n  treeName?: any;\n  menuIds?: any;\n  parentMenuId?: any;\n  parentMenuName?: any;\n  tree: boolean;\n  crud: boolean;\n  // 树表需要添加此属性\n  params?: any;\n  popupComponent?: string;\n  formComponent?: string;\n}\n\nexport interface GenInfo {\n  tables: Table[];\n  rows: Row[];\n  info: Info;\n}\n"
  },
  {
    "path": "apps/web-antd/src/api/workflow/category/index.ts",
    "content": "import type {\n  CategoryForm,\n  CategoryQuery,\n  CategoryTree,\n  CategoryVO,\n} from './model';\n\nimport type { ID, IDS } from '#/api/common';\n\nimport { requestClient } from '#/api/request';\n\n/**\n * 获取流程分类树列表\n * @returns tree\n */\nexport function categoryTree() {\n  return requestClient.get<CategoryTree[]>('/workflow/category/categoryTree');\n}\n\n/**\n * 查询流程分类列表\n * @param params\n * @returns 流程分类列表\n */\nexport function categoryList(params?: CategoryQuery) {\n  return requestClient.get<CategoryVO[]>(`/workflow/category/list`, { params });\n}\n\n/**\n * 查询流程分类详情\n * @param id id\n * @returns 流程分类详情\n */\nexport function categoryInfo(id: ID) {\n  return requestClient.get<CategoryVO>(`/workflow/category/${id}`);\n}\n\n/**\n * 新增流程分类\n * @param data\n * @returns void\n */\nexport function categoryAdd(data: CategoryForm) {\n  return requestClient.postWithMsg<void>('/workflow/category', data);\n}\n\n/**\n * 更新流程分类\n * @param data\n * @returns void\n */\nexport function categoryUpdate(data: CategoryForm) {\n  return requestClient.putWithMsg<void>('/workflow/category', data);\n}\n\n/**\n * 删除流程分类\n * @param id id\n * @returns void\n */\nexport function categoryRemove(id: ID | IDS) {\n  return requestClient.deleteWithMsg<void>(`/workflow/category/${id}`);\n}\n"
  },
  {
    "path": "apps/web-antd/src/api/workflow/category/model.d.ts",
    "content": "import type { BaseEntity } from '#/api/common';\n\nexport interface CategoryVO {\n  /**\n   * 主键\n   */\n  id: number | string;\n\n  /**\n   * 分类名称\n   */\n  categoryName: string;\n\n  /**\n   * 分类编码\n   */\n  categoryCode: string;\n\n  /**\n   * 父级id\n   */\n  parentId: number | string;\n\n  /**\n   * 排序\n   */\n  sortNum: number;\n\n  /**\n   * 子对象\n   */\n  children: CategoryVO[];\n  key: string;\n}\n\nexport interface CategoryForm extends BaseEntity {\n  /**\n   * 主键\n   */\n  id?: number | string;\n\n  /**\n   * 分类名称\n   */\n  categoryName?: string;\n\n  /**\n   * 分类编码\n   */\n  categoryCode?: string;\n\n  /**\n   * 父级id\n   */\n  parentId?: number | string;\n\n  /**\n   * 排序\n   */\n  sortNum?: number;\n}\n\nexport interface CategoryQuery {\n  /**\n   * 分类名称\n   */\n  categoryName?: string;\n\n  /**\n   * 分类编码\n   */\n  categoryCode?: string;\n\n  /**\n   * 父级id\n   */\n  parentId?: number | string;\n\n  /**\n   * 排序\n   */\n  sortNum?: number;\n\n  /**\n   * 日期范围参数\n   */\n  params?: any;\n}\n\nexport interface CategoryTree {\n  id: number;\n  parentId: number;\n  label: string;\n  weight: number;\n  children: CategoryTree[];\n  key: string;\n}\n"
  },
  {
    "path": "apps/web-antd/src/api/workflow/definition/index.ts",
    "content": "import type { ProcessDefinition } from './model';\n\nimport type { ID, IDS, PageQuery, PageResult } from '#/api/common';\n\nimport { requestClient } from '#/api/request';\n\n/**\n * 全部的流程定义\n * @param params 查询参数\n * @returns 分页\n */\nexport function workflowDefinitionList(params?: PageQuery) {\n  return requestClient.get<PageResult<ProcessDefinition>>(\n    '/workflow/definition/list',\n    { params },\n  );\n}\n\n/**\n * 未发布的流程定义\n * @param params 查询参数\n * @returns 分页\n */\nexport function unPublishList(params?: PageQuery) {\n  return requestClient.get<PageResult<ProcessDefinition>>(\n    '/workflow/definition/unPublishList',\n    { params },\n  );\n}\n\n/**\n * 获取历史流程定义列表\n * @param flowCode\n * @returns ProcessDefinition[]\n */\nexport function getHisListByKey(flowCode: string) {\n  return requestClient.get<ProcessDefinition[]>(\n    `/workflow/definition/getHisListByKey/${flowCode}`,\n  );\n}\n\n/**\n * 获取流程定义详细信息\n * @param id id\n * @returns ProcessDefinition\n */\nexport function workflowDefinitionInfo(id: ID) {\n  return requestClient.get<ProcessDefinition>(`/workflow/definition/${id}`);\n}\n\n/**\n * 新增流程定义\n * @param data\n */\nexport function workflowDefinitionAdd(data: any) {\n  return requestClient.postWithMsg<void>('/workflow/definition', data);\n}\n\n/**\n * 更新流程定义\n * @param data\n */\nexport function workflowDefinitionUpdate(data: any) {\n  return requestClient.putWithMsg<void>('/workflow/definition', data);\n}\n\n/**\n * 发布流程定义\n * @param id id\n * @returns boolean\n */\nexport function workflowDefinitionPublish(id: ID) {\n  return requestClient.putWithMsg<boolean>(\n    `/workflow/definition/publish/${id}`,\n  );\n}\n\n/**\n * 取消发布流程定义\n * @param id id\n * @returns boolean\n */\nexport function workflowDefinitionUnPublish(id: ID) {\n  return requestClient.putWithMsg<boolean>(\n    `/workflow/definition/unPublish/${id}`,\n  );\n}\n\n/**\n * 删除流程定义\n * @param ids idList\n */\nexport function workflowDefinitionDelete(ids: IDS) {\n  return requestClient.deleteWithMsg<void>(`/workflow/definition/${ids}`);\n}\n\n/**\n * 复制流程定义\n * @param id id\n */\nexport function workflowDefinitionCopy(id: ID) {\n  return requestClient.postWithMsg<void>(`/workflow/definition/copy/${id}`);\n}\n\n/**\n * 导入流程定义\n * @returns boolean\n */\nexport function workflowDefinitionImport(data: {\n  category: ID;\n  file: Blob | File;\n}) {\n  return requestClient.postWithMsg<boolean>(\n    '/workflow/definition/importDef',\n    data,\n    { headers: { 'Content-Type': 'multipart/form-data' } },\n  );\n}\n\n/**\n * 导出流程定义\n * @param id id\n * @returns blob\n */\nexport function workflowDefinitionExport(id: ID) {\n  return requestClient.postWithMsg<Blob>(\n    `/workflow/definition/exportDef/${id}`,\n    {},\n    {\n      responseType: 'blob',\n      isTransformResponse: false,\n    },\n  );\n}\n\n/**\n * 获取流程定义xml字符串\n * @param id id\n * @returns xml\n */\nexport function workflowDefinitionXml(id: ID) {\n  return requestClient.get<string>(`/workflow/definition/xmlString/${id}`);\n}\n\n/**\n * 激活/挂起流程定义\n * @param id 流程定义id\n * @param active 激活/挂起\n * @returns boolean\n */\nexport function workflowDefinitionActive(id: ID, active: boolean) {\n  return requestClient.putWithMsg<boolean>(\n    `/workflow/definition/active/${id}?active=${active}`,\n  );\n}\n"
  },
  {
    "path": "apps/web-antd/src/api/workflow/definition/model.d.ts",
    "content": "export interface ProcessDefinition {\n  id: string;\n  createTime: string;\n  updateTime: string;\n  tenantId: string;\n  delFlag: string;\n  flowCode: string;\n  flowName: string;\n  category: string;\n  categoryName: string;\n  version: string;\n  isPublish: number;\n  formCustom: string;\n  formPath: string;\n  activityStatus: number;\n  listenerType?: any;\n  listenerPath?: any;\n  ext?: any;\n}\n"
  },
  {
    "path": "apps/web-antd/src/api/workflow/instance/index.ts",
    "content": "import type { TaskInfo } from '../task/model';\nimport type { FlowInfoResponse, FlowInstanceVariableResp } from './model';\n\nimport type { ID, IDS, PageQuery, PageResult } from '#/api/common';\n\nimport { requestClient } from '#/api/request';\n\n/**\n * @param businessId 业务ID\n * @returns TaskInfo\n */\nexport function getTaskByBusinessId(businessId: string) {\n  return requestClient.get<TaskInfo>(\n    `/workflow/instance/getInfo/${businessId}`,\n  );\n}\n\n/**\n * 分页查询正在运行的流程实例\n * @param params\n * @returns\n */\nexport function pageByRunning(params?: PageQuery) {\n  return requestClient.get('/workflow/instance/pageByRunning', { params });\n}\n\n/**\n * pageByFinish\n * @param params\n * @returns\n */\nexport function pageByFinish(params?: PageQuery) {\n  return requestClient.get('/workflow/instance/pageByFinish', { params });\n}\n\n/**\n * 按照业务id删除流程实例\n * @param businessIds 业务id\n */\nexport function deleteByBusinessIds(businessIds: IDS) {\n  return requestClient.deleteWithMsg<void>(\n    `/workflow/instance/deleteByBusinessIds${businessIds}`,\n  );\n}\n\n/**\n * 按照实例id删除流程实例\n * @param instanceIds 实例id\n */\nexport function deleteByInstanceIds(instanceIds: IDS) {\n  return requestClient.deleteWithMsg<void>(\n    `/workflow/instance/deleteByInstanceIds/${instanceIds}`,\n  );\n}\n\n/**\n * 撤销流程\n * @param data\n */\nexport function cancelProcessApply(data: { businessId: ID; message?: string }) {\n  return requestClient.putWithMsg<void>(\n    '/workflow/instance/cancelProcessApply',\n    data,\n  );\n}\n\n/**\n * 激活/挂起流程实例\n * @param instanceId\n * @param active\n */\nexport function workflowInstanceActive(instanceId: ID, active: boolean) {\n  return requestClient.putWithMsg<void>(\n    `/workflow/instance/active/${instanceId}?active=${active}`,\n  );\n}\n\n/**\n * 获取当前登录人发起的流程实例\n * @param params\n * @returns PageResult<Flow>\n */\nexport function pageByCurrent(params?: PageQuery) {\n  return requestClient.get<PageResult<TaskInfo>>(\n    '/workflow/instance/pageByCurrent',\n    { params },\n  );\n}\n\n/**\n * 获取流程图，流程记录\n * @param businessId 业务标识\n * @returns 流程图，流程记录\n */\nexport function flowInfo(businessId: string) {\n  return requestClient.get<FlowInfoResponse>(\n    `/workflow/instance/flowHisTaskList/${businessId}`,\n  );\n}\n\n/**\n * 获取流程变量\n * @param instanceId\n * @returns Map<string,any>\n */\nexport function instanceVariable(instanceId: string) {\n  return requestClient.get<FlowInstanceVariableResp>(\n    `/workflow/instance/instanceVariable/${instanceId}`,\n  );\n}\n\n/**\n * 作废流程\n */\nexport function workflowInstanceInvalid(data: {\n  comment?: string;\n  id: string;\n}) {\n  return requestClient.postWithMsg<void>('/workflow/instance/invalid', data);\n}\n\n/**\n * 修改流程参数\n * @param data 参数\n * @param data.instanceId 实例ID\n * @param data.key 参数key\n * @param data.value 值\n * @returns void\n */\nexport function updateFlowVariable(data: {\n  instanceId: string;\n  key: string;\n  value: any;\n}) {\n  return requestClient.putWithMsg<void>(\n    '/workflow/instance/updateVariable',\n    data,\n  );\n}\n"
  },
  {
    "path": "apps/web-antd/src/api/workflow/instance/model.d.ts",
    "content": "export {};\n\nexport interface Flow {\n  id: string;\n  createTime: string;\n  updateTime: string;\n  tenantId: string;\n  delFlag: string;\n  definitionId: string;\n  flowName?: any;\n  instanceId: string;\n  taskId: string;\n  cooperateType: number;\n  cooperateTypeName: string;\n  businessId?: any;\n  nodeCode: string;\n  nodeName: string;\n  nodeType: number;\n  targetNodeCode: string;\n  targetNodeName: string;\n  approver: string;\n  approveName: string;\n  collaborator?: any;\n  permissionList?: any;\n  skipType: string;\n  flowStatus: string;\n  flowTaskStatus?: any;\n  flowStatusName?: any;\n  message: string;\n  ext: null | string;\n  createBy?: any;\n  formCustom: string;\n  formPath: string;\n  flowCode?: any;\n  version?: any;\n  runDuration: string;\n  nickName?: any;\n}\n\nexport interface FlowInfoResponse {\n  instanceId: string;\n  list: Flow[];\n}\n\nexport interface FlowInstanceVariableResp {\n  /**\n   * json字符串 流程变量\n   */\n  variable: string;\n  variableList: {\n    key: string;\n    value: any;\n  }[];\n}\n"
  },
  {
    "path": "apps/web-antd/src/api/workflow/spel/index.tsx",
    "content": "import type { Spel } from './model';\n\nimport type { ID, PageQuery, PageResult } from '#/api/common';\n\nimport { requestClient } from '#/api/request';\n\nexport function spelList(params?: PageQuery) {\n  return requestClient.get<PageResult<Spel>>('/workflow/spel/list', { params });\n}\n\nexport function spelInfo(id: ID) {\n  return requestClient.get<Spel>(`/workflow/spel/${id}`);\n}\n\nexport function spelAdd(data: Partial<Spel>) {\n  return requestClient.postWithMsg<Spel>('/workflow/spel', data);\n}\n\nexport function spelUpdate(data: Partial<Spel>) {\n  return requestClient.putWithMsg<Spel>('/workflow/spel', data);\n}\n\nexport function spelDelete(ids: ID[]) {\n  return requestClient.deleteWithMsg<Spel>(`/workflow/spel/${ids}`);\n}\n"
  },
  {
    "path": "apps/web-antd/src/api/workflow/spel/model.d.ts",
    "content": "export interface Spel {\n  id: number;\n  componentName: string;\n  methodName: string;\n  methodParams: string;\n  viewSpel: string;\n  status: string;\n  remark: string;\n  createTime: string;\n}\n"
  },
  {
    "path": "apps/web-antd/src/api/workflow/task/index.ts",
    "content": "import type {\n  CompleteTaskReqData,\n  NextNodeInfo,\n  StartWorkFlowReqData,\n  TaskInfo,\n  TaskOperationData,\n  TaskOperationType,\n} from './model';\n\nimport type { ID, IDS, PageQuery, PageResult } from '#/api/common';\n\nimport { requestClient } from '#/api/request';\n\n/**\n * 启动任务\n * @param data\n */\nexport function startWorkFlow(data: StartWorkFlowReqData) {\n  return requestClient.post<{\n    processInstanceId: string;\n    taskId: string;\n  }>('/workflow/task/startWorkFlow', data);\n}\n\n/**\n * 办理任务\n * @param data\n */\nexport function completeTask(data: CompleteTaskReqData) {\n  return requestClient.postWithMsg<void>('/workflow/task/completeTask', data);\n}\n\n/**\n * 查询当前用户的待办任务\n * @param params\n */\nexport function pageByTaskWait(params?: PageQuery) {\n  return requestClient.get<PageResult<TaskInfo>>(\n    '/workflow/task/pageByTaskWait',\n    { params },\n  );\n}\n\n/**\n * 查询当前用户的已办任务\n * @param params\n */\nexport function pageByTaskFinish(params?: PageQuery) {\n  return requestClient.get<PageResult<TaskInfo>>(\n    '/workflow/task/pageByTaskFinish',\n    { params },\n  );\n}\n\n/**\n * 查询所有待办任务\n * @param params\n */\nexport function pageByAllTaskWait(params?: PageQuery) {\n  return requestClient.get<PageResult<TaskInfo>>(\n    '/workflow/task/pageByAllTaskWait',\n    { params },\n  );\n}\n\n/**\n * 查询已办任务\n * @param params\n */\nexport function pageByAllTaskFinish(params?: PageQuery) {\n  return requestClient.get<PageResult<TaskInfo>>(\n    '/workflow/task/pageByAllTaskFinish',\n    { params },\n  );\n}\n\n/**\n * 查询当前用户的抄送\n * @param params\n */\nexport function pageByTaskCopy(params?: PageQuery) {\n  return requestClient.get<PageResult<TaskInfo>>(\n    '/workflow/task/pageByTaskCopy',\n    { params },\n  );\n}\n\n/**\n * 根据taskId查询代表任务\n * @param taskId 任务id\n * @returns info\n */\nexport function getTaskByTaskId(taskId: string) {\n  return requestClient.get<TaskInfo>(`/workflow/task/getTask/${taskId}`);\n}\n\n/**\n * 终止任务\n */\nexport function terminationTask(data: { comment?: string; taskId: string }) {\n  return requestClient.postWithMsg<void>(\n    '/workflow/task/terminationTask',\n    data,\n  );\n}\n\n/**\n * 任务操作\n * @param taskOperationData 参数\n * @param taskOperation 操作类型，委派 delegateTask、转办 transferTask、加签 addSignature、减签 reductionSignature\n */\nexport function taskOperation(\n  taskOperationData: TaskOperationData,\n  taskOperation: TaskOperationType,\n) {\n  return requestClient.postWithMsg<void>(\n    `/workflow/task/taskOperation/${taskOperation}`,\n    taskOperationData,\n  );\n}\n\n/**\n * 修改任务办理人\n * @param taskIdList 任务id\n * @param userId 办理人id\n */\nexport function updateAssignee(taskIdList: IDS, userId: ID) {\n  return requestClient.putWithMsg<void>(\n    `/workflow/task/updateAssignee/${userId}`,\n    taskIdList,\n  );\n}\n\n/**\n * 驳回审批\n * @param data 参数\n */\nexport function backProcess(data: any) {\n  return requestClient.postWithMsg<void>('/workflow/task/backProcess', data);\n}\n\n/**\n * 获取可驳回节点\n * @param taskId 任务ID\n * @param nodeCode 当前节点编码\n */\nexport function getBackTaskNode(taskId: string, nodeCode: string) {\n  return requestClient.get<{ nodeCode: string; nodeName: string }[]>(\n    `/workflow/task/getBackTaskNode/${taskId}/${nodeCode}`,\n  );\n}\n\n/**\n * 获取当前任务的所有办理人\n * @param taskId 任务id\n */\nexport function currentTaskAllUser(taskId: ID) {\n  return requestClient.get<any>(`/workflow/task/currentTaskAllUser/${taskId}`);\n}\n\n/**\n * 获取下一节点\n * @param data data\n * @param data.taskId taskId\n * @returns NextNodeInfo\n */\nexport function getNextNodeList(data: { taskId: string }) {\n  return requestClient.post<NextNodeInfo[]>(\n    '/workflow/task/getNextNodeList',\n    data,\n  );\n}\n"
  },
  {
    "path": "apps/web-antd/src/api/workflow/task/model.d.ts",
    "content": "export interface ButtonWithPermission {\n  code: string;\n  value: null | string;\n  show: boolean;\n}\n\nexport interface TaskInfo {\n  id: string;\n  categoryName: string;\n  createTime: string;\n  updateTime: string;\n  tenantId: string;\n  delFlag?: any;\n  definitionId: string;\n  instanceId: string;\n  flowName: string;\n  businessId: string;\n  nodeCode: string;\n  nodeName: string;\n  /**\n   * >5.5.0版本才有自定义标题\n   */\n  businessTitle?: string;\n  nodeType: number;\n  permissionList?: any;\n  userList?: any;\n  formCustom: string;\n  formPath: string;\n  flowCode: string;\n  version: string;\n  flowStatus: string;\n  flowStatusName: string;\n  assigneeIds: string;\n  assigneeNames: string;\n  processedBy: string;\n  type: string;\n  nodeRatio?: string;\n  createBy: string;\n  createByName: string;\n  targetNodeName?: string;\n  buttonList: ButtonWithPermission[];\n}\n\nexport interface CompleteTaskReqData {\n  messageType: string[];\n  flowCopyList: { userId: string; userName: string }[];\n  taskId: ID;\n  taskVariables: Record<string, any>;\n  variables: any;\n  // 附件ID 1,2,3,4形式\n  fileId?: string;\n  // 选人 key为节点code value为用户ID join(,)\n  assigneeMap: { [key: string]: string };\n}\n\nexport interface StartWorkFlowReqData {\n  /**\n   * 业务ID\n   */\n  businessId: ID;\n  /**\n   * flowCode\n   */\n  flowCode: string;\n  /**\n   * 流程变量\n   */\n  variables: Record<string, any>;\n  /**\n   * 流程实例业务扩展业务对象 必须要有不能为null 可以为空对象\n   */\n  flowInstanceBizExtBo: Record<string, any>;\n}\n\nexport interface TaskOperationData {\n  message?: string;\n  taskId: ID;\n  // 单个操作人\n  userId?: ID;\n  // 多个操作人\n  userIds?: IDS;\n}\n\n/**\n * 操作类型，委派 delegateTask、转办 transferTask、加签 addSignature、减签 reductionSignature\n */\nexport type TaskOperationType =\n  | 'addSignature'\n  | 'delegateTask'\n  | 'reductionSignature'\n  | 'transferTask';\n\nexport interface NextNodeInfo {\n  skipList: string[];\n  id: string;\n  createTime: string;\n  updateTime: string;\n  tenantId: string;\n  delFlag: string;\n  nodeType: number;\n  definitionId: string;\n  nodeCode: string;\n  nodeName: string;\n  permissionFlag: string;\n  nodeRatio: string;\n  coordinate: string;\n  version: string;\n  anyNodeSkip: any;\n  listenerType: any;\n  listenerPath: any;\n  handlerType: any;\n  handlerPath: any;\n  formCustom: string;\n  formPath: any;\n  ext: string;\n}\n"
  },
  {
    "path": "apps/web-antd/src/app.vue",
    "content": "<script lang=\"ts\" setup>\nimport { computed } from 'vue';\n\nimport { useAntdDesignTokens } from '@vben/hooks';\nimport { preferences, usePreferences } from '@vben/preferences';\n\nimport { App, ConfigProvider, theme } from 'ant-design-vue';\n\nimport { antdLocale } from '#/locales';\n\nimport { useUploadTip } from './upload-tip';\n\ndefineOptions({ name: 'App' });\n\nconst { isDark } = usePreferences();\nconst { tokens } = useAntdDesignTokens();\n\nconst tokenTheme = computed(() => {\n  const algorithm = isDark.value\n    ? [theme.darkAlgorithm]\n    : [theme.defaultAlgorithm];\n\n  // antd 紧凑模式算法\n  if (preferences.app.compact) {\n    algorithm.push(theme.compactAlgorithm);\n  }\n\n  return {\n    algorithm,\n    token: tokens,\n  };\n});\n\nuseUploadTip();\n</script>\n\n<template>\n  <ConfigProvider :locale=\"antdLocale\" :theme=\"tokenTheme\">\n    <App>\n      <RouterView />\n    </App>\n  </ConfigProvider>\n</template>\n\n<style lang=\"scss\">\nbody {\n  /**\n   * 全局启用 抗锯齿字体\n   * @see https://developer.mozilla.org/zh-CN/docs/Web/CSS/font-smooth\n   */\n  @apply antialiased;\n}\n</style>\n"
  },
  {
    "path": "apps/web-antd/src/bootstrap.ts",
    "content": "import { createApp, watchEffect } from 'vue';\n\nimport { registerAccessDirective } from '@vben/access';\nimport { registerLoadingDirective } from '@vben/common-ui/es/loading';\nimport { preferences } from '@vben/preferences';\nimport { initStores } from '@vben/stores';\nimport '@vben/styles';\nimport '@vben/styles/antd';\n\nimport { useTitle } from '@vueuse/core';\n\nimport { setupGlobalComponent } from '#/components/global';\nimport { $t, setupI18n } from '#/locales';\n\nimport { initComponentAdapter } from './adapter/component';\nimport { initSetupVbenForm } from './adapter/form';\nimport App from './app.vue';\nimport { router } from './router';\n\nasync function bootstrap(namespace: string) {\n  // 初始化组件适配器\n  await initComponentAdapter();\n\n  // 初始化表单组件\n  await initSetupVbenForm();\n\n  // // 设置弹窗的默认配置\n  // setDefaultModalProps({\n  //   fullscreenButton: false,\n  // });\n  // // 设置抽屉的默认配置\n  // setDefaultDrawerProps({\n  //   zIndex: 1020,\n  // });\n\n  const app = createApp(App);\n\n  // 全局组件\n  setupGlobalComponent(app);\n  // 注册v-loading指令\n  registerLoadingDirective(app, {\n    loading: 'loading', // 在这里可以自定义指令名称，也可以明确提供false表示不注册这个指令\n    spinning: 'spinning',\n  });\n\n  // 国际化 i18n 配置\n  await setupI18n(app);\n\n  // 配置 pinia-tore\n  await initStores(app, { namespace });\n\n  // 安装权限指令\n  registerAccessDirective(app);\n\n  // 初始化 tippy\n  const { initTippy } = await import('@vben/common-ui/es/tippy');\n  initTippy(app);\n\n  // 配置路由及路由守卫\n  app.use(router);\n\n  // 配置Motion插件\n  const { MotionPlugin } = await import('@vben/plugins/motion');\n  app.use(MotionPlugin);\n\n  // 动态更新标题\n  watchEffect(() => {\n    if (preferences.app.dynamicTitle) {\n      const routeTitle = router.currentRoute.value.meta?.title;\n      const pageTitle =\n        (routeTitle ? `${$t(routeTitle)} - ` : '') + preferences.app.name;\n      useTitle(pageTitle);\n    }\n  });\n\n  app.mount('#app');\n}\n\nexport { bootstrap };\n"
  },
  {
    "path": "apps/web-antd/src/components/cropper/index.ts",
    "content": "export { default as CropperAvatar } from './src/cropper-avatar.vue';\nexport { default as CropperImage } from './src/cropper.vue';\nexport type { Cropper } from './src/typing';\n"
  },
  {
    "path": "apps/web-antd/src/components/cropper/src/cropper-avatar.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { ButtonProps } from 'ant-design-vue';\n\nimport type { CSSProperties, PropType } from 'vue';\n\nimport { computed, ref, unref, watch, watchEffect } from 'vue';\n\nimport { useVbenModal } from '@vben/common-ui';\nimport { $t as t } from '@vben/locales';\n\nimport { message } from 'ant-design-vue';\n\nimport cropperModal from './cropper-modal.vue';\n\ndefineOptions({ name: 'CropperAvatar' });\n\nconst props = defineProps({\n  btnProps: { default: () => ({}), type: Object as PropType<ButtonProps> },\n  btnText: { default: '', type: String },\n  showBtn: { default: true, type: Boolean },\n  size: { default: 5, type: Number },\n  uploadApi: {\n    required: true,\n    type: Function as PropType<\n      ({\n        file,\n        filename,\n        name,\n      }: {\n        file: Blob;\n        filename: string;\n        name: string;\n      }) => Promise<any>\n    >,\n  },\n  value: { default: '', type: String },\n\n  width: { default: '200px', type: [String, Number] },\n});\n\nconst emit = defineEmits(['update:value', 'change']);\n\nconst sourceValue = ref(props.value || '');\nconst prefixCls = 'cropper-avatar';\nconst [CropperModal, modalApi] = useVbenModal({\n  connectedComponent: cropperModal,\n});\n\nconst getClass = computed(() => [prefixCls]);\n\nconst getWidth = computed(() => `${`${props.width}`.replace(/px/, '')}px`);\n\nconst getIconWidth = computed(\n  () => `${Number.parseInt(`${props.width}`.replace(/px/, '')) / 2}px`,\n);\n\nconst getStyle = computed((): CSSProperties => ({ width: unref(getWidth) }));\n\nconst getImageWrapperStyle = computed(\n  (): CSSProperties => ({ height: unref(getWidth), width: unref(getWidth) }),\n);\n\nwatchEffect(() => {\n  sourceValue.value = props.value || '';\n});\n\nwatch(\n  () => sourceValue.value,\n  (v: string) => {\n    emit('update:value', v);\n  },\n);\n\nfunction handleUploadSuccess({ data, source }: any) {\n  sourceValue.value = source;\n  emit('change', { data, source });\n  message.success(t('component.cropper.uploadSuccess'));\n}\n\nconst closeModal = () => modalApi.close();\nconst openModal = () => modalApi.open();\n\ndefineExpose({\n  closeModal,\n  openModal,\n});\n</script>\n<template>\n  <div :class=\"getClass\" :style=\"getStyle\">\n    <div\n      :class=\"`${prefixCls}-image-wrapper`\"\n      :style=\"getImageWrapperStyle\"\n      @click=\"openModal\"\n    >\n      <div :class=\"`${prefixCls}-image-mask`\" :style=\"getImageWrapperStyle\">\n        <span\n          :style=\"{\n            ...getImageWrapperStyle,\n            width: `${getIconWidth}`,\n            height: `${getIconWidth}`,\n            lineHeight: `${getIconWidth}`,\n          }\"\n          class=\"icon-[ant-design--cloud-upload-outlined] text-[#d6d6d6]\"\n        ></span>\n      </div>\n      <img v-if=\"sourceValue\" :src=\"sourceValue\" alt=\"avatar\" />\n    </div>\n    <a-button\n      v-if=\"showBtn\"\n      :class=\"`${prefixCls}-upload-btn`\"\n      @click=\"openModal\"\n      v-bind=\"btnProps\"\n    >\n      {{ btnText ? btnText : t('component.cropper.selectImage') }}\n    </a-button>\n\n    <CropperModal\n      :size=\"size\"\n      :src=\"sourceValue\"\n      :upload-api=\"uploadApi\"\n      @upload-success=\"handleUploadSuccess\"\n    />\n  </div>\n</template>\n\n<style lang=\"scss\" scoped>\n.cropper-avatar {\n  display: inline-block;\n  text-align: center;\n\n  &-image-wrapper {\n    overflow: hidden;\n    cursor: pointer;\n    background: #fff;\n    border: 1px solid #eee;\n    border-radius: 50%;\n\n    img {\n      width: 100%;\n    }\n  }\n\n  &-image-mask {\n    position: absolute;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    width: inherit;\n    height: inherit;\n    cursor: pointer;\n    background: rgb(0 0 0 / 40%);\n    border: inherit;\n    border-radius: inherit;\n    opacity: 0;\n    transition: opacity 0.4s;\n\n    ::v-deep(svg) {\n      margin: auto;\n    }\n  }\n\n  &-image-mask:hover {\n    opacity: 40;\n  }\n\n  &-upload-btn {\n    margin: 10px auto;\n  }\n}\n</style>\n"
  },
  {
    "path": "apps/web-antd/src/components/cropper/src/cropper-modal.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { PropType } from 'vue';\n\nimport type { CropendResult, Cropper } from './typing';\n\nimport { ref } from 'vue';\n\nimport { useVbenModal } from '@vben/common-ui';\nimport { $t as t } from '@vben/locales';\n\nimport { Avatar, message, Space, Tooltip, Upload } from 'ant-design-vue';\nimport { isFunction } from 'lodash-es';\n\nimport { dataURLtoBlob } from '#/utils/file/base64Conver';\n\nimport CropperImage from './cropper.vue';\n\ntype apiFunParams = { file: Blob; filename: string; name: string };\n\ndefineOptions({ name: 'CropperModal' });\n\nconst props = defineProps({\n  circled: { default: true, type: Boolean },\n  size: { default: 0, type: Number },\n  src: { default: '', type: String },\n  uploadApi: {\n    required: true,\n    type: Function as PropType<(params: apiFunParams) => Promise<any>>,\n  },\n});\n\nconst emit = defineEmits(['uploadSuccess', 'uploadError', 'register']);\n\nlet filename = '';\nconst src = ref(props.src || '');\nconst previewSource = ref('');\nconst cropper = ref<Cropper>();\nlet scaleX = 1;\nlet scaleY = 1;\n\nconst prefixCls = 'cropper-am';\nconst [BasicModal, modalApi] = useVbenModal({\n  onConfirm: handleOk,\n  onOpenChange(isOpen) {\n    // 打开的时候loading CropperImage组件加载完毕关闭loading\n    if (isOpen) {\n      modalLoading(true);\n    } else {\n      // 关闭时候清空右侧预览\n      previewSource.value = '';\n      modalLoading(false);\n    }\n  },\n});\n\nfunction modalLoading(loading: boolean) {\n  modalApi.setState({ confirmLoading: loading, loading });\n}\n\n// Block upload\nfunction handleBeforeUpload(file: File) {\n  if (props.size > 0 && file.size > 1024 * 1024 * props.size) {\n    emit('uploadError', { msg: t('component.cropper.imageTooBig') });\n    return false;\n  }\n  const reader = new FileReader();\n  reader.readAsDataURL(file);\n  src.value = '';\n  previewSource.value = '';\n  reader.addEventListener('load', (e) => {\n    src.value = (e.target?.result as string) ?? '';\n    filename = file.name;\n  });\n  return false;\n}\n\nfunction handleCropend({ imgBase64 }: CropendResult) {\n  previewSource.value = imgBase64;\n}\n\nfunction handleReady(cropperInstance: Cropper) {\n  cropper.value = cropperInstance;\n  // 画布加载完毕 关闭loading\n  modalLoading(false);\n}\n\nfunction handleReadyError() {\n  modalLoading(false);\n}\n\nfunction handlerToolbar(event: string, arg?: number) {\n  if (event === 'scaleX') {\n    scaleX = arg = scaleX === -1 ? 1 : -1;\n  }\n  if (event === 'scaleY') {\n    scaleY = arg = scaleY === -1 ? 1 : -1;\n  }\n  (cropper?.value as any)?.[event]?.(arg);\n}\n\nasync function handleOk() {\n  const uploadApi = props.uploadApi;\n  if (uploadApi && isFunction(uploadApi)) {\n    if (!previewSource.value) {\n      message.warn('未选择图片');\n      return;\n    }\n    const blob = dataURLtoBlob(previewSource.value);\n    try {\n      modalLoading(true);\n      const result = await uploadApi({ file: blob, filename, name: 'file' });\n      emit('uploadSuccess', { data: result.url, source: previewSource.value });\n      modalApi.close();\n    } finally {\n      modalLoading(false);\n    }\n  }\n}\n</script>\n<template>\n  <BasicModal\n    v-bind=\"$attrs\"\n    :confirm-text=\"t('component.cropper.okText')\"\n    :fullscreen-button=\"false\"\n    :title=\"t('component.cropper.modalTitle')\"\n    class=\"w-[800px]\"\n  >\n    <div :class=\"prefixCls\">\n      <div :class=\"`${prefixCls}-left`\" class=\"w-full\">\n        <div :class=\"`${prefixCls}-cropper`\">\n          <CropperImage\n            v-if=\"src\"\n            :circled=\"circled\"\n            :src=\"src\"\n            crossorigin=\"anonymous\"\n            height=\"300px\"\n            @cropend=\"handleCropend\"\n            @ready=\"handleReady\"\n            @ready-error=\"handleReadyError\"\n          />\n        </div>\n\n        <div :class=\"`${prefixCls}-toolbar`\">\n          <Upload\n            :before-upload=\"handleBeforeUpload\"\n            :file-list=\"[]\"\n            accept=\"image/*\"\n          >\n            <Tooltip\n              :title=\"t('component.cropper.selectImage')\"\n              placement=\"bottom\"\n            >\n              <a-button size=\"small\" type=\"primary\">\n                <template #icon>\n                  <div class=\"flex items-center justify-center\">\n                    <span class=\"icon-[ant-design--upload-outlined]\"></span>\n                  </div>\n                </template>\n              </a-button>\n            </Tooltip>\n          </Upload>\n          <Space>\n            <Tooltip\n              :title=\"t('component.cropper.btn_reset')\"\n              placement=\"bottom\"\n            >\n              <a-button\n                :disabled=\"!src\"\n                size=\"small\"\n                type=\"primary\"\n                @click=\"handlerToolbar('reset')\"\n              >\n                <template #icon>\n                  <div class=\"flex items-center justify-center\">\n                    <span class=\"icon-[ant-design--reload-outlined]\"></span>\n                  </div>\n                </template>\n              </a-button>\n            </Tooltip>\n            <Tooltip\n              :title=\"t('component.cropper.btn_rotate_left')\"\n              placement=\"bottom\"\n            >\n              <a-button\n                :disabled=\"!src\"\n                size=\"small\"\n                type=\"primary\"\n                @click=\"handlerToolbar('rotate', -45)\"\n              >\n                <template #icon>\n                  <div class=\"flex items-center justify-center\">\n                    <span\n                      class=\"icon-[ant-design--rotate-left-outlined]\"\n                    ></span>\n                  </div>\n                </template>\n              </a-button>\n            </Tooltip>\n            <Tooltip\n              :title=\"t('component.cropper.btn_rotate_right')\"\n              placement=\"bottom\"\n            >\n              <a-button\n                :disabled=\"!src\"\n                pre-icon=\"ant-design:rotate-right-outlined\"\n                size=\"small\"\n                type=\"primary\"\n                @click=\"handlerToolbar('rotate', 45)\"\n              >\n                <template #icon>\n                  <div class=\"flex items-center justify-center\">\n                    <span\n                      class=\"icon-[ant-design--rotate-right-outlined]\"\n                    ></span>\n                  </div>\n                </template>\n              </a-button>\n            </Tooltip>\n            <Tooltip\n              :title=\"t('component.cropper.btn_scale_x')\"\n              placement=\"bottom\"\n            >\n              <a-button\n                :disabled=\"!src\"\n                size=\"small\"\n                type=\"primary\"\n                @click=\"handlerToolbar('scaleX')\"\n              >\n                <template #icon>\n                  <div class=\"flex items-center justify-center\">\n                    <span class=\"icon-[vaadin--arrows-long-h]\"></span>\n                  </div>\n                </template>\n              </a-button>\n            </Tooltip>\n            <Tooltip\n              :title=\"t('component.cropper.btn_scale_y')\"\n              placement=\"bottom\"\n            >\n              <a-button\n                :disabled=\"!src\"\n                size=\"small\"\n                type=\"primary\"\n                @click=\"handlerToolbar('scaleY')\"\n              >\n                <template #icon>\n                  <div class=\"flex items-center justify-center\">\n                    <span class=\"icon-[vaadin--arrows-long-v]\"></span>\n                  </div>\n                </template>\n              </a-button>\n            </Tooltip>\n            <Tooltip\n              :title=\"t('component.cropper.btn_zoom_in')\"\n              placement=\"bottom\"\n            >\n              <a-button\n                :disabled=\"!src\"\n                size=\"small\"\n                type=\"primary\"\n                @click=\"handlerToolbar('zoom', 0.1)\"\n              >\n                <template #icon>\n                  <div class=\"flex items-center justify-center\">\n                    <span class=\"icon-[ant-design--zoom-in-outlined]\"></span>\n                  </div>\n                </template>\n              </a-button>\n            </Tooltip>\n            <Tooltip\n              :title=\"t('component.cropper.btn_zoom_out')\"\n              placement=\"bottom\"\n            >\n              <a-button\n                :disabled=\"!src\"\n                size=\"small\"\n                type=\"primary\"\n                @click=\"handlerToolbar('zoom', -0.1)\"\n              >\n                <template #icon>\n                  <div class=\"flex items-center justify-center\">\n                    <span class=\"icon-[ant-design--zoom-out-outlined]\"></span>\n                  </div>\n                </template>\n              </a-button>\n            </Tooltip>\n          </Space>\n        </div>\n      </div>\n      <div :class=\"`${prefixCls}-right`\">\n        <div :class=\"`${prefixCls}-preview`\">\n          <img\n            v-if=\"previewSource\"\n            :alt=\"t('component.cropper.preview')\"\n            :src=\"previewSource\"\n          />\n        </div>\n        <template v-if=\"previewSource\">\n          <div :class=\"`${prefixCls}-group`\">\n            <Avatar :src=\"previewSource\" size=\"large\" />\n            <Avatar :size=\"48\" :src=\"previewSource\" />\n            <Avatar :size=\"64\" :src=\"previewSource\" />\n            <Avatar :size=\"80\" :src=\"previewSource\" />\n          </div>\n        </template>\n      </div>\n    </div>\n  </BasicModal>\n</template>\n\n<style lang=\"scss\">\n.cropper-am {\n  display: flex;\n\n  &-left,\n  &-right {\n    height: 340px;\n  }\n\n  &-left {\n    width: 55%;\n  }\n\n  &-right {\n    width: 45%;\n  }\n\n  &-cropper {\n    height: 300px;\n    background: #eee;\n    background-image:\n      linear-gradient(\n        45deg,\n        rgb(0 0 0 / 25%) 25%,\n        transparent 0,\n        transparent 75%,\n        rgb(0 0 0 / 25%) 0\n      ),\n      linear-gradient(\n        45deg,\n        rgb(0 0 0 / 25%) 25%,\n        transparent 0,\n        transparent 75%,\n        rgb(0 0 0 / 25%) 0\n      );\n    background-position:\n      0 0,\n      12px 12px;\n    background-size: 24px 24px;\n  }\n\n  &-toolbar {\n    display: flex;\n    align-items: center;\n    justify-content: space-between;\n    margin-top: 10px;\n  }\n\n  &-preview {\n    width: 220px;\n    height: 220px;\n    margin: 0 auto;\n    overflow: hidden;\n    border: 1px solid #eee;\n    border-radius: 50%;\n\n    img {\n      width: 100%;\n      height: 100%;\n    }\n  }\n\n  &-group {\n    display: flex;\n    align-items: center;\n    justify-content: space-around;\n    padding-top: 8px;\n    margin-top: 8px;\n    border-top: 1px solid #eee;\n  }\n}\n</style>\n"
  },
  {
    "path": "apps/web-antd/src/components/cropper/src/cropper.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { CSSProperties, PropType } from 'vue';\n\nimport { computed, onMounted, onUnmounted, ref, unref, useAttrs } from 'vue';\n\nimport { useDebounceFn } from '@vueuse/core';\nimport Cropper from 'cropperjs';\n\nimport 'cropperjs/dist/cropper.css';\n\ntype Options = Cropper.Options;\n\ndefineOptions({ name: 'CropperImage' });\n\nconst props = defineProps({\n  alt: { default: '', type: String },\n  circled: { default: false, type: Boolean },\n  crossorigin: {\n    default: undefined,\n    type: String as PropType<'' | 'anonymous' | 'use-credentials' | undefined>,\n  },\n  height: { default: '360px', type: [String, Number] },\n  imageStyle: { default: () => ({}), type: Object as PropType<CSSProperties> },\n  options: { default: () => ({}), type: Object as PropType<Options> },\n  realTimePreview: { default: true, type: Boolean },\n  src: { required: true, type: String },\n});\n\nconst emit = defineEmits(['cropend', 'ready', 'cropendError', 'readyError']);\n\nconst defaultOptions: Options = {\n  aspectRatio: 1,\n  autoCrop: true,\n  background: true,\n  center: true,\n  // 需要设置为false 否则会自动拼接timestamp 导致私有桶sign错误\n  // 需要配合img crossorigin='anonymous'使用(默认已经做了处理)\n  checkCrossOrigin: false,\n  checkOrientation: true,\n  cropBoxMovable: true,\n  cropBoxResizable: true,\n  guides: true,\n  highlight: true,\n  modal: true,\n  movable: true,\n  responsive: true,\n  restore: true,\n  rotatable: true,\n  scalable: true,\n  toggleDragModeOnDblclick: true,\n  zoomable: true,\n  zoomOnTouch: true,\n  zoomOnWheel: true,\n};\n\nconst attrs = useAttrs();\n\ntype ElRef<T extends HTMLElement = HTMLDivElement> = null | T;\nconst imgElRef = ref<ElRef<HTMLImageElement>>();\nconst cropper = ref<Cropper | null>();\nconst isReady = ref(false);\n\nconst prefixCls = 'cropper-image';\nconst debounceRealTimeCroppered = useDebounceFn(realTimeCroppered, 80);\n\nconst getImageStyle = computed((): CSSProperties => {\n  return {\n    height: props.height,\n    maxWidth: '100%',\n    ...props.imageStyle,\n  };\n});\n\nconst getClass = computed(() => {\n  return [\n    prefixCls,\n    attrs.class,\n    {\n      [`${prefixCls}--circled`]: props.circled,\n    },\n  ];\n});\n\nconst getWrapperStyle = computed((): CSSProperties => {\n  return { height: `${`${props.height}`.replace(/px/, '')}px` };\n});\n\nonMounted(init);\n\nonUnmounted(() => {\n  cropper.value?.destroy();\n});\n\nasync function init() {\n  const imgEl = unref(imgElRef);\n  if (!imgEl) {\n    return;\n  }\n  // 判断是否为正常访问的图片\n  try {\n    const resp = await fetch(props.src);\n    if (resp.status !== 200) {\n      emit('readyError');\n    }\n  } catch {\n    emit('readyError');\n  }\n  cropper.value = new Cropper(imgEl, {\n    ...defaultOptions,\n    crop() {\n      debounceRealTimeCroppered();\n    },\n    cropmove() {\n      debounceRealTimeCroppered();\n    },\n    ready: () => {\n      isReady.value = true;\n      realTimeCroppered();\n      emit('ready', cropper.value);\n    },\n    zoom() {\n      debounceRealTimeCroppered();\n    },\n    ...props.options,\n  });\n}\n\n// Real-time display preview\nfunction realTimeCroppered() {\n  props.realTimePreview && croppered();\n}\n\n// event: return base64 and width and height information after cropping\nfunction croppered() {\n  if (!cropper.value) {\n    return;\n  }\n  const imgInfo = cropper.value.getData();\n  const canvas = props.circled\n    ? getRoundedCanvas()\n    : cropper.value.getCroppedCanvas();\n  canvas.toBlob((blob) => {\n    if (!blob) {\n      return;\n    }\n    const fileReader: FileReader = new FileReader();\n    fileReader.readAsDataURL(blob);\n    fileReader.onloadend = (e) => {\n      emit('cropend', {\n        imgBase64: e.target?.result ?? '',\n        imgInfo,\n      });\n    };\n    // eslint-disable-next-line unicorn/prefer-add-event-listener\n    fileReader.onerror = () => {\n      emit('cropendError');\n    };\n  }, 'image/png');\n}\n\n// Get a circular picture canvas\nfunction getRoundedCanvas() {\n  const sourceCanvas = cropper.value!.getCroppedCanvas();\n  const canvas = document.createElement('canvas');\n  const context = canvas.getContext('2d')!;\n  const width = sourceCanvas.width;\n  const height = sourceCanvas.height;\n  canvas.width = width;\n  canvas.height = height;\n  context.imageSmoothingEnabled = true;\n  context.drawImage(sourceCanvas, 0, 0, width, height);\n  context.globalCompositeOperation = 'destination-in';\n  context.beginPath();\n  context.arc(\n    width / 2,\n    height / 2,\n    Math.min(width, height) / 2,\n    0,\n    2 * Math.PI,\n    true,\n  );\n  context.fill();\n  return canvas;\n}\n</script>\n<template>\n  <div :class=\"getClass\" :style=\"getWrapperStyle\">\n    <img\n      v-show=\"isReady\"\n      ref=\"imgElRef\"\n      :alt=\"alt\"\n      :crossorigin=\"crossorigin\"\n      :src=\"src\"\n      :style=\"getImageStyle\"\n    />\n  </div>\n</template>\n<style lang=\"scss\">\n.cropper-image {\n  &--circled {\n    .cropper-view-box,\n    .cropper-face {\n      border-radius: 50%;\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "apps/web-antd/src/components/cropper/src/typing.ts",
    "content": "import type Cropper from 'cropperjs';\n\nexport interface CropendResult {\n  imgBase64: string;\n  imgInfo: Cropper.Data;\n}\n\nexport type { Cropper };\n"
  },
  {
    "path": "apps/web-antd/src/components/description/index.ts",
    "content": "export { default as Description } from './src/description.vue';\nexport * from './src/typing';\nexport { useDescription } from './src/useDescription';\n"
  },
  {
    "path": "apps/web-antd/src/components/description/src/description.vue",
    "content": "<script lang=\"tsx\">\nimport type { CardSize } from 'ant-design-vue/es/card/Card';\nimport type { DescriptionsProps } from 'ant-design-vue/es/descriptions';\n\nimport type { CSSProperties, PropType, Slots } from 'vue';\n\nimport type { DescInstance, DescItem, DescriptionProps } from './typing';\n\nimport { computed, defineComponent, ref, toRefs, unref, useAttrs } from 'vue';\n\nimport { Card, Descriptions } from 'ant-design-vue';\nimport { get, isFunction } from 'lodash-es';\n\nconst props = {\n  bordered: { default: true, type: Boolean },\n  column: {\n    default: () => {\n      return { lg: 3, md: 3, sm: 2, xl: 3, xs: 1, xxl: 4 };\n    },\n    type: [Number, Object],\n  },\n  data: { type: Object },\n  schema: {\n    default: () => [],\n    type: Array as PropType<DescItem[]>,\n  },\n  size: {\n    default: 'small',\n    type: String,\n    validator: (v: string) =>\n      ['default', 'middle', 'small', undefined].includes(v),\n  },\n  title: { default: '', type: String },\n  useCollapse: { default: true, type: Boolean },\n};\n\n/**\n * @deprecated 使用antd原生组件替代 下个版本将会移除\n */\nexport default defineComponent({\n  emits: ['register'],\n  // eslint-disable-next-line vue/order-in-components\n  name: 'Description',\n  // eslint-disable-next-line vue/order-in-components\n  props,\n  setup(props, { emit, slots }) {\n    const propsRef = ref<null | Partial<DescriptionProps>>(null);\n\n    const prefixCls = 'description';\n    const attrs = useAttrs();\n\n    // Custom title component: get title\n    const getMergeProps = computed(() => {\n      return {\n        ...props,\n        ...(unref(propsRef) as any),\n      } as DescriptionProps;\n    });\n\n    const getProps = computed(() => {\n      const opt = {\n        ...unref(getMergeProps),\n        title: undefined,\n      };\n      return opt as DescriptionProps;\n    });\n\n    /**\n     * @description: Whether to setting title\n     */\n    const useWrapper = computed(() => !!unref(getMergeProps).title);\n\n    const getDescriptionsProps = computed(() => {\n      return { ...unref(attrs), ...unref(getProps) } as DescriptionsProps;\n    });\n\n    /**\n     * @description:设置desc\n     */\n    function setDescProps(descProps: Partial<DescriptionProps>): void {\n      // Keep the last setDrawerProps\n      propsRef.value = {\n        ...(unref(propsRef) as Record<string, any>),\n        ...descProps,\n      } as Record<string, any>;\n    }\n\n    // Prevent line breaks\n    function renderLabel({ label, labelMinWidth, labelStyle }: DescItem) {\n      if (!labelStyle && !labelMinWidth) {\n        return label;\n      }\n\n      const labelStyles: CSSProperties = {\n        ...labelStyle,\n        minWidth: `${labelMinWidth}px `,\n      };\n      return <div style={labelStyles}>{label}</div>;\n    }\n\n    function renderItem() {\n      const { data, schema } = unref(getProps);\n      return unref(schema)\n        .map((item) => {\n          const { contentMinWidth, field, render, show, span } = item;\n\n          if (show && isFunction(show) && !show(data)) {\n            return null;\n          }\n\n          const getContent = () => {\n            const _data = unref(getProps)?.data;\n            if (!_data) {\n              return null;\n            }\n            const getField = get(_data, field);\n            // eslint-disable-next-line no-prototype-builtins\n            if (getField && !toRefs(_data).hasOwnProperty(field)) {\n              return isFunction(render) ? render!('', _data) : '';\n            }\n            return isFunction(render)\n              ? render!(getField, _data)\n              : (getField ?? '');\n          };\n\n          const width = contentMinWidth;\n          return (\n            <Descriptions.Item\n              key={field}\n              label={renderLabel(item)}\n              span={span}\n            >\n              {() => {\n                if (!contentMinWidth) {\n                  return getContent();\n                }\n                const style: CSSProperties = {\n                  minWidth: `${width}px`,\n                };\n                return <div style={style}>{getContent()}</div>;\n              }}\n            </Descriptions.Item>\n          );\n        })\n        .filter((item) => !!item);\n    }\n\n    const renderDesc = () => {\n      return (\n        <Descriptions\n          class={`${prefixCls}`}\n          {...(unref(getDescriptionsProps) as any)}\n        >\n          {renderItem()}\n        </Descriptions>\n      );\n    };\n\n    const renderContainer = () => {\n      const content = props.useCollapse ? (\n        renderDesc()\n      ) : (\n        <div>{renderDesc()}</div>\n      );\n      // Reduce the dom level\n      if (!props.useCollapse) {\n        return content;\n      }\n\n      // const { canExpand, helpMessage } = unref(getCollapseOptions);\n      const { title } = unref(getMergeProps);\n\n      function getSlot(slots: Slots, slot = 'default', data?: any) {\n        if (!slots || !Reflect.has(slots, slot)) {\n          return null;\n        }\n        if (!isFunction(slots[slot])) {\n          console.error(`${slot} is not a function!`);\n          return null;\n        }\n        const slotFn = slots[slot];\n        if (!slotFn) return null;\n        const params = { ...data };\n        return slotFn(params);\n      }\n\n      return (\n        <Card size={props.size as CardSize} title={title}>\n          {{\n            default: () => content,\n            extra: () => getSlot(slots, 'extra'),\n          }}\n        </Card>\n      );\n    };\n\n    const methods: DescInstance = {\n      setDescProps,\n    };\n\n    emit('register', methods);\n    return () => (unref(useWrapper) ? renderContainer() : renderDesc());\n  },\n});\n</script>\n"
  },
  {
    "path": "apps/web-antd/src/components/description/src/typing.ts",
    "content": "import type { DescriptionsProps } from 'ant-design-vue/es/descriptions';\nimport type { JSX } from 'vue/jsx-runtime';\n\nimport type { CSSProperties, VNode } from 'vue';\n\nimport type { Recordable } from '@vben/types';\n\nexport interface DescItem {\n  labelMinWidth?: number;\n  contentMinWidth?: number;\n  labelStyle?: CSSProperties;\n  field: string;\n  label: JSX.Element | string | VNode;\n  // Merge column\n  span?: number;\n  show?: (...arg: any) => boolean;\n  // render\n  render?: (\n    val: any,\n    data: Recordable<any>,\n  ) => Element | JSX.Element | number | string | undefined | VNode;\n}\n\nexport interface DescriptionProps extends DescriptionsProps {\n  // Whether to include the collapse component\n  useCollapse?: boolean;\n  /**\n   * item configuration\n   * @type DescItem\n   */\n  schema: DescItem[];\n  /**\n   * 数据\n   * @type object\n   */\n  data: Recordable<any>;\n}\n\nexport interface DescInstance {\n  setDescProps(descProps: Partial<DescriptionProps>, delay?: boolean): void;\n}\n\nexport type Register = (descInstance: DescInstance) => void;\n\n/**\n * @description:\n */\nexport type UseDescReturnType = [Register, DescInstance];\n"
  },
  {
    "path": "apps/web-antd/src/components/description/src/useDescription.ts",
    "content": "import type {\n  DescInstance,\n  DescriptionProps,\n  UseDescReturnType,\n} from './typing';\n\nimport { getCurrentInstance, ref, unref } from 'vue';\n\n/**\n * @deprecated 使用antd原生组件替代 下个版本将会移除\n */\nexport function useDescription(\n  props?: Partial<DescriptionProps>,\n): UseDescReturnType {\n  if (!getCurrentInstance()) {\n    throw new Error(\n      'useDescription() can only be used inside setup() or functional components!',\n    );\n  }\n  const desc = ref<DescInstance | null>(null);\n  const loaded = ref(false);\n\n  function register(instance: DescInstance) {\n    // if (unref(loaded) && import.meta.env.PROD) {\n    //   return;\n    // }\n    desc.value = instance;\n    props && instance.setDescProps(props);\n    loaded.value = true;\n  }\n\n  const methods: DescInstance = {\n    setDescProps: (\n      descProps: Partial<DescriptionProps>,\n      delay = false,\n    ): void => {\n      if (!delay) {\n        unref(desc)?.setDescProps(descProps);\n        return;\n      }\n      // 奇怪的问题 在modal中需要setTimeout才会生效\n      setTimeout(() => unref(desc)?.setDescProps(descProps));\n    },\n  };\n\n  return [register, methods];\n}\n"
  },
  {
    "path": "apps/web-antd/src/components/dict/index.ts",
    "content": "export { tagSelectOptions, tagTypes } from './src/data';\nexport { default as DictTag } from './src/index.vue';\n"
  },
  {
    "path": "apps/web-antd/src/components/dict/src/data.tsx",
    "content": "import type { VNode } from 'vue';\n\nimport { Tag } from 'ant-design-vue';\n\ninterface TagType {\n  [key: string]: { color: string; label: string };\n}\n\nexport const tagTypes: TagType = {\n  cyan: { color: 'cyan', label: 'cyan' },\n  danger: { color: 'error', label: '危险(danger)' },\n  /** 由于和elementUI不同 用于替换颜色 */\n  default: { color: 'default', label: '默认(default)' },\n  green: { color: 'green', label: 'green' },\n  info: { color: 'default', label: '信息(info)' },\n  orange: { color: 'orange', label: 'orange' },\n  /** 自定义预设 color可以为16进制颜色 */\n  pink: { color: 'pink', label: 'pink' },\n  primary: { color: 'processing', label: '主要(primary)' },\n  purple: { color: 'purple', label: 'purple' },\n  red: { color: 'red', label: 'red' },\n  success: { color: 'success', label: '成功(success)' },\n  warning: { color: 'warning', label: '警告(warning)' },\n};\n\n// 字典选择使用 { label: string; value: string }[]\ninterface Options {\n  label: string | VNode;\n  value: string;\n}\n\nexport function tagSelectOptions() {\n  const selectArray: Options[] = [];\n  Object.keys(tagTypes).forEach((key) => {\n    if (!tagTypes[key]) return;\n    const label = tagTypes[key].label;\n    const color = tagTypes[key].color;\n    selectArray.push({\n      label: <Tag color={color}>{label}</Tag>,\n      value: key,\n    });\n  });\n  return selectArray;\n}\n"
  },
  {
    "path": "apps/web-antd/src/components/dict/src/index.vue",
    "content": "<!-- eslint-disable eqeqeq -->\n<script lang=\"tsx\">\nimport type { PropType } from 'vue';\n\nimport type { DictFallback } from './type';\n\nimport type { DictData } from '#/api/system/dict/dict-data-model';\n\nimport { computed, defineComponent, h, isVNode } from 'vue';\n\nimport { Spin, Tag } from 'ant-design-vue';\nimport { isFunction, isString } from 'lodash-es';\n\nimport { tagTypes } from './data';\n\n/**\n * 使用tsx重构原来的template写法\n * 在大量if的情况 tsx比template的v-if好用得多\n */\nexport default defineComponent({\n  name: 'DictTag',\n  props: {\n    /**\n     * 字典项options\n     */\n    dicts: {\n      required: false,\n      type: Array as PropType<DictData[]>,\n      default: () => [],\n    },\n    /**\n     * 当前值\n     */\n    value: {\n      required: true,\n      type: [Number, String],\n    },\n    /**\n     * 未匹配到字典项的fallback\n     */\n    fallback: {\n      required: false,\n      type: [String, Function] as PropType<DictFallback>,\n      default: 'unknown',\n    },\n  },\n  setup(props) {\n    const color = computed<string>(() => {\n      const current = props.dicts.find((item) => item.dictValue == props.value);\n      const listClass = current?.listClass ?? '';\n      // 是否为默认的颜色\n      const isDefault = Reflect.has(tagTypes, listClass);\n      // 判断是默认还是自定义颜色\n      if (isDefault) {\n        // 这里做了antd - element-plus的兼容\n        return tagTypes[listClass]!.color;\n      }\n      return listClass;\n    });\n\n    const cssClass = computed<string>(() => {\n      const current = props.dicts.find((item) => item.dictValue == props.value);\n      return current?.cssClass ?? '';\n    });\n\n    /**\n     * 返回null 走 fallback逻辑\n     */\n    const label = computed<null | string>(() => {\n      const current = props.dicts.find((item) => item.dictValue == props.value);\n      return current?.dictLabel ?? null;\n    });\n\n    const loading = computed(() => {\n      return props.dicts?.length === 0;\n    });\n\n    return {\n      color,\n      cssClass,\n      label,\n      loading,\n    };\n  },\n  render() {\n    const { color, cssClass, label, loading, fallback, value, $slots } = this;\n\n    /**\n     * 字典list为0 加载中\n     */\n    if (loading) {\n      return (\n        <div>\n          <Spin size=\"small\" spinning />\n        </div>\n      );\n    }\n\n    /**\n     * 没有匹配到字典（label === null）的fallback\n     * 可为string/Vnode\n     */\n    if (label === null) {\n      // 优先返回slot\n      if ($slots.fallback) {\n        return $slots.fallback(value);\n      }\n      // VNode / String\n      if (isFunction(fallback)) {\n        const rValue = fallback(value);\n        if (isVNode(rValue)) {\n          return h(rValue);\n        }\n        return <div>{rValue}</div>;\n      }\n      // 默认显示 unknown 文案\n      if (isString(fallback)) {\n        return <div>{fallback}</div>;\n      }\n    }\n\n    /**\n     * 有color 属性 渲染Tag\n     */\n    if (color) {\n      return (\n        <div>\n          <Tag class={cssClass} color={color}>\n            {label}\n          </Tag>\n        </div>\n      );\n    }\n\n    return (\n      <div>\n        <div class={cssClass}>{label}</div>\n      </div>\n    );\n  },\n});\n</script>\n"
  },
  {
    "path": "apps/web-antd/src/components/dict/src/type.d.ts",
    "content": "/**\n * fallback的渲染\n * 可返回 字符串/Vnode\n */\nexport type DictFallback =\n  | ((current: number | string) => string | VNode)\n  | string;\n"
  },
  {
    "path": "apps/web-antd/src/components/global/button.ts",
    "content": "import { defineComponent, h } from 'vue';\n\nimport { Button } from 'ant-design-vue';\nimport buttonProps from 'ant-design-vue/es/button/buttonTypes';\nimport { omit } from 'lodash-es';\n\n/**\n * 表格操作列按钮专用\n */\nexport const GhostButton = defineComponent({\n  name: 'GhostButton',\n  props: omit(buttonProps(), ['type', 'ghost', 'size']),\n  setup(props, { attrs, slots }) {\n    return () =>\n      h(\n        Button,\n        { ...props, ...attrs, type: 'primary', ghost: true, size: 'small' },\n        slots,\n      );\n  },\n});\n"
  },
  {
    "path": "apps/web-antd/src/components/global/index.ts",
    "content": "import type { App } from 'vue';\n\nimport { Button as AButton } from 'ant-design-vue';\n\nimport { GhostButton } from './button';\n\n/**\n * 全局组件注册\n */\nexport function setupGlobalComponent(app: App) {\n  app.use(AButton);\n  // 表格操作列专用按钮\n  app.component('GhostButton', GhostButton);\n}\n"
  },
  {
    "path": "apps/web-antd/src/components/global/slot.ts",
    "content": "import { defineComponent, h } from 'vue';\n\n/**\n * 使用默认插槽来自定义组件\n * 给vbenForm的components使用\n */\nexport const DefaultSlot = defineComponent({\n  name: 'DefaultSlot',\n  inheritAttrs: false,\n  props: {\n    /**\n     * 绑定到根节点的div上的属性\n     */\n    rootDivAttrs: {\n      type: Object,\n      default: () => ({}),\n    },\n  },\n  render() {\n    /**\n     * 获取属性 传递给作用域插槽供外部使用\n     */\n    const attrs = this.$attrs;\n\n    return h('div', { ...this.rootDivAttrs }, this.$slots.default?.(attrs));\n  },\n});\n"
  },
  {
    "path": "apps/web-antd/src/components/table/index.ts",
    "content": "export { default as OptionsTag } from './src/options-tag.vue';\nexport { default as TableSwitch } from './src/table-switch.vue';\n"
  },
  {
    "path": "apps/web-antd/src/components/table/src/options-tag.vue",
    "content": "<script setup lang=\"tsx\">\nimport { computed } from 'vue';\n\nimport { Tag } from 'ant-design-vue';\n\ndefineOptions({ name: 'OptionsTag' });\n\nconst props = defineProps<{\n  options: { color?: string; label: string; value: number | string }[];\n  value: number | string;\n}>();\n\nconst found = computed(() =>\n  props.options.find((item) => item.value === props.value),\n);\n</script>\n\n<template>\n  <Tag v-if=\"found\" :color=\"found.color\">{{ found.label }}</Tag>\n  <span v-else>未知</span>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/components/table/src/table-switch.vue",
    "content": "<script setup lang=\"ts\">\nimport { computed, ref } from 'vue';\n\nimport { $t } from '@vben/locales';\n\nimport { Modal, Switch } from 'ant-design-vue';\nimport { isFunction } from 'lodash-es';\n\ntype CheckedType = boolean | number | string;\n\ninterface Props {\n  /**\n   * 选中的文本\n   * @default i18n 启用\n   */\n  checkedText?: string;\n  /**\n   * 未选中的文本\n   * @default i18n 禁用\n   */\n  unCheckedText?: string;\n  checkedValue?: CheckedType;\n  unCheckedValue?: CheckedType;\n  disabled?: boolean;\n  /**\n   * 需要自己在内部处理更新的逻辑 因为status已经双向绑定了 可以直接获取\n   */\n  api: () => PromiseLike<void>;\n  /**\n   * 更新前是否弹窗确认\n   * @default false\n   */\n  confirm?: boolean;\n  /**\n   * 对应的提示内容\n   * @param checked 选中的值(更新后的值)\n   * @default string '确认要更新状态吗？'\n   */\n  confirmText?: (checked: CheckedType) => string;\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n  checkedText: undefined,\n  unCheckedText: undefined,\n  checkedValue: '0',\n  unCheckedValue: '1',\n  confirm: false,\n  confirmText: undefined,\n});\n\nconst emit = defineEmits<{ reload: [] }>();\n\n// 修改为computed 支持语言切换\nconst checkedTextComputed = computed(() => {\n  return props.checkedText ?? $t('pages.common.enable');\n});\n\nconst unCheckedTextComputed = computed(() => {\n  return props.unCheckedText ?? $t('pages.common.disable');\n});\n\nconst currentChecked = defineModel<CheckedType>('value', {\n  default: false,\n});\n\nconst loading = ref(false);\n\nfunction confirmUpdate(checked: CheckedType, lastStatus: CheckedType) {\n  const content = isFunction(props.confirmText)\n    ? props.confirmText(checked)\n    : `确认要更新状态吗？`;\n\n  Modal.confirm({\n    title: '提示',\n    content,\n    centered: true,\n    onOk: async () => {\n      try {\n        loading.value = true;\n        const { api } = props;\n        isFunction(api) && (await api());\n        emit('reload');\n      } catch {\n        currentChecked.value = lastStatus;\n      } finally {\n        loading.value = false;\n      }\n    },\n    onCancel: () => {\n      currentChecked.value = lastStatus;\n    },\n  });\n}\n\nasync function handleChange(checked: CheckedType, e: Event) {\n  // 阻止事件冒泡 否则会跟行选中冲突\n  e.stopPropagation();\n  const { checkedValue, unCheckedValue } = props;\n  // 原本的状态\n  const lastStatus = checked === checkedValue ? unCheckedValue : checkedValue;\n  // 切换状态\n  currentChecked.value = checked;\n  const { api } = props;\n  try {\n    loading.value = true;\n\n    if (props.confirm) {\n      confirmUpdate(checked, lastStatus);\n      return;\n    }\n\n    isFunction(api) && (await api());\n    emit('reload');\n  } catch {\n    currentChecked.value = lastStatus;\n  } finally {\n    loading.value = false;\n  }\n}\n</script>\n\n<template>\n  <Switch\n    v-bind=\"$attrs\"\n    :loading=\"loading\"\n    :disabled=\"disabled\"\n    :checked=\"currentChecked\"\n    :checked-children=\"checkedTextComputed\"\n    :checked-value=\"checkedValue\"\n    :un-checked-children=\"unCheckedTextComputed\"\n    :un-checked-value=\"unCheckedValue\"\n    @change=\"handleChange\"\n  />\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/components/tenant-toggle/index.ts",
    "content": "export { default as TenantToggle } from './src/index.vue';\n"
  },
  {
    "path": "apps/web-antd/src/components/tenant-toggle/src/index.vue",
    "content": "<script setup lang=\"ts\">\nimport type { MessageType } from 'ant-design-vue/es/message';\nimport type { SelectHandler } from 'ant-design-vue/es/vc-select/Select';\n\nimport type { TenantOption } from '#/api';\n\nimport { computed, onMounted, ref, shallowRef, unref } from 'vue';\nimport { useRoute } from 'vue-router';\n\nimport { useAccess } from '@vben/access';\nimport { useTabs } from '@vben/hooks';\nimport { $t } from '@vben/locales';\n\nimport { message, Select, Spin } from 'ant-design-vue';\nimport { storeToRefs } from 'pinia';\n\nimport { tenantDynamicClear, tenantDynamicToggle } from '#/api/system/tenant';\nimport { useDictStore } from '#/store/dict';\nimport { useTenantStore } from '#/store/tenant';\n\nconst { hasAccessByRoles } = useAccess();\n\n// 上一次选择的租户\nconst lastSelected = ref<string>();\n// 当前选择租户的id\nconst selected = ref<string>();\n\nconst tenantStore = useTenantStore();\nconst { initTenant, setChecked } = tenantStore;\nconst { tenantEnable, tenantList } = storeToRefs(tenantStore);\n\nconst showToggle = computed<boolean>(() => {\n  // 超级管理员 && 启用租户\n  return hasAccessByRoles(['superadmin']) && unref(tenantEnable);\n});\n\nonMounted(async () => {\n  // 没有超级管理员权限 不会调用接口\n  if (!hasAccessByRoles(['superadmin'])) {\n    return;\n  }\n  await initTenant();\n});\n\nconst route = useRoute();\nconst { closeOtherTabs, refreshTab, closeAllTabs } = useTabs();\n\nasync function close(checked: boolean) {\n  // store设置状态\n  setChecked(checked);\n\n  /**\n   * 切换租户需要回到首页的页面 一般为带id的页面\n   * 其他则直接刷新页面\n   */\n  if (route.meta.requireHomeRedirect) {\n    await closeAllTabs();\n  } else {\n    // 先关闭再刷新 这里不用Promise.all()\n    await closeOtherTabs();\n    await refreshTab();\n  }\n}\n\nconst dictStore = useDictStore();\n// 用于清理上一条message\nconst messageInstance = shallowRef<MessageType | null>();\n// loading加载中效果\nconst loading = ref(false);\n\n/**\n * 选中租户的处理\n * @param tenantId tenantId\n * @param option 当前option\n */\nconst onSelected: SelectHandler = async (tenantId: string, option: any) => {\n  if (unref(lastSelected) === tenantId) {\n    // createMessage.info('选择一致');\n    return;\n  }\n  try {\n    loading.value = true;\n\n    await tenantDynamicToggle(tenantId);\n    lastSelected.value = tenantId;\n\n    // 关闭之前的message 只保留一条\n    messageInstance.value?.();\n    messageInstance.value = message.success(\n      `${$t('component.tenantToggle.switch')} ${option.companyName}`,\n    );\n\n    close(true);\n    // 需要放在宏队列处理 直接清空页面由于没有字典会有样式问题(标签变成unknown)\n    setTimeout(() => dictStore.resetCache());\n  } catch (error) {\n    console.error(error);\n  } finally {\n    loading.value = false;\n  }\n};\n\nasync function onDeselect() {\n  try {\n    loading.value = true;\n\n    await tenantDynamicClear();\n    // 关闭之前的message 只保留一条\n    messageInstance.value?.();\n    messageInstance.value = message.success($t('component.tenantToggle.reset'));\n\n    lastSelected.value = '';\n    close(false);\n    // 需要放在宏队列处理 直接清空页面由于没有字典会有样式问题(标签变成unknown)\n    setTimeout(() => dictStore.resetCache());\n  } catch (error) {\n    console.error(error);\n  } finally {\n    loading.value = false;\n  }\n}\n\n/**\n * select搜索使用\n * @param input 输入内容\n * @param option 选项\n */\nfunction filterOption(input: string, option: TenantOption) {\n  return option.companyName.toLowerCase().includes(input.toLowerCase());\n}\n</script>\n\n<template>\n  <div v-if=\"showToggle\" class=\"mr-[8px] hidden md:block\">\n    <Select\n      v-model:value=\"selected\"\n      :disabled=\"loading\"\n      :field-names=\"{ label: 'companyName', value: 'tenantId' }\"\n      :filter-option=\"filterOption\"\n      :options=\"tenantList\"\n      :placeholder=\"$t('component.tenantToggle.placeholder')\"\n      :dropdown-style=\"{ position: 'fixed', zIndex: 1024 }\"\n      allow-clear\n      class=\"w-60\"\n      show-search\n      @deselect=\"onDeselect\"\n      @select=\"onSelected\"\n    >\n      <template v-if=\"loading\" #suffixIcon>\n        <Spin size=\"small\" spinning />\n      </template>\n    </Select>\n  </div>\n</template>\n\n<style lang=\"scss\" scoped>\n// 当选中时 添加border样式\n:deep(.ant-select-selector) {\n  &:has(.ant-select-selection-item) {\n    box-shadow: 0 0 10px hsl(var(--primary));\n  }\n}\n</style>\n"
  },
  {
    "path": "apps/web-antd/src/components/tinymce/index.ts",
    "content": "export { default as Tinymce } from './src/editor.vue';\n"
  },
  {
    "path": "apps/web-antd/src/components/tinymce/src/editor.vue",
    "content": "<script setup lang=\"ts\">\nimport type { IPropTypes } from '@tinymce/tinymce-vue/lib/cjs/main/ts/components/EditorPropTypes';\nimport type { Editor as EditorType } from 'tinymce/tinymce';\n\nimport type { AxiosProgressEvent, UploadResult } from '#/api';\n\nimport { computed, nextTick, ref, shallowRef, useAttrs, watch } from 'vue';\n\nimport { preferences, usePreferences } from '@vben/preferences';\n\nimport Editor from '@tinymce/tinymce-vue';\nimport { Spin } from 'ant-design-vue';\nimport { camelCase } from 'lodash-es';\n\nimport { uploadApi } from '#/api';\nimport {\n  plugins as defaultPlugins,\n  toolbar as defaultToolbar,\n} from '#/components/tinymce/src/tinymce';\n\ntype InitOptions = IPropTypes['init'];\n\ninterface Props {\n  height?: number | string;\n  options?: Partial<InitOptions>;\n  plugins?: string;\n  toolbar?: string;\n  disabled?: boolean;\n}\n\ndefineOptions({\n  name: 'Tinymce',\n  inheritAttrs: false,\n});\n\nconst props = withDefaults(defineProps<Props>(), {\n  height: 400,\n  options: () => ({}),\n  plugins: defaultPlugins,\n  toolbar: defaultToolbar,\n  disabled: false,\n});\n\nconst emit = defineEmits<{\n  mounted: [];\n}>();\n\n/**\n * https://www.jianshu.com/p/59a9c3802443\n * 使用自托管方案（本地）代替cdn  没有key的限制\n * 注意publicPath要以/结尾\n */\nconst tinymceScriptSrc = `${import.meta.env.VITE_BASE}tinymce/tinymce.min.js`;\n\nconst content = defineModel<string>('modelValue', {\n  default: '',\n});\n\nconst editorRef = shallowRef<EditorType | null>(null);\n\n// 存储上传图片的 url -> ossId 映射，用于后续附加 data-oss-id\nconst pendingImageMap = new Map<string, string>();\n\nconst { isDark, locale } = usePreferences();\nconst skinName = computed(() => {\n  return isDark.value ? 'oxide-dark' : 'oxide';\n});\n\nconst contentCss = computed(() => {\n  return isDark.value ? 'dark' : 'default';\n});\n\n/**\n * tinymce支持 en zh_CN\n */\nconst langName = computed(() => {\n  const lang = preferences.app.locale.replace('-', '_');\n  if (lang.includes('en_US')) {\n    return 'en';\n  }\n  return 'zh_CN';\n});\n\n/**\n * 通过v-if来挂载/卸载组件来完成主题切换切换\n * 语言切换也需要监听 不监听在切换时候会显示原始<textarea>样式\n */\nconst init = ref(true);\nwatch([isDark, locale], async () => {\n  if (!editorRef.value) {\n    return;\n  }\n  // 相当于手动unmounted清理 非常重要\n  editorRef.value.destroy();\n  init.value = false;\n  // 放在下一次tick来切换\n  // 需要先加载组件 也就是v-if为true  然后需要拿到editorRef 必须放在setTimeout(相当于onMounted)\n  await nextTick();\n  init.value = true;\n});\n\n// 加载完毕前显示spin\nconst loading = ref(true);\nconst initOptions = computed((): InitOptions => {\n  const { height, options, plugins, toolbar } = props;\n  return {\n    auto_focus: true,\n    branding: false, // 显示右下角的'使用 TinyMCE 构建'\n    content_css: contentCss.value,\n    content_style:\n      'body { font-family:Helvetica,Arial,sans-serif; font-size:16px }',\n    contextmenu: 'link image table',\n    default_link_target: '_blank',\n    height,\n    image_advtab: true, // 图片高级选项\n    image_caption: true,\n    importcss_append: true,\n    language: langName.value,\n    link_title: false,\n    menubar: 'file edit view insert format tools table help',\n    noneditable_class: 'mceNonEditable',\n    /**\n     * 允许粘贴图片 默认base64格式\n     * images_upload_handler启用时为上传\n     */\n    paste_data_images: true,\n    images_file_types: 'jpeg,jpg,png,gif,bmp,webp',\n    plugins,\n    quickbars_selection_toolbar:\n      'bold italic | quicklink h2 h3 blockquote quickimage quicktable',\n    skin: skinName.value,\n    toolbar,\n    toolbar_mode: 'sliding',\n    // 隐藏下面的 按xxx获取帮助\n    help_accessibility: false,\n    // https://blog.csdn.net/qq_46380656/article/details/122171418\n    // 避免图片地址和链接地址转换成相对路径\n    relative_urls: false,\n    remove_script_host: false,\n    convert_urls: false,\n    ...options,\n    /**\n     * 覆盖默认的base64行为\n     * @param blobInfo\n     * 大坑 不要调用这两个函数  success failure:\n     * 使用resolve/reject代替\n     * (PS: 新版已经没有success failure)\n     */\n    images_upload_handler: (blobInfo, progress) => {\n      return new Promise((resolve, reject) => {\n        const file = blobInfo.blob();\n        // const filename = blobInfo.filename();\n        // 进度条事件\n        const progressEvent: AxiosProgressEvent = (e) => {\n          const percent = Math.trunc((e.loaded / e.total!) * 100);\n          progress(percent);\n        };\n        uploadApi(file, { onUploadProgress: progressEvent })\n          .then((response) => {\n            const { url, ossId } = response as unknown as UploadResult;\n            console.log('tinymce上传图片:', url);\n            // 将 url -> ossId 映射存储起来，等待图片插入后再附加\n            pendingImageMap.set(url, ossId);\n            resolve(url);\n          })\n          .catch((error) => {\n            console.error('tinymce上传图片失败:', error);\n            // eslint-disable-next-line prefer-promise-reject-errors\n            reject({ message: error.message, remove: true });\n          });\n      });\n    },\n    setup: (editor) => {\n      editorRef.value = editor;\n      editor.on('init', () => {\n        emit('mounted');\n        loading.value = false;\n      });\n\n      // 监听内容变化，处理待附加 data-oss-id 的图片\n      editor.on('change', () => {\n        if (pendingImageMap.size === 0) {\n          return;\n        }\n\n        pendingImageMap.forEach((ossId, url) => {\n          const imgDoms = editor.dom.select(`img[src=\"${url}\"]`);\n          if (imgDoms && imgDoms.length > 0) {\n            imgDoms.forEach((imgDom) => {\n              // 只处理还没有 data-oss-id 属性的图片\n              if (!editor.dom.getAttrib(imgDom, 'data-oss-id')) {\n                editor.dom.setAttrib(imgDom, 'data-oss-id', ossId);\n                console.log('已附加 data-oss-id:', url, ossId);\n              }\n            });\n            pendingImageMap.delete(url);\n          }\n        });\n      });\n    },\n  };\n});\n\nconst attrs = useAttrs();\n/**\n * 获取透传的事件 通过v-on绑定\n * 可绑定的事件 https://www.tiny.cloud/docs/tinymce/latest/vue-ref/#event-binding\n */\nconst events = computed(() => {\n  const onEvents: Record<string, any> = {};\n  for (const key in attrs) {\n    if (key.startsWith('on')) {\n      const eventKey = camelCase(key.split('on')[1]!);\n      onEvents[eventKey] = attrs[key];\n    }\n  }\n  return onEvents;\n});\n</script>\n\n<template>\n  <div class=\"app-tinymce\">\n    <Spin :spinning=\"loading\">\n      <Editor\n        v-if=\"init\"\n        v-model=\"content\"\n        :init=\"initOptions\"\n        :tinymce-script-src=\"tinymceScriptSrc\"\n        :disabled=\"disabled\"\n        license-key=\"gpl\"\n        v-on=\"events\"\n      />\n    </Spin>\n  </div>\n</template>\n\n<style lang=\"scss\">\n// 展开层元素z-index\n$dropdown-index: 2025;\n\n@mixin tinymce-valid-fail($color) {\n  .app-tinymce {\n    // 最外层的tinymce容器\n    .tox-tinymce {\n      border-color: $color;\n    }\n    // focus样式\n    .tox .tox-edit-area::before {\n      border-color: $color;\n      border-right: none;\n      border-left: none;\n    }\n  }\n}\n\n.tox.tox-silver-sink.tox-tinymce-aux {\n  /** 该样式默认为1300的zIndex  */\n  z-index: $dropdown-index;\n}\n\n.tox-fullscreen .tox.tox-tinymce-aux {\n  z-index: $dropdown-index !important;\n}\n\n.app-tinymce {\n  /**\n  隐藏右上角upgrade按钮\n  */\n  .tox-promotion {\n    display: none;\n  }\n\n  /** 保持focus时与primary色一致 */\n  .tox .tox-edit-area::before {\n    border-color: hsl(var(--primary));\n  }\n}\n\n// antd原生表单 校验失败样式\n.ant-form-item:has(.ant-form-item-explain-error) {\n  $error-color: #ff3860;\n\n  @include tinymce-valid-fail($error-color);\n}\n\n// useVbenForm 校验失败样式\n.form-valid-error {\n  $error-color: hsl(var(--destructive));\n\n  @include tinymce-valid-fail($error-color);\n}\n\n// 全屏下样式处理 不去掉transform位置会异常\ndiv[role='dialog']:has(.tox.tox-tinymce.tox-fullscreen) {\n  transform: none !important;\n}\n</style>\n"
  },
  {
    "path": "apps/web-antd/src/components/tinymce/src/helper.ts",
    "content": "import { ossInfo } from '#/api/system/oss';\n\n/**\n * 富文本内容中图片ossId转换  确保每次链接都是最新获取的(对于私有桶情况)\n *\n * 当然你可以使用后端来解析dom替换 达到相同的效果 就不用前端调用了\n * 使用方法: 在赋值前调用此方法 contentWithOssIdTransform(content); 转换一次再赋值\n * @param content 富文本内容\n * @returns string\n */\nexport async function contentWithOssIdTransform(content: string) {\n  if (!content) {\n    return null;\n  }\n  const parser = new DOMParser();\n  const doc = parser.parseFromString(content, 'text/html');\n  const imgDom = doc.querySelectorAll('img[data-oss-id]');\n\n  // 没有包含图片 不做处理\n  if (imgDom.length === 0) {\n    return content;\n  }\n\n  // 提取所有data-oss-id属性 作为string[]\n  const ossIds = [...imgDom].map(\n    (img) => (img as HTMLImageElement).dataset.ossId ?? '',\n  );\n  // 兼容之前的代码 可能并没有储存ossId\n  if (ossIds.length === 0) {\n    return content;\n  }\n  const ossFileList = await ossInfo(ossIds);\n\n  imgDom.forEach((item) => {\n    const img = item as HTMLImageElement;\n    // 找到对应的 替换\n    const src =\n      ossFileList.find((file) => file.ossId === img.dataset.ossId)?.url ??\n      // 未找到 取原先自己的src\n      img.src;\n    img.setAttribute('src', src);\n  });\n\n  // 获取dom string\n  return doc.body.innerHTML;\n}\n"
  },
  {
    "path": "apps/web-antd/src/components/tinymce/src/tinymce.ts",
    "content": "// Any plugins you want to setting has to be imported\n// Detail plugins list see https://www.tinymce.com/docs/plugins/\n// Custom builds see https://www.tinymce.com/download/custom-builds/\n// colorpicker/contextmenu/textcolor plugin is now built in to the core editor, please remove it from your editor configuration\n\n// quickbars 快捷栏\nexport const plugins =\n  'preview importcss searchreplace autolink autosave save directionality code visualblocks visualchars fullscreen image link media codesample table charmap pagebreak nonbreaking anchor insertdatetime advlist lists wordcount help charmap emoticons accordion';\n\nexport const toolbar =\n  'undo redo | accordion accordionremove | blocks fontfamily fontsize | bold italic underline strikethrough | align numlist bullist | link image | table media | lineheight outdent indent| forecolor backcolor removeformat | charmap emoticons | code fullscreen preview | save print | pagebreak anchor codesample | ltr rtl';\n"
  },
  {
    "path": "apps/web-antd/src/components/tree/index.ts",
    "content": "export { default as MenuSelectTable } from './src/menu-select-table.vue';\nexport { default as TreeSelectPanel } from './src/tree-select-panel.vue';\n"
  },
  {
    "path": "apps/web-antd/src/components/tree/src/data.tsx",
    "content": "import type { VxeGridProps } from '#/adapter/vxe-table';\nimport type { ID } from '#/api/common';\nimport type { MenuOption } from '#/api/system/menu/model';\n\nimport { h, markRaw } from 'vue';\n\nimport { FolderIcon, MenuIcon, OkButtonIcon, VbenIcon } from '@vben/icons';\n\nexport interface Permission {\n  checked: boolean;\n  id: ID;\n  label: string;\n}\n\nexport interface MenuPermissionOption extends MenuOption {\n  permissions: Permission[];\n}\n\nconst menuTypes = {\n  C: { icon: markRaw(MenuIcon), value: '菜单' },\n  F: { icon: markRaw(OkButtonIcon), value: '按钮' },\n  M: { icon: markRaw(FolderIcon), value: '目录' },\n};\n\nexport const nodeOptions = [\n  { label: '节点关联', value: true },\n  { label: '节点独立', value: false },\n];\n\nexport const columns: VxeGridProps['columns'] = [\n  {\n    type: 'checkbox',\n    title: '菜单名称',\n    field: 'label',\n    treeNode: true,\n    headerAlign: 'left',\n    align: 'left',\n    width: 230,\n  },\n  {\n    title: '图标',\n    field: 'icon',\n    width: 80,\n    slots: {\n      default: ({ row }) => {\n        if (row?.icon === '#') {\n          return '';\n        }\n        return (\n          <span class={'flex justify-center'}>\n            <VbenIcon icon={row.icon} />\n          </span>\n        );\n      },\n    },\n  },\n  {\n    title: '类型',\n    field: 'menuType',\n    width: 80,\n    slots: {\n      default: ({ row }) => {\n        const current = menuTypes[row.menuType as 'C' | 'F' | 'M'];\n        if (!current) {\n          return '未知';\n        }\n        return (\n          <span class=\"flex items-center justify-center gap-1\">\n            {h(current.icon, { class: 'size-[18px]' })}\n            <span>{current.value}</span>\n          </span>\n        );\n      },\n    },\n  },\n  {\n    title: '权限标识',\n    field: 'permissions',\n    headerAlign: 'left',\n    align: 'left',\n    slots: {\n      default: 'permissions',\n    },\n  },\n];\n"
  },
  {
    "path": "apps/web-antd/src/components/tree/src/helper.tsx",
    "content": "import type { MenuPermissionOption } from './data';\n\nimport type { useVbenVxeGrid } from '#/adapter/vxe-table';\nimport type { MenuOption } from '#/api/system/menu/model';\n\nimport { eachTree, treeToList } from '@vben/utils';\n\nimport { notification } from 'ant-design-vue';\nimport { difference, isEmpty, isUndefined } from 'lodash-es';\n\n/**\n * 权限列设置是否全选\n * @param record 行记录\n * @param checked 是否选中\n */\nexport function setPermissionsChecked(\n  record: MenuPermissionOption,\n  checked: boolean,\n) {\n  if (record?.permissions?.length > 0) {\n    // 全部设置为选中\n    record.permissions.forEach((permission) => {\n      permission.checked = checked;\n    });\n  }\n}\n\n/**\n * 设置当前行 & 所有子节点选中状态\n * @param record 行\n * @param checked 是否选中\n */\nexport function rowAndChildrenChecked(\n  record: MenuPermissionOption,\n  checked: boolean,\n) {\n  // 当前行选中\n  setPermissionsChecked(record, checked);\n  // 所有子节点选中\n  record?.children?.forEach?.((permission) => {\n    rowAndChildrenChecked(permission as MenuPermissionOption, checked);\n  });\n}\n\n/**\n * void方法 会直接修改原始数据\n * 将树结构转为 tree+permissions结构\n * @param menus 后台返回的menu\n */\nexport function menusWithPermissions(menus: MenuOption[]) {\n  eachTree(menus, (item: MenuPermissionOption) => {\n    validateMenuTree(item);\n    if (item.children && item.children.length > 0) {\n      /**\n       * 所有为按钮的节点提取出来\n       * 需要注意 这里需要过滤目录下直接是按钮的情况item.menuType !== 'M'\n       * 将按钮往children添加而非加到permissions\n       */\n      const permissions = item.children.filter(\n        (child: MenuOption) => child.menuType === 'F' && item.menuType !== 'M',\n      );\n      // 取差集\n      const diffCollection = difference(item.children, permissions);\n      // 更新后的children  即去除按钮\n      item.children = diffCollection;\n\n      // permissions作为字段添加到item\n      const permissionsArr = permissions.map((permission) => {\n        return {\n          id: permission.id,\n          label: permission.label,\n          checked: false,\n        };\n      });\n      item.permissions = permissionsArr;\n    }\n  });\n}\n\n/**\n * 设置表格选中\n * @param checkedKeys 选中的keys\n * @param menus 菜单 转换后的菜单\n * @param tableApi api\n * @param association 是否节点关联\n */\nexport function setTableChecked(\n  checkedKeys: (number | string)[],\n  menus: MenuPermissionOption[],\n  tableApi: ReturnType<typeof useVbenVxeGrid>['1'],\n  association: boolean,\n) {\n  // tree转list\n  const menuList: MenuPermissionOption[] = treeToList(menus);\n  // 拿到勾选的行数据\n  let checkedRows = menuList.filter((item) => checkedKeys.includes(item.id));\n\n  /**\n   * 节点独立切换到节点关联 只需要最末尾的数据 即children为空\n   */\n  if (!association) {\n    checkedRows = checkedRows.filter(\n      (item) => isUndefined(item.children) || isEmpty(item.children),\n    );\n  }\n\n  // 设置行选中 & permissions选中\n  checkedRows.forEach((item) => {\n    tableApi.grid.setCheckboxRow(item, true);\n    if (item?.permissions?.length > 0) {\n      item.permissions.forEach((permission) => {\n        if (checkedKeys.includes(permission.id)) {\n          permission.checked = true;\n        }\n      });\n    }\n  });\n\n  /**\n   * 节点独立切换到节点关联\n   * 勾选后还需要过滤权限没有任何勾选的情况 这时候取消行的勾选\n   */\n  if (!association) {\n    const emptyRows = checkedRows.filter((item) => {\n      if (isUndefined(item.permissions) || isEmpty(item.permissions)) {\n        return false;\n      }\n      return item.permissions.every(\n        (permission) => permission.checked === false,\n      );\n    });\n    // 设置为不选中\n    tableApi.grid.setCheckboxRow(emptyRows, false);\n  }\n}\n\n/**\n * 校验是否符合规范 给出warning提示\n *\n * 不符合规范\n * 比如: 菜单下放目录 菜单下放菜单\n * 比如: 按钮下放目录 按钮下放菜单 按钮下放按钮\n * @param menu menu\n */\nfunction validateMenuTree(menu: MenuOption) {\n  /**\n   * C: { icon: markRaw(MenuIcon), value: '菜单' },\n      F: { icon: markRaw(OkButtonIcon), value: '按钮' },\n      M: { icon: markRaw(FolderIcon), value: '目录' },\n   */\n  // 菜单下不能放目录/菜单\n  if (menu.menuType === 'C') {\n    menu.children?.forEach?.((item) => {\n      if (['C', 'M'].includes(item.menuType)) {\n        const description = `错误用法: [${menu.label} - 菜单]下不能放 目录/菜单 -> [${item.label}]`;\n        console.warn(description);\n        notification.warning({\n          message: '提示',\n          description,\n          duration: 0,\n        });\n      }\n    });\n  }\n  // 按钮为最末级 不能再放置\n  if (menu.menuType === 'F') {\n    /**\n     * 其实可以直接判断length 这里为了更准确知道label 采用遍历的形式\n     */\n    menu.children?.forEach?.((item) => {\n      if (['C', 'F', 'M'].includes(item.menuType)) {\n        const description = `错误用法: [${menu.label} - 按钮]下不能放置'目录/菜单/按钮' -> [${item.label}]`;\n        console.warn(description);\n        notification.warning({\n          message: '提示',\n          description,\n          duration: 0,\n        });\n      }\n    });\n  }\n}\n"
  },
  {
    "path": "apps/web-antd/src/components/tree/src/hook.tsx",
    "content": "/* eslint-disable @typescript-eslint/no-non-null-assertion */\nimport type { TourProps } from 'ant-design-vue';\n\nimport { defineComponent, ref } from 'vue';\n\nimport { useLocalStorage } from '@vueuse/core';\nimport { Tour } from 'ant-design-vue';\n\n/**\n * 全屏引导\n * @returns value\n */\nexport function useFullScreenGuide() {\n  const open = ref(false);\n  /**\n   * 是否已读 只显示一次\n   */\n  const read = useLocalStorage('menu_select_fullscreen_read', false);\n\n  function openGuide() {\n    if (!read.value) {\n      open.value = true;\n    }\n  }\n\n  function closeGuide() {\n    open.value = false;\n    read.value = true;\n  }\n\n  const steps: TourProps['steps'] = [\n    {\n      title: '提示',\n      description: '点击这里可以全屏',\n      target: () =>\n        document.querySelector(\n          'div#menu-select-table .vxe-tools--operate > button[title=\"全屏\"]',\n        )!,\n    },\n  ];\n\n  const FullScreenGuide = defineComponent({\n    name: 'FullScreenGuide',\n    inheritAttrs: false,\n    setup() {\n      return () => (\n        <Tour\n          onClose={closeGuide}\n          open={open.value}\n          steps={steps}\n          zIndex={9999}\n        />\n      );\n    },\n  });\n\n  return {\n    FullScreenGuide,\n    openGuide,\n    closeGuide,\n  };\n}\n"
  },
  {
    "path": "apps/web-antd/src/components/tree/src/menu-select-table.vue",
    "content": "<!--\n不兼容也不会兼容一些错误用法\n比如: 菜单下放目录 菜单下放菜单\n比如: 按钮下放目录 按钮下放菜单 按钮下放按钮\n-->\n<script setup lang=\"tsx\">\nimport type { RadioChangeEvent } from 'ant-design-vue';\n\nimport type { MenuPermissionOption } from './data';\n\nimport type { VxeGridProps } from '#/adapter/vxe-table';\nimport type { MenuOption } from '#/api/system/menu/model';\n\nimport { nextTick, onMounted, ref, shallowRef, watch } from 'vue';\n\nimport { cloneDeep, findGroupParentIds } from '@vben/utils';\n\nimport { Alert, Checkbox, RadioGroup, Space } from 'ant-design-vue';\nimport { uniq } from 'lodash-es';\n\nimport { useVbenVxeGrid } from '#/adapter/vxe-table';\n\nimport { columns, nodeOptions } from './data';\nimport {\n  menusWithPermissions,\n  rowAndChildrenChecked,\n  setPermissionsChecked,\n  setTableChecked,\n} from './helper';\nimport { useFullScreenGuide } from './hook';\n\ndefineOptions({\n  name: 'MenuSelectTable',\n  inheritAttrs: false,\n});\n\nconst props = withDefaults(\n  defineProps<{\n    checkedKeys: (number | string)[];\n    defaultExpandAll?: boolean;\n    menus: MenuOption[];\n  }>(),\n  {\n    /**\n     * 是否默认展开全部\n     */\n    defaultExpandAll: true,\n    /**\n     * 注意这里不是双向绑定 需要调用getCheckedKeys实例方法来获取真正选中的节点\n     */\n    checkedKeys: () => [],\n  },\n);\n\n/**\n * 是否节点关联\n */\nconst association = defineModel<boolean>('association', {\n  default: true,\n});\n\nconst gridOptions: VxeGridProps = {\n  checkboxConfig: {\n    // checkbox显示的字段\n    labelField: 'label',\n    // 是否严格模式 即节点不关联\n    checkStrictly: !association.value,\n  },\n  size: 'small',\n  columns,\n  height: 'auto',\n  keepSource: true,\n  pagerConfig: {\n    enabled: false,\n  },\n  proxyConfig: {\n    enabled: false,\n  },\n  toolbarConfig: {\n    refresh: false,\n    custom: false,\n  },\n  rowConfig: {\n    isHover: false,\n    isCurrent: false,\n    keyField: 'id',\n  },\n  /**\n   * 开启虚拟滚动\n   * 数据量小可以选择关闭\n   * 如果遇到样式问题(空白、错位 滚动等)可以选择关闭虚拟滚动\n   */\n  scrollY: {\n    enabled: true,\n    gt: 0,\n  },\n  treeConfig: {\n    parentField: 'parentId',\n    rowField: 'id',\n    transform: false,\n  },\n  // 溢出换行显示\n  showOverflow: false,\n};\n\n/**\n * 用于界面显示选中的数量\n */\nconst checkedNum = ref(0);\n/**\n * 更新选中的数量\n */\nfunction updateCheckedNumber() {\n  checkedNum.value = getCheckedKeys().length;\n}\n\nconst [BasicTable, tableApi] = useVbenVxeGrid({\n  gridOptions,\n  gridEvents: {\n    // 勾选事件\n    checkboxChange: (params) => {\n      // 选中还是取消选中\n      const checked = params.checked;\n      // 行\n      const record = params.row;\n      if (association.value) {\n        // 节点关联\n        // 设置所有子节点选中状态\n        rowAndChildrenChecked(record, checked);\n      } else {\n        // 节点独立\n        // 点行会勾选/取消全部权限  点权限不会勾选行\n        setPermissionsChecked(record, checked);\n      }\n      updateCheckedNumber();\n    },\n    // 全选事件\n    checkboxAll: (params) => {\n      const records = params.$grid.getData();\n      records.forEach((item) => {\n        rowAndChildrenChecked(item, params.checked);\n      });\n      updateCheckedNumber();\n    },\n  },\n});\n\n/**\n * 设置表格选中\n * @param menus menu\n * @param keys 选中的key\n * @param triggerOnchange 节点独立情况 不需要触发onChange(false)\n */\nfunction setCheckedByKeys(\n  menus: MenuPermissionOption[],\n  keys: (number | string)[],\n  triggerOnchange: boolean,\n) {\n  menus.forEach((item) => {\n    // 设置行选中\n    if (keys.includes(item.id)) {\n      tableApi.grid.setCheckboxRow(item, true);\n    }\n    // 设置权限columns选中\n    if (item.permissions && item.permissions.length > 0) {\n      // 遍历 设置勾选\n      item.permissions.forEach((permission) => {\n        if (keys.includes(permission.id)) {\n          permission.checked = true;\n          // 手动触发onChange来选中 节点独立情况不需要处理\n          triggerOnchange && handlePermissionChange(item);\n        }\n      });\n    }\n    // 设置children选中\n    if (item.children && item.children.length > 0) {\n      setCheckedByKeys(item.children as any, keys, triggerOnchange);\n    }\n  });\n}\n\nconst { FullScreenGuide, openGuide } = useFullScreenGuide();\nonMounted(() => {\n  /**\n   * 加载表格数据 转为指定结构\n   */\n  watch(\n    () => props.menus,\n    async (menus) => {\n      const clonedMenus = cloneDeep(menus);\n      menusWithPermissions(clonedMenus);\n      // console.log(clonedMenus);\n      await tableApi.grid.loadData(clonedMenus);\n      // 展开全部 默认true\n      if (props.defaultExpandAll) {\n        await nextTick();\n        setExpandOrCollapse(true);\n      }\n    },\n  );\n\n  /**\n   * 节点关联变动 更新表格勾选效果\n   */\n  watch(association, (value) => {\n    tableApi.setGridOptions({\n      checkboxConfig: {\n        checkStrictly: !value,\n      },\n    });\n  });\n\n  /**\n   * checkedKeys依赖menus\n   * 要注意加载顺序\n   * !!!要在外部确保menus先加载!!!\n   */\n  watch(\n    () => props.checkedKeys,\n    (value) => {\n      const allCheckedKeys = uniq([...value]);\n      // 获取表格data 如果checkedKeys在menus的watch之前触发 这里会拿到空 导致勾选异常\n      const records = tableApi.grid.getData();\n      setCheckedByKeys(records, allCheckedKeys, association.value);\n      updateCheckedNumber();\n\n      // 全屏引导\n      setTimeout(openGuide, 1000);\n    },\n  );\n});\n\n// 缓存上次(切换节点关系前)选中的keys\nconst lastCheckedKeys = shallowRef<(number | string)[]>([]);\n/**\n * 节点关联变动 事件\n */\nasync function handleAssociationChange(e: RadioChangeEvent) {\n  lastCheckedKeys.value = getCheckedKeys();\n  // 清空全部permissions选中\n  const records = tableApi.grid.getData();\n  records.forEach((item) => {\n    rowAndChildrenChecked(item, false);\n  });\n  // 需要清空全部勾选\n  await tableApi.grid.clearCheckboxRow();\n  // 滚动到顶部\n  await tableApi.grid.scrollTo(0, 0);\n\n  // 节点切换 不同的选中\n  setTableChecked(lastCheckedKeys.value, records, tableApi, !e.target.value);\n\n  updateCheckedNumber();\n}\n\n/**\n * 全部展开/折叠\n * @param expand 是否展开\n */\nfunction setExpandOrCollapse(expand: boolean) {\n  tableApi.grid?.setAllTreeExpand(expand);\n}\n\n/**\n * 权限列表 checkbox勾选的事件\n * @param row 行\n */\nfunction handlePermissionChange(row: any) {\n  // 节点关联\n  if (association.value) {\n    const checkedPermissions = row.permissions.filter(\n      (item: any) => item.checked === true,\n    );\n    // 有一条选中 则整个行选中\n    if (checkedPermissions.length > 0) {\n      tableApi.grid.setCheckboxRow(row, true);\n    }\n    // 无任何选中 则整个行不选中\n    if (checkedPermissions.length === 0) {\n      tableApi.grid.setCheckboxRow(row, false);\n    }\n  }\n  // 节点独立 不处理\n  updateCheckedNumber();\n}\n\n/**\n * 获取勾选的key\n * @param records 行记录列表\n * @param addCurrent 是否添加当前行的id\n */\nfunction getKeys(records: MenuPermissionOption[], addCurrent: boolean) {\n  const allKeys: (number | string)[] = [];\n  records.forEach((item) => {\n    // 处理children\n    if (item.children && item.children.length > 0) {\n      const keys = getKeys(item.children as MenuPermissionOption[], addCurrent);\n      allKeys.push(...keys);\n    } else {\n      // 当前行的id\n      addCurrent && allKeys.push(item.id);\n      // 当前行权限id 获取已经选中的\n      if (item.permissions && item.permissions.length > 0) {\n        const ids = item.permissions\n          .filter((m) => m.checked === true)\n          .map((m) => m.id);\n        allKeys.push(...ids);\n      }\n    }\n  });\n  return uniq(allKeys);\n}\n\n/**\n * 获取选中的key\n */\nfunction getCheckedKeys() {\n  // 节点关联\n  if (association.value) {\n    const records = tableApi?.grid?.getCheckboxRecords?.(true) ?? [];\n    // 子节点\n    const nodeKeys = getKeys(records, true);\n    // 所有父节点\n    const parentIds = findGroupParentIds(props.menus, nodeKeys as number[]);\n    // 拼接 去重\n    const realKeys = uniq([...parentIds, ...nodeKeys]);\n    return realKeys;\n  }\n  // 节点独立\n\n  // 勾选的行\n  const records = tableApi?.grid?.getCheckboxRecords?.(true) ?? [];\n  // 全部数据 用于获取permissions\n  const allRecords = tableApi?.grid?.getData?.() ?? [];\n  // 表格已经选中的行ids\n  const checkedIds = records.map((item) => item.id);\n  // 所有已经勾选权限的ids\n  const permissionIds = getKeys(allRecords, false);\n  // 合并 去重\n  const allIds = uniq([...checkedIds, ...permissionIds]);\n  return allIds;\n}\n\n/**\n * 暴露给外部使用 获取已选中的key\n */\ndefineExpose({\n  getCheckedKeys,\n});\n</script>\n\n<template>\n  <div class=\"flex h-full flex-col\" id=\"menu-select-table\">\n    <BasicTable>\n      <template #toolbar-actions>\n        <RadioGroup\n          v-model:value=\"association\"\n          :options=\"nodeOptions\"\n          button-style=\"solid\"\n          option-type=\"button\"\n          @change=\"handleAssociationChange\"\n        />\n        <Alert class=\"mx-2\" type=\"info\">\n          <template #message>\n            <div>\n              已选中\n              <span class=\"text-primary mx-1 font-semibold\">\n                {{ checkedNum }}\n              </span>\n              个节点\n            </div>\n          </template>\n        </Alert>\n      </template>\n      <template #toolbar-tools>\n        <Space>\n          <a-button @click=\"setExpandOrCollapse(false)\">\n            {{ $t('pages.common.collapse') }}\n          </a-button>\n          <a-button @click=\"setExpandOrCollapse(true)\">\n            {{ $t('pages.common.expand') }}\n          </a-button>\n        </Space>\n      </template>\n      <template #permissions=\"{ row }\">\n        <div class=\"flex flex-wrap gap-x-3 gap-y-1\">\n          <Checkbox\n            v-for=\"permission in row.permissions\"\n            :key=\"permission.id\"\n            v-model:checked=\"permission.checked\"\n            @change=\"() => handlePermissionChange(row)\"\n          >\n            {{ permission.label }}\n          </Checkbox>\n        </div>\n      </template>\n    </BasicTable>\n    <!-- 全屏引导 -->\n    <FullScreenGuide />\n  </div>\n</template>\n\n<style scoped>\n:deep(.ant-alert) {\n  padding: 4px 8px;\n}\n</style>\n"
  },
  {
    "path": "apps/web-antd/src/components/tree/src/tree-select-panel.vue",
    "content": "<script setup lang=\"ts\">\nimport type { CheckboxChangeEvent } from 'ant-design-vue/es/checkbox/interface';\nimport type { DataNode } from 'ant-design-vue/es/tree';\n\nimport { computed, nextTick, onMounted, ref } from 'vue';\n\nimport { treeToList } from '@vben/utils';\n\nimport { Checkbox, Tree } from 'ant-design-vue';\n\n/** 需要禁止透传 */\ndefineOptions({ inheritAttrs: false });\n\nconst props = withDefaults(defineProps<Props>(), {\n  expandAllOnInit: false,\n  fieldNames: () => ({ key: 'id', title: 'label' }),\n  resetOnStrictlyChange: true,\n  treeData: () => [],\n});\n\ninterface Props {\n  /**\n   * 是否展开所有节点 mount\n   */\n  expandAllOnInit?: boolean;\n  /**\n   * 自定义字段\n   */\n  fieldNames?: { key: string; title: string };\n  /**\n   * 点击节点关联/独立时 清空已勾选的节点\n   */\n  resetOnStrictlyChange?: boolean;\n  /**\n   * 树结构数据\n   */\n  treeData?: DataNode[];\n}\n\n/**\n * 展开的状态\n */\nconst expandStatus = ref(false);\n/**\n * 全选状态\n */\nconst selectAllStatus = ref(false);\n\nconst associationText = computed(() => {\n  return checkStrictly.value ? '父子节点关联' : '父子节点独立';\n});\n\n/**\n * 这个只用于界面显示\n * 关联情况下 只会有最末尾的节点被选中\n */\nconst checkedKeys = defineModel<(number | string)[]>('value', {\n  default: () => [],\n});\n\n/**\n * 是否节点关联 后端字段跟前端字段是反的\n */\nconst checkStrictly = defineModel<boolean>('checkStrictly', {\n  default: () => true,\n});\n\nconst computedCheckedKeys = computed<any>({\n  get() {\n    /**\n     * 严格模式(节点不关联)  需要返回{checked: string[] | number[], halfChecked: string[]}\n     * @see https://www.antdv.com/components/tree-cn#tree-props\n     */\n    if (!checkStrictly.value) {\n      return {\n        checked: [...checkedKeys.value],\n        halfChecked: [],\n      };\n    }\n    return checkedKeys.value;\n  },\n  set(v) {\n    if (!checkStrictly.value) {\n      checkedKeys.value = [...v.checked, ...v.halfChecked];\n      return;\n    }\n    checkedKeys.value = v;\n  },\n});\n\n// 所有节点的ID\nconst allKeys = computed(() => {\n  const idField = props.fieldNames.key;\n  return treeToList(props.treeData).map((item: any) => item[idField]);\n});\n\nfunction handleCheckedAllChange(e: CheckboxChangeEvent) {\n  // 这个用于展示\n  checkedKeys.value = e.target.checked ? allKeys.value : [];\n}\n\nconst expandedKeys = ref<string[]>([]);\nfunction handleExpandOrCollapseAll() {\n  expandStatus.value = !expandStatus.value;\n  expandedKeys.value = expandStatus.value ? allKeys.value : [];\n}\n\nfunction handleCheckStrictlyChange() {\n  if (props.resetOnStrictlyChange) {\n    checkedKeys.value = [];\n  }\n}\n\nonMounted(async () => {\n  if (props.expandAllOnInit) {\n    await nextTick();\n    expandedKeys.value = allKeys.value;\n  }\n});\n</script>\n\n<template>\n  <div class=\"bg-background w-full rounded-lg border-[1px] p-[12px]\">\n    <!-- <div class=\"flex flex-col gap-6 text-[13px]\">\n      <div>computedCheckedKeys {{ computedCheckedKeys }}</div>\n      <div>checkedKeys {{ checkedKeys }}</div>\n    </div> -->\n\n    <div class=\"flex items-center justify-between gap-2 border-b-[1px] pb-2\">\n      <div class=\"opacity-75\">\n        <span>节点状态: </span>\n        <span :class=\"[checkStrictly ? 'text-primary' : 'text-red-500']\">\n          {{ associationText }}\n        </span>\n      </div>\n    </div>\n    <div\n      class=\"flex flex-wrap items-center justify-between border-b-[1px] py-2\"\n    >\n      <a-button size=\"small\" @click=\"handleExpandOrCollapseAll\">\n        展开/折叠全部\n      </a-button>\n      <Checkbox\n        v-model:checked=\"selectAllStatus\"\n        @change=\"handleCheckedAllChange\"\n      >\n        全选/取消全选\n      </Checkbox>\n      <Checkbox\n        v-model:checked=\"checkStrictly\"\n        @change=\"handleCheckStrictlyChange\"\n      >\n        父子节点关联\n      </Checkbox>\n    </div>\n    <div class=\"py-2\">\n      <Tree\n        :check-strictly=\"!checkStrictly\"\n        v-model:checked-keys=\"computedCheckedKeys\"\n        v-model:expanded-keys=\"expandedKeys\"\n        :checkable=\"true\"\n        :field-names=\"fieldNames\"\n        :selectable=\"false\"\n        :tree-data=\"treeData\"\n      >\n        <template\n          v-for=\"slotName in Object.keys($slots)\"\n          :key=\"slotName\"\n          #[slotName]=\"data\"\n        >\n          <slot :name=\"slotName\" v-bind=\"data ?? {}\"></slot>\n        </template>\n      </Tree>\n    </div>\n  </div>\n</template>\n\n<style lang=\"scss\" scoped>\n:deep(.ant-tree) {\n  // 勾选框居中\n  & .ant-tree-checkbox {\n    margin: 0;\n    margin-right: 6px;\n  }\n\n  // 展开图标居中\n  & .ant-tree-switcher {\n    display: flex;\n    align-items: center;\n    justify-content: center;\n  }\n}\n</style>\n"
  },
  {
    "path": "apps/web-antd/src/components/upload/index.ts",
    "content": "export { default as FileUpload } from './src/file-upload.vue';\nexport { default as ImageUpload } from './src/image-upload.vue';\n"
  },
  {
    "path": "apps/web-antd/src/components/upload/src/file-upload.vue",
    "content": "<!--\n不再支持url 统一使用ossId\n去除使用`file-type`库进行文件类型检测 在Safari无法使用\n-->\n<script setup lang=\"ts\">\nimport type { UploadListType } from 'ant-design-vue/es/upload/interface';\n\nimport type { BaseUploadProps, UploadEmits } from './props';\n\nimport { computed } from 'vue';\n\nimport { $t, I18nT } from '@vben/locales';\n\nimport { InboxOutlined, UploadOutlined } from '@ant-design/icons-vue';\nimport { Upload } from 'ant-design-vue';\n\nimport { uploadApi } from '#/api';\n\nimport { defaultFileAcceptExts, defaultFilePreview } from './helper';\nimport { useUpload } from './hook';\n\ninterface FileUploadProps extends BaseUploadProps {\n  /**\n   * 同antdv的listType 但是排除picture-card\n   * 文件上传不适合用picture-card显示\n   * @default text\n   */\n  listType?: Exclude<UploadListType, 'picture-card'>;\n}\n\nconst props = withDefaults(defineProps<FileUploadProps>(), {\n  api: () => uploadApi,\n  removeOnError: true,\n  showSuccessMsg: true,\n  removeConfirm: false,\n  accept: defaultFileAcceptExts.join(','),\n  data: () => undefined,\n  maxCount: 1,\n  maxSize: 5,\n  disabled: false,\n  helpMessage: true,\n  preview: defaultFilePreview,\n  enableDragUpload: false,\n  directory: false,\n  abortOnUnmounted: true,\n  listType: 'text',\n});\n\nconst emit = defineEmits<UploadEmits>();\n\n/** 返回不同的上传组件 */\nconst CurrentUploadComponent = computed(() => {\n  if (props.enableDragUpload) {\n    return Upload.Dragger;\n  }\n  return Upload;\n});\n\n// 双向绑定 ossId\nconst ossIdList = defineModel<string | string[]>('value', {\n  default: () => [],\n});\n\nconst {\n  customRequest,\n  acceptStr,\n  handleChange,\n  handleRemove,\n  beforeUpload,\n  innerFileList,\n} = useUpload(props, emit, ossIdList, 'file');\n</script>\n\n<!--\nUpload.Dragger只会影响样式\n使用普通Upload也是支持拖拽上传的\n-->\n<template>\n  <div>\n    <CurrentUploadComponent\n      v-model:file-list=\"innerFileList\"\n      :accept=\"accept\"\n      :list-type=\"listType\"\n      :disabled=\"disabled\"\n      :directory=\"directory\"\n      :max-count=\"maxCount\"\n      :progress=\"{ showInfo: true }\"\n      :multiple=\"multiple\"\n      :before-upload=\"beforeUpload\"\n      :custom-request=\"customRequest\"\n      @preview=\"preview\"\n      @change=\"handleChange\"\n      @remove=\"handleRemove\"\n    >\n      <div v-if=\"!enableDragUpload && innerFileList?.length < maxCount\">\n        <a-button :disabled=\"disabled\">\n          <UploadOutlined />\n          {{ $t('component.upload.upload') }}\n        </a-button>\n      </div>\n      <div v-if=\"enableDragUpload\">\n        <p class=\"ant-upload-drag-icon\">\n          <InboxOutlined />\n        </p>\n        <p class=\"ant-upload-text\">\n          {{ $t('component.upload.clickOrDrag') }}\n        </p>\n      </div>\n    </CurrentUploadComponent>\n    <slot name=\"helpMessage\" v-bind=\"{ maxCount, disabled, maxSize, accept }\">\n      <I18nT\n        v-if=\"helpMessage\"\n        scope=\"global\"\n        keypath=\"component.upload.uploadHelpMessage\"\n        tag=\"div\"\n        class=\"mt-2 text-[14px] leading-[1.5] text-black/45 dark:text-white/45\"\n        :class=\"{ 'upload-text__disabled': disabled }\"\n      >\n        <template #size>\n          <span\n            class=\"text-primary mx-1 font-medium\"\n            :class=\"{ 'upload-text__disabled': disabled }\"\n          >\n            {{ maxSize }}MB\n          </span>\n        </template>\n        <template #ext>\n          <span\n            class=\"text-primary mx-1 font-medium\"\n            :class=\"{ 'upload-text__disabled': disabled }\"\n          >\n            {{ acceptStr }}\n          </span>\n        </template>\n      </I18nT>\n    </slot>\n  </div>\n</template>\n\n<style lang=\"scss\">\n// 禁用的样式和antd保持一致\n.upload-text__disabled {\n  color: rgb(50 54 57 / 25%);\n  cursor: not-allowed;\n\n  &:where(.dark, .dark *) {\n    color: rgb(242 242 242 / 25%);\n  }\n}\n</style>\n"
  },
  {
    "path": "apps/web-antd/src/components/upload/src/helper.ts",
    "content": "import type { UploadFile } from 'ant-design-vue';\n\n/**\n * 默认支持上传的图片文件类型\n */\nexport const defaultImageAcceptExts = [\n  '.jpg',\n  '.jpeg',\n  '.png',\n  '.gif',\n  '.webp',\n];\n\n/**\n * 默认支持上传的文件类型\n */\nexport const defaultFileAcceptExts = ['.xlsx', '.csv', '.docx', '.pdf'];\n\n/**\n * 文件(非图片)的默认预览逻辑\n * 默认: window.open打开 交给浏览器接管\n * @param file file\n */\nexport function defaultFilePreview(file: UploadFile) {\n  if (file?.url) {\n    window.open(file.url);\n  }\n}\n"
  },
  {
    "path": "apps/web-antd/src/components/upload/src/hook.ts",
    "content": "/* eslint-disable @typescript-eslint/no-non-null-assertion */\nimport type { UploadChangeParam, UploadFile } from 'ant-design-vue';\nimport type { FileType } from 'ant-design-vue/es/upload/interface';\nimport type {\n  RcFile,\n  UploadRequestOption,\n} from 'ant-design-vue/es/vc-upload/interface';\n\nimport type { ModelRef } from 'vue';\n\nimport type {\n  BaseUploadProps,\n  CustomGetter,\n  UploadEmits,\n  UploadType,\n} from './props';\n\nimport type { AxiosProgressEvent, UploadResult } from '#/api';\nimport type { OssFile } from '#/api/system/oss/model';\n\nimport { computed, onUnmounted, ref, watch } from 'vue';\n\nimport { $t } from '@vben/locales';\n\nimport { message, Modal } from 'ant-design-vue';\nimport { isFunction, isString } from 'lodash-es';\n\nimport { ossInfo } from '#/api/system/oss';\n\n/**\n * 图片预览hook\n * @returns 预览\n */\nexport function useImagePreview() {\n  /**\n   * 获取base64字符串\n   * @param file 文件\n   * @returns base64字符串\n   */\n  function getBase64(file: File) {\n    return new Promise((resolve, reject) => {\n      const reader = new FileReader();\n      reader.readAsDataURL(file);\n      reader.addEventListener('load', () => resolve(reader.result));\n      reader.addEventListener('error', (error) => reject(error));\n    });\n  }\n\n  // Modal可见\n  const previewVisible = ref(false);\n  // 预览的图片 url/base64\n  const previewImage = ref('');\n  // 预览的图片名称\n  const previewTitle = ref('');\n\n  function handleCancel() {\n    previewVisible.value = false;\n    previewTitle.value = '';\n  }\n\n  async function handlePreview(file: UploadFile) {\n    if (!file) {\n      return;\n    }\n    // 文件预览 取base64\n    if (!file.url && !file.preview && file.originFileObj) {\n      file.preview = (await getBase64(file.originFileObj)) as string;\n    }\n    // 这里不可能为空\n    const url = file.url ?? '';\n    previewImage.value = url || file.preview || '';\n    previewVisible.value = true;\n    previewTitle.value =\n      file.name || url.slice(Math.max(0, url.lastIndexOf('/') + 1));\n  }\n\n  return {\n    previewVisible,\n    previewImage,\n    previewTitle,\n    handleCancel,\n    handlePreview,\n  };\n}\n\n/**\n * 图片上传和文件上传的通用hook\n * @param props 组件props\n * @param emit 事件\n * @param bindValue 双向绑定的idList\n * @param uploadType 区分是文件还是图片上传\n * @returns hook\n */\nexport function useUpload(\n  props: Readonly<BaseUploadProps>,\n  emit: UploadEmits,\n  bindValue: ModelRef<string | string[]>,\n  uploadType: UploadType,\n) {\n  // 组件内部维护fileList\n  const innerFileList = ref<UploadFile[]>([]);\n\n  const acceptStr = computed(() => {\n    // string类型\n    if (isString(props.acceptFormat)) {\n      return props.acceptFormat;\n    }\n    // 函数类型\n    if (isFunction(props.acceptFormat)) {\n      return props.acceptFormat(props.accept!);\n    }\n    // 默认 会对拓展名做处理\n    return props.accept\n      ?.split(',')\n      .map((item) => {\n        if (item.startsWith('.')) {\n          return item.slice(1);\n        }\n        return item;\n      })\n      .join(', ');\n  });\n\n  /**\n   * 自定义文件显示名称 需要区分不同的接口\n   * @param cb callback\n   * @returns 文件名\n   */\n  function transformFilename(cb: Parameters<CustomGetter<string>>[0]) {\n    if (isFunction(props.customFilename)) {\n      return props.customFilename(cb);\n    }\n    // info接口\n    if (cb.type === 'info') {\n      return cb.response.originalName;\n    }\n    // 上传接口\n    return cb.response.fileName;\n  }\n\n  /**\n   * 自定义缩略图 需要区分不同的接口\n   * @param cb callback\n   * @returns 缩略图地址\n   */\n  function transformThumbUrl(cb: Parameters<CustomGetter<undefined>>[0]) {\n    if (isFunction(props.customThumbUrl)) {\n      return props.customThumbUrl(cb);\n    }\n    // image 默认返回图片链接\n    if (uploadType === 'image') {\n      // info接口\n      if (cb.type === 'info') {\n        return cb.response.url;\n      }\n      // 上传接口\n      return cb.response.url;\n    }\n    // 文件默认返回空 走antd默认的预览图逻辑\n    return undefined;\n  }\n\n  // 用来标识是否为上传 这样在watch内部不需要请求api\n  let isUpload = false;\n  function handleChange(info: UploadChangeParam) {\n    /**\n     * 移除当前文件\n     * @param currentFile 当前文件\n     * @param currentFileList 当前所有文件list\n     */\n    function removeCurrentFile(\n      currentFile: UploadChangeParam['file'],\n      currentFileList: UploadChangeParam['fileList'],\n    ) {\n      if (props.removeOnError) {\n        currentFileList.splice(currentFileList.indexOf(currentFile), 1);\n      } else {\n        currentFile.status = 'error';\n      }\n    }\n\n    const { file: currentFile, fileList } = info;\n\n    switch (currentFile.status) {\n      // 上传成功 只是判断httpStatus 200 需要手动判断业务code\n      case 'done': {\n        if (!currentFile.response) {\n          return;\n        }\n        // 获取返回结果 为customRequest的reslove参数\n        // 只有success才会走到这里\n        const { ossId, url } = currentFile.response as UploadResult;\n        currentFile.url = url;\n        currentFile.uid = ossId;\n\n        const cb = {\n          type: 'upload',\n          response: currentFile.response as UploadResult,\n        } as const;\n\n        currentFile.fileName = transformFilename(cb);\n        currentFile.name = transformFilename(cb);\n        currentFile.thumbUrl = transformThumbUrl(cb);\n        // 标记为上传 watch根据值做处理\n        isUpload = true;\n        // ossID添加 单个文件会被当做string\n        if (props.maxCount === 1) {\n          bindValue.value = ossId;\n        } else {\n          // 给默认值\n          if (!Array.isArray(bindValue.value)) {\n            bindValue.value = [];\n          }\n          // 直接使用.value无法触发useForm的更新(原生是正常的) 需要修改地址\n          bindValue.value = [...bindValue.value, ossId];\n        }\n        break;\n      }\n      // 上传失败 网络原因导致httpStatus 不等于200\n      case 'error': {\n        removeCurrentFile(currentFile, fileList);\n      }\n    }\n    emit('change', info);\n  }\n\n  function handleRemove(currentFile: UploadFile) {\n    function remove() {\n      // fileList会自行处理删除 这里只需要处理ossId\n      if (props.maxCount === 1) {\n        bindValue.value = '';\n      } else {\n        (bindValue.value as string[]).splice(\n          bindValue.value.indexOf(currentFile.uid),\n          1,\n        );\n      }\n      // 触发remove事件\n      emit('remove', currentFile);\n    }\n\n    if (!props.removeConfirm) {\n      remove();\n      return true;\n    }\n\n    return new Promise<boolean>((resolve) => {\n      Modal.confirm({\n        title: $t('pages.common.tip'),\n        content: $t('component.upload.confirmDelete', [currentFile.name]),\n        okButtonProps: { danger: true },\n        centered: true,\n        onOk() {\n          resolve(true);\n          remove();\n        },\n        onCancel() {\n          resolve(false);\n        },\n      });\n    });\n  }\n\n  /**\n   * 上传前检测文件大小\n   * 拖拽时候前置会有浏览器自身的accept校验 校验失败不会执行此方法\n   * @param file file\n   * @returns file | false\n   */\n  function beforeUpload(file: FileType) {\n    const isLtMax = file.size / 1024 / 1024 < props.maxSize!;\n    if (!isLtMax) {\n      message.error($t('component.upload.maxSize', [props.maxSize]));\n      return false;\n    }\n    // 大坑 Safari不支持file-type库 去除文件类型的校验\n    return file;\n  }\n\n  const uploadAbort = new AbortController();\n  /**\n   * 自定义上传实现\n   * @param info\n   */\n  async function customRequest(info: UploadRequestOption<any>) {\n    const { api } = props;\n    if (!isFunction(api)) {\n      console.warn('upload api must exist and be a function');\n      return;\n    }\n    try {\n      // 进度条事件\n      const progressEvent: AxiosProgressEvent = (e) => {\n        const percent = Math.trunc((e.loaded / e.total!) * 100);\n        info.onProgress!({ percent });\n      };\n      const res = await api(info.file as File, {\n        onUploadProgress: progressEvent,\n        signal: uploadAbort.signal,\n        otherData: props?.data,\n      });\n      info.onSuccess!(res);\n      if (props.showSuccessMsg) {\n        message.success($t('component.upload.uploadSuccess'));\n      }\n      emit('success', info.file as RcFile, res);\n    } catch (error: any) {\n      console.error(error);\n      info.onError!(error);\n    }\n  }\n\n  onUnmounted(() => {\n    props.abortOnUnmounted && uploadAbort.abort();\n  });\n\n  /**\n   * 这里默认只监听list地址变化 即重新赋值才会触发watch\n   * immediate用于初始化触发\n   */\n  watch(\n    () => bindValue.value,\n    async (value) => {\n      if (value.length === 0) {\n        // 清空绑定值时，同时清空innerFileList，避免外部使用时还能读取到\n        innerFileList.value = [];\n        return;\n      }\n\n      // 上传完毕 不需要调用获取信息接口\n      if (isUpload) {\n        // 清理 使下一次状态可用\n        isUpload = false;\n        return;\n      }\n\n      // 处理URL直接传入的情况（例如编辑时从数据库获取的完整URL）\n      // 检查value是否是URL格式\n      const isUrl = isString(value) && (value.startsWith('http://') || value.startsWith('https://'));\n\n      if (isUrl) {\n        // 直接使用URL，不调用ossInfo\n        const fileitem: UploadFile = {\n          uid: value, // 使用URL作为uid\n          name: value.split('/').pop() || value, // 从URL提取文件名\n          fileName: value.split('/').pop() || value,\n          url: value,\n          thumbUrl: value,\n          status: 'done',\n        };\n        innerFileList.value = [fileitem];\n        return;\n      }\n\n      const resp = await ossInfo(value);\n      function transformFile(info: OssFile) {\n        const cb = { type: 'info', response: info } as const;\n\n        const fileitem: UploadFile = {\n          uid: info.ossId,\n          name: transformFilename(cb),\n          fileName: transformFilename(cb),\n          url: info.url,\n          thumbUrl: transformThumbUrl(cb),\n          status: 'done',\n        };\n        return fileitem;\n      }\n      const transformOptions = resp.map((item) => transformFile(item));\n      innerFileList.value = transformOptions;\n      // 单文件 丢弃策略\n      if (props.maxCount === 1 && resp.length === 0 && !props.keepMissingId) {\n        bindValue.value = '';\n        return;\n      }\n      // 多文件\n      // 单文件查到了也会走这里的逻辑 filter会报错 需要maxCount判断处理\n      if (\n        resp.length !== value.length &&\n        !props.keepMissingId &&\n        props.maxCount !== 1\n      ) {\n        // 给默认值\n        if (!Array.isArray(bindValue.value)) {\n          bindValue.value = [];\n        }\n        bindValue.value = bindValue.value.filter((ossId) =>\n          resp.map((res) => res.ossId).includes(ossId),\n        );\n      }\n    },\n    { immediate: true },\n  );\n\n  return {\n    handleChange,\n    handleRemove,\n    beforeUpload,\n    customRequest,\n    innerFileList,\n    acceptStr,\n  };\n}\n"
  },
  {
    "path": "apps/web-antd/src/components/upload/src/image-upload.vue",
    "content": "<!--\n不再支持url 统一使用ossId\n去除使用`file-type`库进行文件类型检测 在Safari无法使用\n-->\n<script setup lang=\"ts\">\nimport type {\n  UploadFile,\n  UploadListType,\n} from 'ant-design-vue/es/upload/interface';\n\nimport type { BaseUploadProps, UploadEmits } from './props';\n\nimport { $t, I18nT } from '@vben/locales';\n\nimport { PlusOutlined, UploadOutlined } from '@ant-design/icons-vue';\nimport { Image, ImagePreviewGroup, Upload } from 'ant-design-vue';\nimport { isFunction } from 'lodash-es';\n\nimport { uploadApi } from '#/api';\n\nimport { defaultImageAcceptExts } from './helper';\nimport { useImagePreview, useUpload } from './hook';\n\ninterface ImageUploadProps extends BaseUploadProps {\n  /**\n   * 同antdv的listType\n   * @default picture-card\n   */\n  listType?: UploadListType;\n  /**\n   * 使用list-type: picture-card时 是否显示动画\n   * 会有一个`弹跳`的效果 默认关闭\n   * @default false\n   */\n  withAnimation?: boolean;\n}\n\nconst props = withDefaults(defineProps<ImageUploadProps>(), {\n  api: () => uploadApi,\n  removeOnError: true,\n  showSuccessMsg: true,\n  removeConfirm: false,\n  accept: defaultImageAcceptExts.join(','),\n  data: () => undefined,\n  maxCount: 1,\n  maxSize: 5,\n  disabled: false,\n  listType: 'picture-card',\n  helpMessage: true,\n  enableDragUpload: false,\n  abortOnUnmounted: true,\n  withAnimation: false,\n});\n\nconst emit = defineEmits<UploadEmits>();\n\n// 双向绑定 ossId\nconst ossIdList = defineModel<string | string[]>('value', {\n  default: () => [],\n});\n\nconst {\n  acceptStr,\n  handleChange,\n  handleRemove,\n  beforeUpload,\n  innerFileList,\n  customRequest,\n} = useUpload(props, emit, ossIdList, 'image');\n\nconst { previewVisible, previewImage, handleCancel, handlePreview } =\n  useImagePreview();\n\nfunction currentPreview(file: UploadFile) {\n  // 有自定义预览逻辑走自定义\n  if (isFunction(props.preview)) {\n    return props.preview(file);\n  }\n  // 否则走默认预览\n  return handlePreview(file);\n}\n</script>\n\n<template>\n  <div>\n    <Upload\n      v-model:file-list=\"innerFileList\"\n      :class=\"{ 'upload-animation__disabled': !withAnimation }\"\n      :list-type=\"listType\"\n      :accept=\"accept\"\n      :disabled=\"disabled\"\n      :directory=\"directory\"\n      :max-count=\"maxCount\"\n      :progress=\"{ showInfo: true }\"\n      :multiple=\"multiple\"\n      :before-upload=\"beforeUpload\"\n      :custom-request=\"customRequest\"\n      @preview=\"currentPreview\"\n      @change=\"handleChange\"\n      @remove=\"handleRemove\"\n    >\n      <div\n        v-if=\"innerFileList?.length < maxCount && listType === 'picture-card'\"\n      >\n        <PlusOutlined />\n        <div class=\"mt-[8px]\">{{ $t('component.upload.upload') }}</div>\n      </div>\n      <a-button\n        v-if=\"innerFileList?.length < maxCount && listType !== 'picture-card'\"\n        :disabled=\"disabled\"\n      >\n        <UploadOutlined />\n        {{ $t('component.upload.upload') }}\n      </a-button>\n    </Upload>\n    <slot name=\"helpMessage\" v-bind=\"{ maxCount, disabled, maxSize, accept }\">\n      <I18nT\n        v-if=\"helpMessage\"\n        scope=\"global\"\n        keypath=\"component.upload.uploadHelpMessage\"\n        tag=\"div\"\n        class=\"text-[14px] leading-[1.5] text-black/45 dark:text-white/45\"\n        :class=\"{\n          'upload-text__disabled': disabled,\n          'mt-2': listType !== 'picture-card',\n        }\"\n      >\n        <template #size>\n          <span\n            class=\"text-primary mx-1 font-medium\"\n            :class=\"{ 'upload-text__disabled': disabled }\"\n          >\n            {{ maxSize }}MB\n          </span>\n        </template>\n        <template #ext>\n          <span\n            class=\"text-primary mx-1 font-medium\"\n            :class=\"{ 'upload-text__disabled': disabled }\"\n          >\n            {{ acceptStr }}\n          </span>\n        </template>\n      </I18nT>\n    </slot>\n\n    <ImagePreviewGroup\n      :preview=\"{\n        visible: previewVisible,\n        onVisibleChange: handleCancel,\n      }\"\n    >\n      <Image class=\"hidden\" :src=\"previewImage\" />\n    </ImagePreviewGroup>\n  </div>\n</template>\n\n<style lang=\"scss\">\n.ant-upload-select-picture-card {\n  i {\n    @apply text-[32px] text-[#999];\n  }\n\n  .ant-upload-text {\n    @apply mt-[8px] text-[#666];\n  }\n}\n\n.ant-upload-list-picture-card {\n  .ant-upload-list-item::before {\n    border-radius: 4px;\n  }\n}\n\n// 禁用的样式和antd保持一致\n.upload-text__disabled {\n  color: rgb(50 54 57 / 25%);\n  cursor: not-allowed;\n\n  &:where(.dark, .dark *) {\n    color: rgb(242 242 242 / 25%);\n  }\n}\n\n// list-type: picture-card动画效果关闭样式\n.upload-animation__disabled {\n  .ant-upload-animate-inline {\n    animation-duration: 0s !important;\n  }\n}\n</style>\n"
  },
  {
    "path": "apps/web-antd/src/components/upload/src/note.md",
    "content": "Safari在执行到beforeUpload方法\n\n有两种情况\n\n1. 不继续执行 也无法上传(没有调用上传)\n2. 报错\n\nUnhandled Promise Rejection: TypeError: ReadableStreamBYOBReader needs a ReadableByteStreamController\n\nhttps://github.com/oven-sh/bun/issues/12908#issuecomment-2490151231\n\n刚开始以为是异步的问题 由于`file-type`调用了异步方法 调试也是在这里没有后续打印了\n\n使用别的异步代码测试结果是正常上传的\n\n```js\nreturn new Promise<FileType>((resolve) =>\n  setTimeout(() => resolve(file), 2000),\n);\n```\n\n根本原因在于`file-typ`库的`fileTypeFromBlob`方法不支持Safari 去掉可以正常上传\n\nsafari不支持`ReadableStreamBYOBReader`api\n\n详见: https://github.com/sindresorhus/file-type/issues/690\n"
  },
  {
    "path": "apps/web-antd/src/components/upload/src/props.d.ts",
    "content": "import type { UploadFile } from 'ant-design-vue';\nimport type { RcFile } from 'ant-design-vue/es/vc-upload/interface';\n\nimport type { UploadApi, UploadResult } from '#/api';\nimport type { OssFile } from '#/api/system/oss/model';\n\nimport { UploadChangeParam } from 'ant-design-vue';\n\nexport type UploadType = 'file' | 'image';\n\n/**\n * 自定义返回文件名/缩略图使用 泛型控制返回是否必填\n * type 为不同的接口返回值 需要自行if判断\n */\nexport type CustomGetter<T extends string | undefined> = (\n  cb:\n    | { response: OssFile; type: 'info' }\n    | { response: UploadResult; type: 'upload' },\n) => T extends undefined ? string | undefined : string;\n\nexport interface BaseUploadProps {\n  /**\n   * 上传接口\n   */\n  api?: UploadApi;\n  /**\n   * 文件上传失败 是否从展示列表中删除\n   * @default true\n   */\n  removeOnError?: boolean;\n  /**\n   * 上传成功 是否展示提示信息\n   * @default true\n   */\n  showSuccessMsg?: boolean;\n  /**\n   * 删除文件前是否需要确认\n   * @default false\n   */\n  removeConfirm?: boolean;\n  /**\n   * 同antdv参数\n   */\n  accept?: string;\n  /**\n   * 你可能使用的是application/pdf这种mime类型, 但是这样用户可能看不懂, 在这里自定义逻辑\n   * @default 原始accept\n   */\n  acceptFormat?: ((accept: string) => string) | string;\n  /**\n   * 附带的请求参数\n   */\n  data?: any;\n  /**\n   * 最大上传图片数量\n   * maxCount为1时 会被绑定为string而非string[]\n   * @default 1\n   */\n  maxCount?: number;\n  /**\n   * 文件最大 单位M\n   * @default 5\n   */\n  maxSize?: number;\n  /**\n   * 是否禁用\n   * @default false\n   */\n  disabled?: boolean;\n  /**\n   * 是否显示文案 请上传不超过...\n   * @default true\n   */\n  helpMessage?: boolean;\n  /**\n   * 是否支持多选文件，ie10+ 支持。开启后按住 ctrl 可选择多个文件。\n   * @default false\n   */\n  multiple?: boolean;\n  /**\n   * 是否支持上传文件夹\n   * @default false\n   */\n  directory?: boolean;\n  /**\n   * 是否支持拖拽上传\n   * @default false\n   */\n  enableDragUpload?: boolean;\n  /**\n   * 当ossId查询不到文件信息时  比如被删除了\n   * 是否保留列表对应的ossId 默认不保留\n   * @default false\n   */\n  keepMissingId?: boolean;\n  /**\n   * 自定义文件/图片预览逻辑 比如: 你可以改为下载\n   * 图片上传默认为预览\n   * 文件上传默认为window.open\n   * @param file file\n   */\n  preview?: (file: UploadFile) => Promise<void> | void;\n  /**\n   * 是否在组件Unmounted时取消上传\n   * @default true\n   */\n  abortOnUnmounted?: boolean;\n  /**\n   * 自定义文件名 需要区分两个接口的返回值\n   */\n  customFilename?: CustomGetter<string>;\n  /**\n   * 自定义缩略图 需要区分两个接口的返回值\n   */\n  customThumbUrl?: CustomGetter<undefined>;\n}\n\nexport interface UploadEmits {\n  (e: 'success', file: RcFile, response: UploadResult): void;\n  (e: 'remove', file: UploadFile): void;\n  (e: 'change', info: UploadChangeParam): void;\n}\n"
  },
  {
    "path": "apps/web-antd/src/components/upload-old/index.ts",
    "content": "/**\n * @description: 旧版文件上传组件 使用FileUpload代替\n */\nexport { default as FileUploadOld } from './src/file-upload.vue';\n/**\n * @description: 旧版图片上传组件 使用ImageUpload代替\n */\nexport { default as ImageUploadOld } from './src/image-upload.vue';\n"
  },
  {
    "path": "apps/web-antd/src/components/upload-old/src/file-upload.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { UploadFile, UploadProps } from 'ant-design-vue';\nimport type { UploadRequestOption } from 'ant-design-vue/lib/vc-upload/interface';\n\nimport type { AxiosProgressEvent, UploadApi } from '#/api';\n\nimport { ref, toRefs, watch } from 'vue';\n\nimport { $t } from '@vben/locales';\n\nimport { UploadOutlined } from '@ant-design/icons-vue';\nimport { message, Upload } from 'ant-design-vue';\nimport { isArray, isFunction, isObject, isString } from 'lodash-es';\n\nimport { uploadApi } from '#/api';\n\nimport { checkFileType } from './helper';\nimport { UploadResultStatus } from './typing';\nimport { useUploadType } from './use-upload';\n\ndefineOptions({ name: 'FileUpload', inheritAttrs: false });\n\nconst props = withDefaults(\n  defineProps<{\n    /**\n     * 建议使用拓展名(不带.)\n     * 或者文件头 image/png等(测试判断不准确)  不支持image/*类似的写法\n     * 需自行改造 ./helper/checkFileType方法\n     */\n    accept?: string[];\n    api?: UploadApi;\n    disabled?: boolean;\n    helpText?: string;\n    // 最大数量的文件，Infinity不限制\n    maxNumber?: number;\n    // 文件最大多少MB\n    maxSize?: number;\n    // 是否支持多选\n    multiple?: boolean;\n    // support xxx.xxx.xx\n    // 返回的字段 默认url\n    resultField?: 'fileName' | 'ossId' | 'url' | string;\n    /**\n     * 是否显示下面的描述\n     */\n    showDescription?: boolean;\n    value?: string[];\n  }>(),\n  {\n    value: () => [],\n    disabled: false,\n    helpText: '',\n    maxSize: 2,\n    maxNumber: 1,\n    accept: () => [],\n    multiple: false,\n    api: () => uploadApi,\n    resultField: '',\n    showDescription: true,\n  },\n);\nconst emit = defineEmits(['change', 'update:value', 'delete']);\nconst { accept, helpText, maxNumber, maxSize } = toRefs(props);\nconst isInnerOperate = ref<boolean>(false);\nconst { getStringAccept } = useUploadType({\n  acceptRef: accept,\n  helpTextRef: helpText,\n  maxNumberRef: maxNumber,\n  maxSizeRef: maxSize,\n});\n\nconst fileList = ref<UploadProps['fileList']>([]);\nconst isLtMsg = ref<boolean>(true);\nconst isActMsg = ref<boolean>(true);\nconst isFirstRender = ref<boolean>(true);\n\nwatch(\n  () => props.value,\n  (v) => {\n    if (isInnerOperate.value) {\n      isInnerOperate.value = false;\n      return;\n    }\n    let value: string[] = [];\n    if (v) {\n      if (isArray(v)) {\n        value = v;\n      } else {\n        value.push(v);\n      }\n      fileList.value = value.map((item, i) => {\n        if (item && isString(item)) {\n          return {\n            uid: `${-i}`,\n            name: item.slice(Math.max(0, item.lastIndexOf('/') + 1)),\n            status: 'done',\n            url: item,\n          };\n        } else if (item && isObject(item)) {\n          return item;\n        }\n        return null;\n      }) as UploadProps['fileList'];\n    }\n    if (!isFirstRender.value) {\n      emit('change', value);\n      isFirstRender.value = false;\n    }\n  },\n  {\n    immediate: true,\n    deep: true,\n  },\n);\n\nconst handleRemove = async (file: UploadFile) => {\n  if (fileList.value) {\n    const index = fileList.value.findIndex((item) => item.uid === file.uid);\n    index !== -1 && fileList.value.splice(index, 1);\n    const value = getValue();\n    isInnerOperate.value = true;\n    emit('update:value', value);\n    emit('change', value);\n    emit('delete', file);\n  }\n};\n\nconst beforeUpload = async (file: File) => {\n  const { maxSize, accept } = props;\n  const isAct = await checkFileType(file, accept);\n  if (!isAct) {\n    message.error($t('component.upload.acceptUpload', [accept]));\n    isActMsg.value = false;\n    // 防止弹出多个错误提示\n    setTimeout(() => (isActMsg.value = true), 1000);\n  }\n  const isLt = file.size / 1024 / 1024 > maxSize;\n  if (isLt) {\n    message.error($t('component.upload.maxSizeMultiple', [maxSize]));\n    isLtMsg.value = false;\n    // 防止弹出多个错误提示\n    setTimeout(() => (isLtMsg.value = true), 1000);\n  }\n  return (isAct && !isLt) || Upload.LIST_IGNORE;\n};\n\nasync function customRequest(info: UploadRequestOption<any>) {\n  const { api } = props;\n  if (!api || !isFunction(api)) {\n    console.warn('upload api must exist and be a function');\n    return;\n  }\n  try {\n    // 进度条事件\n    const progressEvent: AxiosProgressEvent = (e) => {\n      const percent = Math.trunc((e.loaded / e.total!) * 100);\n      info.onProgress!({ percent });\n    };\n    const res = await api?.(info.file as File, {\n      onUploadProgress: progressEvent,\n    });\n    /**\n     * 由getValue处理 传对象过去\n     * 直接传string(id)会被转为Number\n     * 内部的逻辑由requestClient.upload处理 这里不用判断业务状态码 不符合会自动reject\n     */\n    info.onSuccess!(res);\n    message.success($t('component.upload.uploadSuccess'));\n    // 获取\n    const value = getValue();\n    isInnerOperate.value = true;\n    emit('update:value', value);\n    emit('change', value);\n  } catch (error: any) {\n    console.error(error);\n    info.onError!(error);\n  }\n}\n\nfunction getValue() {\n  const list = (fileList.value || [])\n    .filter((item) => item?.status === UploadResultStatus.DONE)\n    .map((item: any) => {\n      if (item?.response && props?.resultField) {\n        return item?.response?.[props.resultField];\n      }\n      // 适用于已经有图片 回显的情况 会默认在init处理为{url: 'xx'}\n      if (item?.url) {\n        return item.url;\n      }\n      // 注意这里取的key为 url\n      return item?.response?.url;\n    });\n  return list;\n}\n</script>\n\n<template>\n  <div>\n    <Upload\n      v-bind=\"$attrs\"\n      v-model:file-list=\"fileList\"\n      :accept=\"getStringAccept\"\n      :before-upload=\"beforeUpload\"\n      :custom-request=\"customRequest\"\n      :disabled=\"disabled\"\n      :max-count=\"maxNumber\"\n      :multiple=\"multiple\"\n      list-type=\"text\"\n      :progress=\"{ showInfo: true }\"\n      @remove=\"handleRemove\"\n    >\n      <div v-if=\"fileList && fileList.length < maxNumber\">\n        <a-button>\n          <UploadOutlined />\n          {{ $t('component.upload.upload') }}\n        </a-button>\n      </div>\n      <div v-if=\"showDescription\" class=\"mt-2 flex flex-wrap items-center\">\n        请上传不超过\n        <div class=\"text-primary mx-1 font-bold\">{{ maxSize }}MB</div>\n        的\n        <div class=\"text-primary mx-1 font-bold\">{{ accept.join('/') }}</div>\n        格式文件\n      </div>\n    </Upload>\n  </div>\n</template>\n\n<style>\n.ant-upload-select-picture-card i {\n  font-size: 32px;\n  color: #999;\n}\n\n.ant-upload-select-picture-card .ant-upload-text {\n  margin-top: 8px;\n  color: #666;\n}\n</style>\n"
  },
  {
    "path": "apps/web-antd/src/components/upload-old/src/helper.ts",
    "content": "import { fileTypeFromBlob } from '@vben/utils';\n\n/**\n * 不支持txt文件 @see https://github.com/sindresorhus/file-type/issues/55\n * 需要自行修改\n * @param file file对象\n * @param accepts 文件类型数组  包括拓展名(不带点) 文件头(image/png等 不包括泛写法即image/*)\n * @returns 是否通过文件类型校验\n */\nexport async function checkFileType(file: File, accepts: string[]) {\n  if (!accepts || accepts?.length === 0) {\n    return true;\n  }\n  console.log(file);\n  const fileType = await fileTypeFromBlob(file);\n  if (!fileType) {\n    console.error('无法获取文件类型');\n    return false;\n  }\n  console.log('文件类型', fileType);\n  // 是否文件拓展名/文件头任意有一个匹配\n  return accepts.includes(fileType.ext) || accepts.includes(fileType.mime);\n}\n\n/**\n * 默认图片类型\n */\nexport const defaultImageAccept = ['jpg', 'jpeg', 'png', 'gif', 'webp'];\n/**\n * 判断文件类型是否符合要求\n * @param file file对象\n * @param accepts 文件类型数组  包括拓展名(不带点) 文件头(image/png等 不包括泛写法即image/*)\n * @returns 是否通过文件类型校验\n */\nexport async function checkImageFileType(file: File, accepts: string[]) {\n  // 空的accepts 使用默认规则\n  if (!accepts || accepts.length === 0) {\n    accepts = defaultImageAccept;\n  }\n  const fileType = await fileTypeFromBlob(file);\n  if (!fileType) {\n    console.error('无法获取文件类型');\n    return false;\n  }\n  console.log('文件类型', fileType);\n  // 是否文件拓展名/文件头任意有一个匹配\n  if (accepts.includes(fileType.ext) || accepts.includes(fileType.mime)) {\n    return true;\n  }\n  return false;\n}\n"
  },
  {
    "path": "apps/web-antd/src/components/upload-old/src/image-upload.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { UploadFile, UploadProps } from 'ant-design-vue';\nimport type { UploadRequestOption } from 'ant-design-vue/lib/vc-upload/interface';\n\nimport type { AxiosProgressEvent, UploadApi } from '#/api';\n\nimport { ref, toRefs, watch } from 'vue';\n\nimport { $t } from '@vben/locales';\n\nimport { PlusOutlined } from '@ant-design/icons-vue';\nimport { message, Modal, Upload } from 'ant-design-vue';\nimport { isArray, isFunction, isObject, isString, uniqueId } from 'lodash-es';\n\nimport { uploadApi } from '#/api';\nimport { ossInfo } from '#/api/system/oss';\n\nimport { checkImageFileType, defaultImageAccept } from './helper';\nimport { UploadResultStatus } from './typing';\nimport { useUploadType } from './use-upload';\n\ndefineOptions({ name: 'ImageUpload', inheritAttrs: false });\n\nconst props = withDefaults(\n  defineProps<{\n    /**\n     * 包括拓展名(不带点) 文件头(image/png等 不包括泛写法即image/*)\n     */\n    accept?: string[];\n    api?: UploadApi;\n    disabled?: boolean;\n    helpText?: string;\n    // eslint-disable-next-line no-use-before-define\n    listType?: ListType;\n    // 最大数量的文件，Infinity不限制\n    maxNumber?: number;\n    // 文件最大多少MB\n    maxSize?: number;\n    // 是否支持多选\n    multiple?: boolean;\n    // support xxx.xxx.xx\n    // 返回的字段 默认url\n    resultField?: 'fileName' | 'ossId' | 'url';\n    /**\n     * 是否显示下面的描述\n     */\n    showDescription?: boolean;\n    value?: string | string[];\n  }>(),\n  {\n    value: () => [],\n    disabled: false,\n    listType: 'picture-card',\n    helpText: '',\n    maxSize: 2,\n    maxNumber: 1,\n    accept: () => defaultImageAccept,\n    multiple: false,\n    api: () => uploadApi,\n    resultField: 'url',\n    showDescription: true,\n  },\n);\nconst emit = defineEmits(['change', 'update:value', 'delete']);\ntype ListType = 'picture' | 'picture-card' | 'text';\nconst { accept, helpText, maxNumber, maxSize } = toRefs(props);\nconst isInnerOperate = ref<boolean>(false);\nconst { getStringAccept } = useUploadType({\n  acceptRef: accept,\n  helpTextRef: helpText,\n  maxNumberRef: maxNumber,\n  maxSizeRef: maxSize,\n});\nconst previewOpen = ref<boolean>(false);\nconst previewImage = ref<string>('');\nconst previewTitle = ref<string>('');\n\nconst fileList = ref<UploadProps['fileList']>([]);\nconst isLtMsg = ref<boolean>(true);\nconst isActMsg = ref<boolean>(true);\nconst isFirstRender = ref<boolean>(true);\n\nwatch(\n  () => props.value,\n  async (v) => {\n    if (isInnerOperate.value) {\n      isInnerOperate.value = false;\n      return;\n    }\n    let value: string | string[] = [];\n    if (v) {\n      const _fileList: string[] = [];\n      if (isString(v)) {\n        _fileList.push(v);\n      }\n      if (isArray(v)) {\n        _fileList.push(...v);\n      }\n      // 直接赋值 可能为string | string[]\n      value = v;\n      const withUrlList: UploadProps['fileList'] = [];\n      for (const item of _fileList) {\n        // ossId情况\n        if (props.resultField === 'ossId') {\n          const resp = await ossInfo([item]);\n          if (item && isString(item)) {\n            withUrlList.push({\n              uid: item, // ossId作为uid 方便getValue获取\n              name: item.slice(Math.max(0, item.lastIndexOf('/') + 1)),\n              status: 'done',\n              url: resp?.[0]?.url,\n            });\n          } else if (item && isObject(item)) {\n            withUrlList.push({\n              ...(item as any),\n              uid: item,\n              url: resp?.[0]?.url,\n            });\n          }\n        } else {\n          // 非ossId情况\n          if (item && isString(item)) {\n            withUrlList.push({\n              uid: uniqueId(),\n              name: item.slice(Math.max(0, item.lastIndexOf('/') + 1)),\n              status: 'done',\n              url: item,\n            });\n          } else if (item && isObject(item)) {\n            withUrlList.push(item);\n          }\n        }\n      }\n      fileList.value = withUrlList;\n    }\n    if (!isFirstRender.value) {\n      emit('change', value);\n      isFirstRender.value = false;\n    }\n  },\n  {\n    immediate: true,\n    deep: true,\n  },\n);\n\nfunction getBase64<T extends ArrayBuffer | null | string>(file: File) {\n  return new Promise<T>((resolve, reject) => {\n    const reader = new FileReader();\n    reader.readAsDataURL(file);\n    reader.addEventListener('load', () => {\n      resolve(reader.result as T);\n    });\n    reader.addEventListener('error', (error) => reject(error));\n  });\n}\n\nconst handlePreview = async (file: UploadFile) => {\n  if (!file.url && !file.preview) {\n    file.preview = await getBase64<string>(file.originFileObj!);\n  }\n  previewImage.value = file.url || file.preview || '';\n  previewOpen.value = true;\n  previewTitle.value =\n    file.name ||\n    previewImage.value.slice(\n      Math.max(0, previewImage.value.lastIndexOf('/') + 1),\n    );\n};\n\nconst handleRemove = async (file: UploadFile) => {\n  if (fileList.value) {\n    const index = fileList.value.findIndex((item) => item.uid === file.uid);\n    index !== -1 && fileList.value.splice(index, 1);\n    const value = getValue();\n    isInnerOperate.value = true;\n    emit('update:value', value);\n    emit('change', value);\n    emit('delete', file);\n  }\n};\n\nconst handleCancel = () => {\n  previewOpen.value = false;\n  previewTitle.value = '';\n};\n\nconst beforeUpload = async (file: File) => {\n  const { maxSize, accept } = props;\n  const isAct = await checkImageFileType(file, accept);\n  if (!isAct) {\n    message.error($t('component.upload.acceptUpload', [accept]));\n    isActMsg.value = false;\n    // 防止弹出多个错误提示\n    setTimeout(() => (isActMsg.value = true), 1000);\n  }\n  const isLt = file.size / 1024 / 1024 > maxSize;\n  if (isLt) {\n    message.error($t('component.upload.maxSizeMultiple', [maxSize]));\n    isLtMsg.value = false;\n    // 防止弹出多个错误提示\n    setTimeout(() => (isLtMsg.value = true), 1000);\n  }\n  return (isAct && !isLt) || Upload.LIST_IGNORE;\n};\n\nasync function customRequest(info: UploadRequestOption<any>) {\n  const { api } = props;\n  if (!api || !isFunction(api)) {\n    console.warn('upload api must exist and be a function');\n    return;\n  }\n  try {\n    // 进度条事件\n    const progressEvent: AxiosProgressEvent = (e) => {\n      const percent = Math.trunc((e.loaded / e.total!) * 100);\n      info.onProgress!({ percent });\n    };\n    const res = await api?.(info.file as File, {\n      onUploadProgress: progressEvent,\n    });\n    /**\n     * 由getValue处理 传对象过去\n     * 直接传string(id)会被转为Number\n     * 内部的逻辑由requestClient.upload处理 这里不用判断业务状态码 不符合会自动reject\n     */\n    info.onSuccess!(res);\n    message.success($t('component.upload.uploadSuccess'));\n    // 获取\n    const value = getValue();\n    isInnerOperate.value = true;\n    emit('update:value', value);\n    emit('change', value);\n  } catch (error: any) {\n    console.error(error);\n    info.onError!(error);\n  }\n}\n\nfunction getValue() {\n  console.log(fileList.value);\n  const list = (fileList.value || [])\n    .filter((item) => item?.status === UploadResultStatus.DONE)\n    .map((item: any) => {\n      if (item?.response && props?.resultField) {\n        return item?.response?.[props.resultField];\n      }\n      // ossId兼容 uid为ossId直接返回\n      if (props.resultField === 'ossId' && item.uid) {\n        return item.uid;\n      }\n      // 适用于已经有图片 回显的情况 会默认在init处理为{url: 'xx'}\n      if (item?.url) {\n        return item.url;\n      }\n      // 注意这里取的key为 url\n      return item?.response?.url;\n    });\n  // 只有一张图片 默认绑定string而非string[]\n  if (props.maxNumber === 1 && list.length === 1) {\n    return list[0];\n  }\n  // 只有一张图片 && 删除图片时 可自行修改\n  if (props.maxNumber === 1 && list.length === 0) {\n    return '';\n  }\n  return list;\n}\n</script>\n\n<template>\n  <div>\n    <Upload\n      v-bind=\"$attrs\"\n      v-model:file-list=\"fileList\"\n      :accept=\"getStringAccept\"\n      :before-upload=\"beforeUpload\"\n      :custom-request=\"customRequest\"\n      :disabled=\"disabled\"\n      :list-type=\"listType\"\n      :max-count=\"maxNumber\"\n      :multiple=\"multiple\"\n      :progress=\"{ showInfo: true }\"\n      @preview=\"handlePreview\"\n      @remove=\"handleRemove\"\n    >\n      <div v-if=\"fileList && fileList.length < maxNumber\">\n        <PlusOutlined />\n        <div style=\"margin-top: 8px\">{{ $t('component.upload.upload') }}</div>\n      </div>\n    </Upload>\n    <div\n      v-if=\"showDescription\"\n      class=\"mt-2 flex flex-wrap items-center text-[14px]\"\n    >\n      请上传不超过\n      <div class=\"text-primary mx-1 font-bold\">{{ maxSize }}MB</div>\n      的\n      <div class=\"text-primary mx-1 font-bold\">{{ accept.join('/') }}</div>\n      格式文件\n    </div>\n    <Modal\n      :footer=\"null\"\n      :open=\"previewOpen\"\n      :title=\"previewTitle\"\n      @cancel=\"handleCancel\"\n    >\n      <img :src=\"previewImage\" alt=\"\" style=\"width: 100%\" />\n    </Modal>\n  </div>\n</template>\n\n<style>\n.ant-upload-select-picture-card i {\n  font-size: 32px;\n  color: #999;\n}\n\n.ant-upload-select-picture-card .ant-upload-text {\n  margin-top: 8px;\n  color: #666;\n}\n</style>\n"
  },
  {
    "path": "apps/web-antd/src/components/upload-old/src/typing.ts",
    "content": "import type { Recordable } from '@vben/types';\n\nexport enum UploadResultStatus {\n  DONE = 'done',\n  ERROR = 'error',\n  SUCCESS = 'success',\n  UPLOADING = 'uploading',\n}\n\nexport interface FileItem {\n  thumbUrl?: string;\n  name: string;\n  size: number | string;\n  type?: string;\n  percent: number;\n  file: File;\n  status?: UploadResultStatus;\n  response?: Recordable<any> | { fileName: string; ossId: string; url: string };\n  uuid: string;\n}\n\nexport interface Wrapper {\n  record: FileItem;\n  uidKey: string;\n  valueKey: string;\n}\n\nexport interface BaseFileItem {\n  uid: number | string;\n  url: string;\n  name?: string;\n}\nexport interface PreviewFileItem {\n  url: string;\n  name: string;\n  type: string;\n}\n"
  },
  {
    "path": "apps/web-antd/src/components/upload-old/src/use-upload.ts",
    "content": "import type { Ref } from 'vue';\n\nimport { computed, unref } from 'vue';\n\nimport { $t } from '@vben/locales';\n\nexport function useUploadType({\n  acceptRef,\n  helpTextRef,\n  maxNumberRef,\n  maxSizeRef,\n}: {\n  acceptRef: Ref<string[]>;\n  helpTextRef: Ref<string>;\n  maxNumberRef: Ref<number>;\n  maxSizeRef: Ref<number>;\n}) {\n  // 文件类型限制\n  const getAccept = computed(() => {\n    const accept = unref(acceptRef);\n    if (accept && accept.length > 0) {\n      return accept;\n    }\n    return [];\n  });\n  const getStringAccept = computed(() => {\n    return unref(getAccept)\n      .map((item) => {\n        return item.indexOf('/') > 0 || item.startsWith('.')\n          ? item\n          : `.${item}`;\n      })\n      .join(',');\n  });\n\n  // 支持jpg、jpeg、png格式，不超过2M，最多可选择10张图片，。\n  const getHelpText = computed(() => {\n    const helpText = unref(helpTextRef);\n    if (helpText) {\n      return helpText;\n    }\n    const helpTexts: string[] = [];\n\n    const accept = unref(acceptRef);\n    if (accept.length > 0) {\n      helpTexts.push($t('component.upload.accept', [accept.join(',')]));\n    }\n\n    const maxSize = unref(maxSizeRef);\n    if (maxSize) {\n      helpTexts.push($t('component.upload.maxSize', [maxSize]));\n    }\n\n    const maxNumber = unref(maxNumberRef);\n    if (maxNumber && maxNumber !== Infinity) {\n      helpTexts.push($t('component.upload.maxNumber', [maxNumber]));\n    }\n    return helpTexts.join('，');\n  });\n  return { getAccept, getStringAccept, getHelpText };\n}\n"
  },
  {
    "path": "apps/web-antd/src/layouts/auth.vue",
    "content": "<script lang=\"ts\" setup>\nimport { computed } from 'vue';\n\nimport { AuthPageLayout } from '@vben/layouts';\nimport { preferences } from '@vben/preferences';\n\nimport { $t } from '#/locales';\n\nconst appName = computed(() => preferences.app.name);\nconst logo = computed(() => preferences.logo.source);\n</script>\n\n<template>\n  <AuthPageLayout\n    :app-name=\"appName\"\n    :logo=\"logo\"\n    :page-description=\"$t('authentication.pageDesc')\"\n    :page-title=\"$t('authentication.pageTitle')\"\n  >\n    <!-- 自定义工具栏 -->\n    <!-- <template #toolbar></template> -->\n  </AuthPageLayout>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/layouts/basic.vue",
    "content": "<script lang=\"ts\" setup>\nimport { computed, h, onMounted, watch } from 'vue';\nimport { useRouter } from 'vue-router';\n\nimport { AuthenticationLoginExpiredModal } from '@vben/common-ui';\nimport { VBEN_DOC_URL, VBEN_GITHUB_URL } from '@vben/constants';\nimport { useWatermark } from '@vben/hooks';\nimport {\n  BookOpenText,\n  CircleHelp,\n  GiteeIcon,\n  GitHubOutlined,\n  UserOutlined,\n} from '@vben/icons';\nimport {\n  BasicLayout,\n  LockScreen,\n  Notification,\n  UserDropdown,\n} from '@vben/layouts';\nimport { preferences } from '@vben/preferences';\nimport { useAccessStore, useUserStore } from '@vben/stores';\nimport { openWindow } from '@vben/utils';\n\nimport { message } from 'ant-design-vue';\n\nimport { TenantToggle } from '#/components/tenant-toggle';\nimport { $t } from '#/locales';\nimport { resetRoutes } from '#/router';\nimport { useAuthStore, useNotifyStore } from '#/store';\nimport { useTenantStore } from '#/store/tenant';\nimport LoginForm from '#/views/_core/authentication/login.vue';\n\nconst userStore = useUserStore();\nconst authStore = useAuthStore();\nconst accessStore = useAccessStore();\nconst router = useRouter();\nconst { destroyWatermark, updateWatermark } = useWatermark();\n\nconst tenantStore = useTenantStore();\nconst menus = computed(() => {\n  const defaultMenus = [\n    {\n      handler: () => {\n        router.push('/profile');\n      },\n      icon: UserOutlined,\n      text: $t('ui.widgets.profile'),\n    },\n  ];\n  /**\n   * 租户选中状态 不显示个人中心\n   */\n  if (tenantStore.checked) {\n    defaultMenus.splice(1, 1);\n  }\n  return defaultMenus;\n});\n\nconst avatar = computed(() => {\n  return userStore.userInfo?.avatar || preferences.app.defaultAvatar;\n});\n\nasync function handleLogout() {\n  /**\n   * 主动登出不需要带跳转地址\n   */\n  await authStore.logout(false);\n  resetRoutes();\n}\n\nconst notifyStore = useNotifyStore();\nonMounted(() => notifyStore.startListeningMessage());\n\nfunction handleViewAll() {\n  message.warning('暂未开放');\n}\nwatch(\n  () => ({\n    enable: preferences.app.watermark,\n    content: preferences.app.watermarkContent,\n  }),\n  async ({ enable, content }) => {\n    if (enable) {\n      await updateWatermark({\n        content:\n          content ||\n          `${userStore.userInfo?.username} - ${userStore.userInfo?.realName}`,\n      });\n    } else {\n      destroyWatermark();\n    }\n  },\n  {\n    immediate: true,\n  },\n);\n</script>\n\n<template>\n  <BasicLayout @clear-preferences-and-logout=\"handleLogout\">\n    <template #header-right-1>\n      <TenantToggle />\n    </template>\n    <template #user-dropdown>\n      <UserDropdown\n        :avatar\n        :menus\n        :text=\"userStore.userInfo?.realName\"\n        :description=\"userStore.userInfo?.email || '未设置邮箱'\"\n        :tag-text=\"userStore.userInfo?.username\"\n        @logout=\"handleLogout\"\n      />\n    </template>\n    <template #notification>\n      <Notification\n        :dot=\"notifyStore.showDot\"\n        :notifications=\"notifyStore.notifications\"\n        @clear=\"notifyStore.clearAllMessage\"\n        @make-all=\"notifyStore.setAllRead\"\n        @read=\"notifyStore.setRead\"\n        @view-all=\"handleViewAll\"\n      />\n    </template>\n    <template #extra>\n      <AuthenticationLoginExpiredModal\n        v-model:open=\"accessStore.loginExpired\"\n        :avatar\n      >\n        <LoginForm />\n      </AuthenticationLoginExpiredModal>\n    </template>\n    <template #lock-screen>\n      <LockScreen :avatar @to-login=\"handleLogout\" />\n    </template>\n  </BasicLayout>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/layouts/index.ts",
    "content": "const BasicLayout = () => import('./basic.vue');\nconst AuthPageLayout = () => import('./auth.vue');\n\nconst IFrameView = () => import('@vben/layouts').then((m) => m.IFrameView);\n\nexport { AuthPageLayout, BasicLayout, IFrameView };\n"
  },
  {
    "path": "apps/web-antd/src/locales/README.md",
    "content": "# locale\n\n每个app使用的国际化可能不同，这里用于扩展国际化的功能，例如扩展 dayjs、antd组件库的多语言切换，以及app本身的国际化文件。\n"
  },
  {
    "path": "apps/web-antd/src/locales/index.ts",
    "content": "import type { Locale } from 'ant-design-vue/es/locale';\n\nimport type { App } from 'vue';\n\nimport type { LocaleSetupOptions, SupportedLanguagesType } from '@vben/locales';\n\nimport { ref } from 'vue';\n\nimport {\n  $t,\n  setupI18n as coreSetup,\n  loadLocalesMapFromDir,\n} from '@vben/locales';\nimport { preferences } from '@vben/preferences';\n\nimport antdEnLocale from 'ant-design-vue/es/locale/en_US';\nimport antdDefaultLocale from 'ant-design-vue/es/locale/zh_CN';\nimport dayjs from 'dayjs';\n\nconst antdLocale = ref<Locale>(antdDefaultLocale);\n\nconst modules = import.meta.glob('./langs/**/*.json');\n\nconst localesMap = loadLocalesMapFromDir(\n  /\\.\\/langs\\/([^/]+)\\/(.*)\\.json$/,\n  modules,\n);\n/**\n * 加载应用特有的语言包\n * 这里也可以改造为从服务端获取翻译数据\n * @param lang\n */\nasync function loadMessages(lang: SupportedLanguagesType) {\n  const [appLocaleMessages] = await Promise.all([\n    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion\n    localesMap[lang]!(),\n    loadThirdPartyMessage(lang),\n  ]);\n  return appLocaleMessages.default;\n}\n\n/**\n * 加载第三方组件库的语言包\n * @param lang\n */\nasync function loadThirdPartyMessage(lang: SupportedLanguagesType) {\n  await Promise.all([loadAntdLocale(lang), loadDayjsLocale(lang)]);\n}\n\n/**\n * 加载dayjs的语言包\n * @param lang\n */\nasync function loadDayjsLocale(lang: SupportedLanguagesType) {\n  let locale;\n  switch (lang) {\n    case 'en-US': {\n      locale = await import('dayjs/locale/en');\n      break;\n    }\n    case 'zh-CN': {\n      locale = await import('dayjs/locale/zh-cn');\n      break;\n    }\n    // 默认使用英语\n    default: {\n      locale = await import('dayjs/locale/en');\n    }\n  }\n  if (locale) {\n    dayjs.locale(locale);\n  } else {\n    console.error(`Failed to load dayjs locale for ${lang}`);\n  }\n}\n\n/**\n * 加载antd的语言包\n * @param lang\n */\nasync function loadAntdLocale(lang: SupportedLanguagesType) {\n  switch (lang) {\n    case 'en-US': {\n      antdLocale.value = antdEnLocale;\n      break;\n    }\n    case 'zh-CN': {\n      antdLocale.value = antdDefaultLocale;\n      break;\n    }\n  }\n}\n\nasync function setupI18n(app: App, options: LocaleSetupOptions = {}) {\n  await coreSetup(app, {\n    defaultLocale: preferences.app.locale,\n    loadMessages,\n    missingWarn: !import.meta.env.PROD,\n    ...options,\n  });\n}\n\nexport { $t, antdLocale, setupI18n };\n"
  },
  {
    "path": "apps/web-antd/src/locales/langs/en-US/component.json",
    "content": "{\n  \"cropper\": {\n    \"selectImage\": \"Select Image\",\n    \"uploadSuccess\": \"Uploaded success!\",\n    \"imageTooBig\": \"Image too big\",\n    \"modalTitle\": \"Avatar upload\",\n    \"okText\": \"Confirm and upload\",\n    \"btn_reset\": \"Reset\",\n    \"btn_rotate_left\": \"Counterclockwise rotation\",\n    \"btn_rotate_right\": \"Clockwise rotation\",\n    \"btn_scale_x\": \"Flip horizontal\",\n    \"btn_scale_y\": \"Flip vertical\",\n    \"btn_zoom_in\": \"Zoom in\",\n    \"btn_zoom_out\": \"Zoom out\",\n    \"preview\": \"Preview\"\n  },\n  \"tenantToggle\": {\n    \"placeholder\": \"Please select a tenant\",\n    \"switch\": \"Switch to tenant: \",\n    \"reset\": \"Reset to default tenant\"\n  },\n  \"notice\": {\n    \"title\": \"Notice\",\n    \"received\": \"You have received a new message\"\n  },\n  \"upload\": {\n    \"save\": \"Save\",\n    \"upload\": \"Upload\",\n    \"imgUpload\": \"ImageUpload\",\n    \"uploaded\": \"Uploaded\",\n    \"operating\": \"Operating\",\n    \"del\": \"Delete\",\n    \"download\": \"download\",\n    \"saveWarn\": \"Please wait for the file to upload and save!\",\n    \"saveError\": \"There is no file successfully uploaded and cannot be saved!\",\n    \"preview\": \"Preview\",\n    \"choose\": \"Select the file\",\n    \"accept\": \"Support {0} format\",\n    \"acceptUpload\": \"Only upload files in {0} format\",\n    \"maxSize\": \"A single file does not exceed {0}MB \",\n    \"maxSizeMultiple\": \"Only upload files up to {0}MB!\",\n    \"maxNumber\": \"Only upload up to {0} files\",\n    \"legend\": \"Legend\",\n    \"fileName\": \"File name\",\n    \"fileSize\": \"File size\",\n    \"fileStatue\": \"File status\",\n    \"pending\": \"Pending\",\n    \"startUpload\": \"Start upload\",\n    \"uploadSuccess\": \"Upload successfully\",\n    \"uploadError\": \"Upload failed\",\n    \"uploading\": \"Uploading\",\n    \"uploadWait\": \"Please wait for the file upload to finish\",\n    \"reUploadFailed\": \"Re-upload failed files\",\n    \"uploadHelpMessage\": \"Please upload a file in {ext} format that does not exceed {size} .\",\n    \"unknownFileType\": \"Unknown file type, unable to upload\",\n    \"confirmDelete\": \"Confirm file deletion {0}?\",\n    \"clickOrDrag\": \"Click or drag file to this area to upload\"\n  }\n}\n"
  },
  {
    "path": "apps/web-antd/src/locales/langs/en-US/demos.json",
    "content": "{\n  \"title\": \"Demos\",\n  \"antd\": \"Ant Design Vue\",\n  \"vben\": {\n    \"title\": \"Project\",\n    \"about\": \"About\",\n    \"document\": \"Document\",\n    \"antdv\": \"Ant Design Vue Version\",\n    \"naive-ui\": \"Naive UI Version\",\n    \"element-plus\": \"Element Plus Version\"\n  }\n}\n"
  },
  {
    "path": "apps/web-antd/src/locales/langs/en-US/http.json",
    "content": "{\n  \"apiRequestFailed\": \"Operation failed\",\n  \"operationSuccess\": \"Operation Success\",\n  \"successTip\": \"Success Tip\",\n  \"errorTip\": \"Error Tip\",\n  \"loginTimeout\": \"Login timeout, please log in again\"\n}\n"
  },
  {
    "path": "apps/web-antd/src/locales/langs/en-US/menu.json",
    "content": "{\n  \"root\": \"Root\",\n  \"system\": {\n    \"root\": \"System\",\n    \"user\": \"User\",\n    \"role\": \"Role\",\n    \"menu\": \"Menu\",\n    \"dept\": \"Department\",\n    \"post\": \"Post\",\n    \"dict\": \"Dictionary\",\n    \"config\": \"Parameter Settings\",\n    \"notice\": \"Notifications\",\n    \"log\": {\n      \"root\": \"Log\",\n      \"operation\": \"Operation Log\",\n      \"login\": \"Login Log\"\n    },\n    \"oss\": \"File\",\n    \"client\": \"Client\"\n  },\n  \"tenant\": {\n    \"root\": \"Tenant\",\n    \"package\": \"Package\"\n  },\n  \"monitor\": {\n    \"root\": \"System Monitoring\",\n    \"online\": \"Online Users\",\n    \"cache\": \"Cache Monitoring\",\n    \"admin\": \"Admin Monitoring\",\n    \"job\": \"Task Scheduling Center\"\n  },\n  \"tool\": {\n    \"root\": \"System Tools\",\n    \"gen\": \"Code Generation\"\n  },\n  \"workflow\": {\n    \"root\": \"Workflow\",\n    \"category\": \"Process Category\",\n    \"model\": \"Model\",\n    \"define\": \"Process Definition\",\n    \"monitor\": {\n      \"root\": \"Process Monitoring\",\n      \"instance\": \"Process Instance\",\n      \"todo\": \"Pending Tasks\"\n    },\n    \"form\": \"Form\"\n  },\n  \"task\": {\n    \"root\": \"My Tasks\",\n    \"apply\": \"My Initiated Tasks\",\n    \"todo\": \"My Pending Tasks\",\n    \"done\": \"My Completed Tasks\",\n    \"cc\": \"My CC\"\n  }\n}\n"
  },
  {
    "path": "apps/web-antd/src/locales/langs/en-US/page.json",
    "content": "{\n  \"auth\": {\n    \"login\": \"Login\",\n    \"register\": \"Register\",\n    \"codeLogin\": \"Code Login\",\n    \"qrcodeLogin\": \"Qr Code Login\",\n    \"forgetPassword\": \"Forget Password\",\n    \"oauthLogin\": \"Oauth Login\"\n  },\n  \"dashboard\": {\n    \"title\": \"Dashboard\",\n    \"analytics\": \"Analytics\",\n    \"workspace\": \"Workspace\"\n  }\n}\n"
  },
  {
    "path": "apps/web-antd/src/locales/langs/en-US/pages.json",
    "content": "{\n  \"common\": {\n    \"add\": \"Add\",\n    \"edit\": \"Edit\",\n    \"delete\": \"Delete\",\n    \"more\": \"More\",\n    \"search\": \"Search\",\n    \"reset\": \"Reset\",\n    \"import\": \"Import\",\n    \"export\": \"Export\",\n    \"expand\": \"Expand\",\n    \"collapse\": \"Collapse\",\n    \"info\": \"Info\",\n    \"clear\": \"Clear\",\n    \"unlock\": \"Unlock\",\n    \"download\": \"Download\",\n    \"sync\": \"Sync\",\n    \"refresh\": \"Refresh\",\n    \"generate\": \"Generate\",\n    \"downloadLoading\": \"Downloading... Please wait.\",\n    \"preview\": \"Preview\",\n    \"tip\": \"Tip\",\n    \"enable\": \"On\",\n    \"disable\": \"Off\",\n    \"beforeCloseTip\": \"You have unsaved changes. Are you sure you want to exit?\"\n  }\n}\n"
  },
  {
    "path": "apps/web-antd/src/locales/langs/zh-CN/component.json",
    "content": "{\n  \"cropper\": {\n    \"selectImage\": \"选择图片\",\n    \"uploadSuccess\": \"上传成功\",\n    \"imageTooBig\": \"图片超限\",\n    \"modalTitle\": \"头像上传\",\n    \"okText\": \"确认并上传\",\n    \"btn_reset\": \"重置\",\n    \"btn_rotate_left\": \"逆时针旋转\",\n    \"btn_rotate_right\": \"顺时针旋转\",\n    \"btn_scale_x\": \"水平翻转\",\n    \"btn_scale_y\": \"垂直翻转\",\n    \"btn_zoom_in\": \"放大\",\n    \"btn_zoom_out\": \"缩小\",\n    \"preview\": \"预览\"\n  },\n  \"tenantToggle\": {\n    \"placeholder\": \"选择租户\",\n    \"switch\": \"切换当前租户为: \",\n    \"reset\": \"还原为默认租户\"\n  },\n  \"notice\": {\n    \"title\": \"消息\",\n    \"received\": \"收到新消息\"\n  },\n  \"upload\": {\n    \"save\": \"保存\",\n    \"upload\": \"上传\",\n    \"imgUpload\": \"图片上传\",\n    \"uploaded\": \"已上传\",\n    \"operating\": \"操作\",\n    \"del\": \"删除\",\n    \"download\": \"下载\",\n    \"saveWarn\": \"请等待文件上传后，保存!\",\n    \"saveError\": \"没有上传成功的文件，无法保存!\",\n    \"preview\": \"预览\",\n    \"choose\": \"选择文件\",\n    \"accept\": \"支持{0}格式\",\n    \"acceptUpload\": \"只能上传{0}格式文件\",\n    \"maxSize\": \"单个文件不超过{0}MB\",\n    \"maxSizeMultiple\": \"只能上传不超过{0}MB的文件!\",\n    \"maxNumber\": \"最多只能上传{0}个文件\",\n    \"legend\": \"略缩图\",\n    \"fileName\": \"文件名\",\n    \"fileSize\": \"文件大小\",\n    \"fileStatue\": \"状态\",\n    \"pending\": \"待上传\",\n    \"startUpload\": \"开始上传\",\n    \"uploadSuccess\": \"上传成功\",\n    \"uploadError\": \"上传失败\",\n    \"uploading\": \"上传中\",\n    \"uploadWait\": \"请等待文件上传结束后操作\",\n    \"reUploadFailed\": \"重新上传失败文件\",\n    \"uploadHelpMessage\": \"请上传不超过{size}的{ext}格式文件\",\n    \"unknownFileType\": \"未知的文件类型, 无法上传\",\n    \"confirmDelete\": \"确认删除文件 {0}?\",\n    \"clickOrDrag\": \"点击或拖动文件到这个区域上传\"\n  }\n}\n"
  },
  {
    "path": "apps/web-antd/src/locales/langs/zh-CN/demos.json",
    "content": "{\n  \"title\": \"演示\",\n  \"antd\": \"Ant Design Vue\",\n  \"vben\": {\n    \"title\": \"项目\",\n    \"about\": \"关于\",\n    \"document\": \"文档\",\n    \"antdv\": \"Ant Design Vue 版本\",\n    \"naive-ui\": \"Naive UI 版本\",\n    \"element-plus\": \"Element Plus 版本\"\n  }\n}\n"
  },
  {
    "path": "apps/web-antd/src/locales/langs/zh-CN/http.json",
    "content": "{\n  \"apiRequestFailed\": \"请求出错，请稍候重试\",\n  \"operationSuccess\": \"操作成功\",\n  \"successTip\": \"成功提示\",\n  \"errorTip\": \"错误提示\",\n  \"loginTimeout\": \"登录超时, 请重新登录\"\n}\n"
  },
  {
    "path": "apps/web-antd/src/locales/langs/zh-CN/menu.json",
    "content": "{\n  \"root\": \"根目录\",\n  \"system\": {\n    \"root\": \"系统管理\",\n    \"user\": \"用户管理\",\n    \"role\": \"角色管理\",\n    \"menu\": \"菜单管理\",\n    \"dept\": \"部门管理\",\n    \"post\": \"岗位管理\",\n    \"dict\": \"字典管理\",\n    \"config\": \"参数设置\",\n    \"notice\": \"通知公告\",\n    \"log\": {\n      \"root\": \"日志管理\",\n      \"operation\": \"操作日志\",\n      \"login\": \"登录日志\"\n    },\n    \"oss\": \"文件管理\",\n    \"client\": \"客户端管理\"\n  },\n  \"tenant\": {\n    \"root\": \"租户管理\",\n    \"package\": \"套餐管理\"\n  },\n  \"monitor\": {\n    \"root\": \"系统监控\",\n    \"online\": \"在线用户\",\n    \"cache\": \"缓存监控\",\n    \"admin\": \"Admin监控\",\n    \"job\": \"任务调度中心\"\n  },\n  \"tool\": {\n    \"root\": \"系统工具\",\n    \"gen\": \"代码生成\"\n  },\n  \"workflow\": {\n    \"root\": \"工作流\",\n    \"category\": \"流程分类\",\n    \"model\": \"模型管理\",\n    \"define\": \"流程定义\",\n    \"monitor\": {\n      \"root\": \"流程监控\",\n      \"instance\": \"流程实例\",\n      \"todo\": \"待办任务\"\n    },\n    \"form\": \"表单管理\"\n  },\n  \"task\": {\n    \"root\": \"我的任务\",\n    \"apply\": \"我发起的\",\n    \"todo\": \"我的待办\",\n    \"done\": \"我的已办\",\n    \"cc\": \"我的抄送\"\n  }\n}\n"
  },
  {
    "path": "apps/web-antd/src/locales/langs/zh-CN/page.json",
    "content": "{\n  \"auth\": {\n    \"login\": \"登录\",\n    \"register\": \"注册\",\n    \"codeLogin\": \"验证码登录\",\n    \"qrcodeLogin\": \"二维码登录\",\n    \"forgetPassword\": \"忘记密码\",\n    \"oauthLogin\": \"第三方登录\"\n  },\n  \"dashboard\": {\n    \"title\": \"概览\",\n    \"analytics\": \"分析页\",\n    \"workspace\": \"工作台\"\n  }\n}\n"
  },
  {
    "path": "apps/web-antd/src/locales/langs/zh-CN/pages.json",
    "content": "{\n  \"common\": {\n    \"add\": \"新增\",\n    \"edit\": \"编辑\",\n    \"delete\": \"删除\",\n    \"more\": \"更多\",\n    \"search\": \"搜索\",\n    \"reset\": \"重置\",\n    \"import\": \"导入\",\n    \"export\": \"导出\",\n    \"expand\": \"展开\",\n    \"collapse\": \"收起\",\n    \"info\": \"详情\",\n    \"clear\": \"清空\",\n    \"unlock\": \"解锁\",\n    \"download\": \"下载\",\n    \"sync\": \"同步\",\n    \"refresh\": \"刷新\",\n    \"generate\": \"生成\",\n    \"downloadLoading\": \"下载中, 请稍后...\",\n    \"preview\": \"预览\",\n    \"tip\": \"提示\",\n    \"enable\": \"启用\",\n    \"disable\": \"禁用\",\n    \"beforeCloseTip\": \"您有未保存的更改，确认要退出吗？\"\n  }\n}\n"
  },
  {
    "path": "apps/web-antd/src/main.ts",
    "content": "import { initPreferences } from '@vben/preferences';\nimport { unmountGlobalLoading } from '@vben/utils';\n\nimport { overridesPreferences } from './preferences';\n\n/**\n * 应用初始化完成之后再进行页面加载渲染\n */\nasync function initApplication() {\n  // name用于指定项目唯一标识\n  // 用于区分不同项目的偏好设置以及存储数据的key前缀以及其他一些需要隔离的数据\n  const env = import.meta.env.PROD ? 'prod' : 'dev';\n  const appVersion = import.meta.env.VITE_APP_VERSION;\n  const namespace = `${import.meta.env.VITE_APP_NAMESPACE}-${appVersion}-${env}`;\n\n  // app偏好设置初始化\n  await initPreferences({\n    namespace,\n    overrides: overridesPreferences,\n  });\n\n  // 启动应用并挂载\n  // vue应用主要逻辑及视图\n  const { bootstrap } = await import('./bootstrap');\n  await bootstrap(namespace);\n\n  // 移除并销毁loading\n  unmountGlobalLoading();\n}\n\ninitApplication();\n"
  },
  {
    "path": "apps/web-antd/src/packages/workflow-designer/StandaloneWorkflowDesigner.vue",
    "content": "<script setup lang=\"ts\">\nimport { nextTick, onMounted, reactive, ref, computed, provide, watch, markRaw } from 'vue'\nimport { Button, message } from 'ant-design-vue'\nimport type { Edge, Node, NodeChange, EdgeChange, Connection, NodeMouseEvent } from '@vue-flow/core'\nimport { VueFlow, useVueFlow } from '@vue-flow/core'\nimport { Background } from '@vue-flow/background'\nimport SvgIcon from './components/SvgIcon.vue'\nimport { createNewEdge, createNewNode, emptyWorkflowInfo, getIconByComponentName, getIconClassByComponentName } from './utils/workflow-util'\nimport type { WorkflowInfo, WorkflowComponent, WorkflowNode } from './types/index.d'\nimport RightPanel from './panels/RightPanel.vue'\nimport NodeShell from './components/nodes/NodeShell.vue'\n\ninterface Props {\n  workflow: WorkflowInfo\n  wfComponents: WorkflowComponent[]\n  componentIdMap: Record<number, string>\n  saving?: boolean\n}\nconst props = withDefaults(defineProps<Props>(), {\n  workflow: () => emptyWorkflowInfo(),\n  wfComponents: () => [],\n  componentIdMap: () => ({}),\n  saving: false,\n})\n\n// 组件库收起状态\nconst siderCollapsed = ref(false)\n\nconst emit = defineEmits<{\n  (e: 'save', workflow: WorkflowInfo): void\n  (e: 'run', payload: { workflow: WorkflowInfo }): void\n  (e: 'deleteNode', nodeUuid: string): void\n}>()\n\nconst hidePropertyPanel = ref<boolean>(true)\nconst selectedWfNode = ref<WorkflowNode>()\nconst { onInit, fitView, onConnect, onEdgesChange, onNodesChange, onNodeClick, onEdgeClick, onNodeDragStop, addSelectedNodes, project, getNodes } = useVueFlow()\n\nconst uw = { nodes: [] as Array<Node>, edges: [] as Array<Edge> }\nconst uiWorkflow = reactive(uw)\n\n// 自动扫描并注册节点/边组件，未提供节点组件时回退到 NodeShell\nconst nodeModules = import.meta.glob('./components/nodes/*Node.vue', { eager: true, import: 'default' }) as Record<string, any>\nconst edgeModules = import.meta.glob('./components/edges/*Edge.vue', { eager: true, import: 'default' }) as Record<string, any>\n\nfunction toKey(path: string, suffix: 'Node' | 'Edge') {\n  const file = path.substring(path.lastIndexOf('/') + 1)\n  return file.replace(new RegExp(`${suffix}\\\\.vue$`), '').toLowerCase()\n}\n\nconst nodeTypes = computed(() => {\n  const map: Record<string, any> = {}\n  for (const [p, mod] of Object.entries(nodeModules)) {\n    map[toKey(p, 'Node')] = markRaw(mod)\n  }\n  for (const c of props.wfComponents) {\n    const key = c.name.toLowerCase()\n    if (!map[key]) map[key] = markRaw(NodeShell)\n  }\n  return map\n})\n\nconst edgeTypes = computed(() => {\n  const map: Record<string, any> = {}\n  for (const [p, mod] of Object.entries(edgeModules)) {\n    map[toKey(p, 'Edge')] = markRaw(mod)\n  }\n  return map\n})\n\nfunction renderGraph() {\n  console.log('开始渲染工作流:', props.workflow)\n  console.log('节点数量:', props.workflow.nodes.length)\n  console.log('边数量:', props.workflow.edges.length)\n  \n  const initX = 10, initY = 50\n  const validNodeIds = new Set<string>()\n  const newNodes: Array<any> = []\n  const newEdges: Array<any> = []\n  \n  // 先渲染所有有效节点\n  for (let i = 0; i < props.workflow.nodes.length; i++) {\n    const node = props.workflow.nodes[i]\n    console.log(`检查节点 ${i}:`, node)\n    \n    if (!node) {\n      console.warn(`跳过节点 ${i}: 节点为空`)\n      continue\n    }\n    \n    if (!node.uuid) {\n      console.warn(`跳过节点 ${i}: 缺少 uuid 字段`, node)\n      continue\n    }\n    \n    // 处理组件信息：后端可能只提供 workflowComponentId，需要映射到组件名称\n    let componentName = ''\n    if (node.wfComponent && node.wfComponent.name) {\n      // 如果已经有 wfComponent.name，直接使用\n      componentName = node.wfComponent.name\n    } else if (node.workflowComponentId !== undefined) {\n      // 使用调用方传入的组件ID映射\n      componentName = props.componentIdMap[node.workflowComponentId] || 'Unknown'\n      console.log(`根据 workflowComponentId ${node.workflowComponentId} 映射到组件: ${componentName}`)\n    } else {\n      console.warn(`跳过节点 ${i}: 无法确定组件类型`, node)\n      continue\n    }\n    \n    const px = node.positionX ? node.positionX : initX + 230 * i\n    const py = node.positionY ? node.positionY : initY\n    \n    // 直接在原始节点对象上补齐 wfComponent，保持引用一致，确保属性修改能写回 workflow.nodes\n    if (!node.wfComponent) {\n      node.wfComponent = {\n        name: componentName,\n        title: componentName === 'Start' ? '开始' : componentName === 'End' ? '结束' : componentName,\n        remark: componentName === 'Start' ? '工作流开始节点' : componentName === 'End' ? '工作流结束节点' : `${componentName}节点`\n      }\n    }\n\n    newNodes.push({ id: node.uuid, type: componentName.toLowerCase(), data: node, position: { x: px, y: py } })\n    validNodeIds.add(node.uuid)\n    console.log(`✅ 添加节点: ${node.uuid} (${componentName})`)\n  }\n  \n  console.log('有效节点ID:', Array.from(validNodeIds))\n  \n  // 只渲染有效的边（源节点和目标节点都存在）\n  for (const wfEdge of props.workflow.edges) {\n    if (!wfEdge || !wfEdge.uuid || !wfEdge.sourceNodeUuid || !wfEdge.targetNodeUuid) {\n      console.warn('跳过无效边（缺少必要字段）:', wfEdge)\n      continue\n    }\n    \n    // 验证源节点和目标节点是否都存在\n    if (validNodeIds.has(wfEdge.sourceNodeUuid) && validNodeIds.has(wfEdge.targetNodeUuid)) {\n      newEdges.push({ \n        id: wfEdge.uuid, \n        source: wfEdge.sourceNodeUuid, \n        target: wfEdge.targetNodeUuid, \n        sourceHandle: wfEdge.sourceHandle, \n        type: 'special', \n        animated: true, \n        data: wfEdge \n      })\n      console.log(`添加边: ${wfEdge.uuid} (${wfEdge.sourceNodeUuid} -> ${wfEdge.targetNodeUuid})`)\n    } else {\n      console.warn(`跳过无效的边: ${wfEdge.uuid}, 源节点: ${wfEdge.sourceNodeUuid}, 目标节点: ${wfEdge.targetNodeUuid}`)\n      console.warn('源节点存在:', validNodeIds.has(wfEdge.sourceNodeUuid))\n      console.warn('目标节点存在:', validNodeIds.has(wfEdge.targetNodeUuid))\n    }\n  }\n  \n  // 使用 nextTick 确保 DOM 更新安全\n  nextTick(() => {\n    // 安全地更新数组\n    uiWorkflow.nodes.length = 0\n    uiWorkflow.edges.length = 0\n    uiWorkflow.nodes.push(...newNodes)\n    uiWorkflow.edges.push(...newEdges)\n    \n    console.log('最终渲染结果:')\n    console.log('UI节点数量:', uiWorkflow.nodes.length)\n    console.log('UI边数量:', uiWorkflow.edges.length)\n    console.log('UI节点:', uiWorkflow.nodes)\n    console.log('UI边:', uiWorkflow.edges)\n  })\n}\n\nonNodesChange((changes: NodeChange[]) => {\n  let nodeUnSelected = false\n  for (const change of changes) {\n    // 同步选中状态\n    if ('selected' in change) {\n      if (!change.selected && selectedWfNode.value?.uuid === (change as any).id) nodeUnSelected = true\n    }\n    // 同步位置：当拖拽结束或位置变更时写回到 workflow.nodes\n    if ((change as any).type === 'position') {\n      const wfNode = props.workflow.nodes.find((n: WorkflowNode) => n.uuid === (change as any).id)\n      const uiNode = uiWorkflow.nodes.find((n: any) => n.id === (change as any).id)\n      if (wfNode && uiNode) {\n        wfNode.positionX = uiNode.position.x\n        wfNode.positionY = uiNode.position.y\n      }\n    }\n  }\n  if (nodeUnSelected) hidePropertyPanel.value = true\n})\n\nonEdgesChange((changes: EdgeChange[]) => {\n  console.log(changes)\n})\n\nonConnect((connection: Connection) => {\n  createNewEdge({\n    workflow: props.workflow,\n    uiWorkflow,\n    source: connection.source,\n    sourceHandle: connection.sourceHandle ? connection.sourceHandle : '',\n    target: connection.target,\n  })\n})\n\nonInit(() => {\n  nextTick(() => fitView())\n})\n\nonNodeClick(({ node }: NodeMouseEvent) => {\n  if (node && node.selected) {\n    selectedWfNode.value = node.data as WorkflowNode\n    hidePropertyPanel.value = false\n  }\n})\n\nfunction onDragOver(event: DragEvent) {\n  event.preventDefault()\n  if (event.dataTransfer) event.dataTransfer.dropEffect = 'move'\n}\n\nconst wrapper = ref()\n\nfunction onDrop(event: DragEvent) {\n  const comName = event.dataTransfer?.getData('application/vueflow') as string\n  const component = props.wfComponents.find((c: WorkflowComponent) => c.name === comName)\n  if (!component) { message.warning('组件未找到'); return }\n  if (comName === 'Start' && props.workflow.nodes.some((n: WorkflowNode) => n.wfComponent?.name === 'Start')) { message.warning('开始节点只能有一个'); return }\n  const flowbounds = (wrapper.value as any).$el.getBoundingClientRect()\n  const position = project({ x: event.clientX - flowbounds.left, y: event.clientY - flowbounds.top })\n  createNewNode(props.workflow, uiWorkflow, component, position)\n  const lastNode = uiWorkflow.nodes[uiWorkflow.nodes.length - 1]\n  if (lastNode) {\n    const graphNode = getNodes.value.find((n: any) => n.id === lastNode.id)\n    if (graphNode) addSelectedNodes([graphNode])\n  }\n}\n\nfunction onPaletteDragStart(event: DragEvent, name: string) {\n  event.dataTransfer?.setData('application/vueflow', name)\n  if (event.dataTransfer) event.dataTransfer.effectAllowed = 'move'\n}\n\n// 将画布中的节点坐标写回 workflow\nfunction syncPositionsFromUi() {\n  const nodes = getNodes.value as any[]\n  for (const n of nodes) {\n    const wfNode = props.workflow.nodes.find((x: WorkflowNode) => x.uuid === n.id)\n    if (wfNode) {\n      wfNode.positionX = n.position?.x ?? wfNode.positionX\n      wfNode.positionY = n.position?.y ?? wfNode.positionY\n    }\n  }\n}\n\n// 拖拽结束时同步该节点的坐标\nonNodeDragStop(({ node }: any) => {\n  const wfNode = props.workflow.nodes.find((x: WorkflowNode) => x.uuid === node?.id)\n  if (wfNode) {\n    wfNode.positionX = node.position?.x ?? wfNode.positionX\n    wfNode.positionY = node.position?.y ?? wfNode.positionY\n  }\n})\n\nfunction onRun() { emit('run', { workflow: props.workflow }) }\n\nasync function onSave() {\n  if (props.saving) return\n  // 最后一次同步：以画布为准写回所有节点坐标\n  syncPositionsFromUi()\n  // 去重 deleteEdges，避免重复 id\n  if (Array.isArray((props.workflow as any).deleteEdges)) {\n    const dedup = Array.from(new Set((props.workflow as any).deleteEdges))\n    ;(props.workflow as any).deleteEdges = dedup\n  }\n  emit('save', props.workflow)\n}\n\n// 点击边时删除该边\nonEdgeClick(({ edge }: any) => {\n  const id = edge?.id\n  if (!id) return\n  const idx = props.workflow.edges.findIndex((e: any) => e.uuid === id || e.id === id)\n  if (idx > -1) {\n    const removedList = props.workflow.edges.splice(idx, 1)\n    const removed = removedList.length ? (removedList[0] as any) : undefined\n    if (removed) {\n      if (!props.workflow.deleteEdges) props.workflow.deleteEdges = []\n      const removedId: string = (removed.uuid || removed.id || '') as string\n      if (removedId) props.workflow.deleteEdges.push(removedId)\n    }\n  }\n  const uiIdx = (uiWorkflow.edges as any[]).findIndex((e: any) => e.id === id)\n  if (uiIdx > -1) (uiWorkflow.edges as any[]).splice(uiIdx, 1)\n})\n\n// 监听 workflow prop 变化，重新渲染\nwatch(() => props.workflow, () => {\n  nextTick(() => {\n    renderGraph()\n  })\n}, { deep: true })\n\nonMounted(() => { renderGraph() })\n\nfunction onDeleteNode(nodeUuid: string) {\n  const idx = props.workflow.nodes.findIndex((n: WorkflowNode) => n.uuid === nodeUuid)\n  if (idx > -1) props.workflow.nodes.splice(idx, 1)\n  const uiIdx = uiWorkflow.nodes.findIndex((n: any) => n.id === nodeUuid)\n  if (uiIdx > -1) uiWorkflow.nodes.splice(uiIdx, 1)\n  // 同步删除与该节点相关的边，并记录到 deleteEdges，确保后端同步删除\n  const toRemove: any[] = []\n  for (let i = props.workflow.edges.length - 1; i >= 0; i--) {\n    const e: any = props.workflow.edges[i]\n    if (e.sourceNodeUuid === nodeUuid || e.targetNodeUuid === nodeUuid) {\n      const removed = props.workflow.edges.splice(i, 1)[0]\n      if (removed) toRemove.push(removed)\n    }\n  }\n  if (toRemove.length > 0) {\n    if (!props.workflow.deleteEdges) (props.workflow as any).deleteEdges = []\n    toRemove.forEach((e) => {\n      const id: string = (e.uuid || e.id || '') as string\n      if (id) (props.workflow as any).deleteEdges.push(id)\n      // 从 UI 边列表中删除\n      const uiIdx2 = (uiWorkflow.edges as any[]).findIndex((x: any) => x.id === id)\n      if (uiIdx2 > -1) (uiWorkflow.edges as any[]).splice(uiIdx2, 1)\n    })\n  }\n  // 清理右侧属性面板与当前选中节点\n  if (selectedWfNode.value?.uuid === nodeUuid) {\n    selectedWfNode.value = undefined\n    hidePropertyPanel.value = true\n  }\n  // 取消画布选中状态\n  try { addSelectedNodes([]) } catch {}\n  emit('deleteNode', nodeUuid)\n}\n\n// 向节点组件注入删除回调，便于在节点内部触发删除\nprovide('wfOnDeleteNode', (uuid: string) => onDeleteNode(uuid))\n</script>\n\n<template>\n  <div class=\"workflow-designer-root\">\n    <!-- 左侧组件面板 -->\n    <div class=\"workflow-sider\" :class=\"{ collapsed: siderCollapsed }\">\n      <div class=\"sider-header\">\n        <div class=\"header-content\">\n          <div class=\"header-title\">组件库</div>\n          <div class=\"header-subtitle\">拖拽组件到画布</div>\n        </div>\n        <Button \n          size=\"small\" \n          type=\"text\" \n          class=\"collapse-toggle\"\n          @click=\"siderCollapsed = !siderCollapsed\"\n        >\n          <SvgIcon :icon=\"siderCollapsed ? 'ri:arrow-right-s-line' : 'ri:arrow-left-s-line'\" class=\"text-lg\" />\n        </Button>\n      </div>\n      <div class=\"component-list\">\n        <template v-for=\"component in wfComponents\" :key=\"component.name\">\n          <div \n            v-if=\"component.isEnable !== false\" \n            class=\"component-item\" \n            :draggable=\"true\" \n            @dragstart=\"onPaletteDragStart($event, component.name)\"\n          >\n            <div class=\"component-icon\">\n              <SvgIcon :class=\"getIconClassByComponentName(component.name)\" :icon=\"getIconByComponentName(component.name)\" />\n            </div>\n            <div class=\"component-name\">{{ component.title }}</div>\n          </div>\n        </template>\n      </div>\n    </div>\n    \n    <!-- 展开按钮（当侧边栏收起时显示） -->\n    <div v-if=\"siderCollapsed\" class=\"sider-expand-btn\" @click=\"siderCollapsed = false\">\n      <SvgIcon icon=\"ri:arrow-right-s-line\" class=\"text-lg\" />\n    </div>\n    \n    <!-- 右侧画布区域 -->\n    <div class=\"workflow-canvas-container\" @drop=\"onDrop\" @dragover=\"onDragOver\">\n      <VueFlow ref=\"wrapper\" :nodes=\"uiWorkflow.nodes\" :edges=\"uiWorkflow.edges\" :node-types=\"nodeTypes\" :edge-types=\"edgeTypes\" fit-view-on-init class=\"workflow-canvas\">\n        <Background />\n      </VueFlow>\n      <RightPanel :workflow=\"props.workflow\" :ui-workflow=\"uiWorkflow\" :hide-property-panel=\"hidePropertyPanel\" :wf-node=\"selectedWfNode\" />\n      <div class=\"canvas-toolbar\">\n        <Button :disabled=\"props.saving\" class=\"toolbar-btn toolbar-btn-default\" @click=\"onRun\">运 行</Button>\n        <Button :disabled=\"props.saving\" :loading=\"props.saving\" type=\"primary\" class=\"toolbar-btn\" @click=\"onSave\">保 存</Button>\n      </div>\n    </div>\n  </div>\n</template>\n\n<style>\n@import '@vue-flow/core/dist/style.css';\n@import '@vue-flow/core/dist/theme-default.css';\n\n/* 工作流设计器根容器 */\n.workflow-designer-root {\n  display: flex;\n  width: 100%;\n  height: 100%;\n  overflow: hidden;\n  background: #f5f5f5;\n}\n\n/* 左侧组件面板 */\n.workflow-sider { \n  width: 260px; \n  height: 100%;\n  background: white;\n  border-right: 1px solid #e0e0e0;\n  display: flex;\n  flex-direction: column;\n  flex-shrink: 0;\n  transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s ease;\n  overflow: hidden;\n}\n\n.workflow-sider.collapsed {\n  width: 0;\n  opacity: 0;\n  border-right: none;\n}\n\n/* 侧边栏头部 */\n.sider-header {\n  padding: 16px;\n  border-bottom: 1px solid #e0e0e0;\n  background: white;\n  display: flex;\n  justify-content: space-between;\n  align-items: flex-start;\n  gap: 8px;\n}\n\n.header-content {\n  flex: 1;\n  min-width: 0;\n}\n\n.header-title {\n  font-size: 16px;\n  font-weight: 600;\n  color: #333;\n  margin-bottom: 4px;\n  white-space: nowrap;\n}\n\n.header-subtitle {\n  font-size: 12px;\n  color: #999;\n  white-space: nowrap;\n}\n\n.collapse-toggle {\n  padding: 4px;\n  height: auto;\n  min-width: auto;\n  color: #666;\n  flex-shrink: 0;\n}\n\n.collapse-toggle:hover {\n  color: #1890ff;\n  background: #f0f9ff;\n}\n\n/* 展开按钮 */\n.sider-expand-btn {\n  position: absolute;\n  left: 0;\n  top: 100px;\n  width: 32px;\n  height: 48px;\n  background: white;\n  border: 1px solid #e0e0e0;\n  border-left: none;\n  border-radius: 0 8px 8px 0;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  cursor: pointer;\n  z-index: 10;\n  box-shadow: 2px 0 8px rgba(0, 0, 0, 0.08);\n  transition: all 0.2s ease;\n  color: #666;\n}\n\n.sider-expand-btn:hover {\n  background: #f0f9ff;\n  border-color: #1890ff;\n  color: #1890ff;\n  width: 36px;\n}\n\n/* 组件列表 */\n.component-list {\n  flex: 1;\n  overflow-y: auto;\n  padding: 12px;\n}\n\n/* 美化滚动条 */\n.component-list::-webkit-scrollbar {\n  width: 6px;\n}\n\n.component-list::-webkit-scrollbar-track {\n  background: #f5f5f5;\n}\n\n.component-list::-webkit-scrollbar-thumb {\n  background: #d9d9d9;\n  border-radius: 3px;\n}\n\n.component-list::-webkit-scrollbar-thumb:hover {\n  background: #bfbfbf;\n}\n\n/* 组件项 */\n.component-item {\n  display: flex;\n  align-items: center;\n  padding: 10px 12px;\n  margin-bottom: 8px;\n  background: white;\n  border: 1px solid #e0e0e0;\n  border-radius: 6px;\n  cursor: grab;\n  transition: all 0.2s ease;\n  user-select: none;\n}\n\n.component-item:hover {\n  border-color: #1890ff;\n  background: #f0f9ff;\n  box-shadow: 0 2px 8px rgba(24, 144, 255, 0.15);\n  transform: translateY(-1px);\n}\n\n.component-item:active {\n  cursor: grabbing;\n  transform: translateY(0);\n  box-shadow: 0 1px 4px rgba(24, 144, 255, 0.2);\n}\n\n.component-icon {\n  width: 28px;\n  height: 28px;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  margin-right: 10px;\n  font-size: 20px;\n  flex-shrink: 0;\n}\n\n.component-name {\n  font-size: 14px;\n  color: #333;\n  font-weight: 500;\n}\n\n.vue-flow__node { \n  border: 1px solid #eee; \n  box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); \n  padding: 10px; \n  border-radius: 10px; \n  background: #FFF; \n  display: flex; \n  flex-direction: column; \n  justify-content: space-between; \n  align-items: center; \n  gap: 10px; \n  width: 220px; \n}\n\n.vue-flow__node.selected { \n  border: 1px solid #2563eb; \n  padding: 10px; \n  border-radius: 10px; \n}\n\n.vue-flow__node.selected .vue-flow__handle { \n  background: #2563eb; \n}\n\n.vue-flow__edge.selected .vue-flow__edge-path { \n  stroke: #2563eb; \n  stroke-width: 1.5; \n}\n\n.vue-flow__handle { \n  background: #555; \n  height: 16px; \n  width: 8px; \n  border-radius: 4px;\n}\n\n.vue-flow__node .header { \n  height: 45px; \n  line-height: 45px; \n  margin-bottom: 10px; \n  text-align: center; \n  font-weight: 600; \n}\n\n.vue-flow__node .content_line { \n  height: 40px; \n  line-height: 40px; \n  background: #9696961a; \n  margin-bottom: 10px; \n  text-align: center; \n  white-space: nowrap; \n  overflow: hidden; \n  text-overflow: ellipsis; \n}\n\n/* 工作流画布容器 - 占据剩余空间 */\n.workflow-canvas-container {\n  flex: 1;\n  height: 100%;\n  min-width: 0;\n  position: relative;\n  background: #fafafa;\n}\n\n/* VueFlow 画布 - 绝对定位占满父容器 */\n.workflow-canvas {\n  position: absolute;\n  top: 0;\n  left: 0;\n  right: 0;\n  bottom: 0;\n  width: 100%;\n  height: 100%;\n}\n\n/* 画布工具栏 */\n.canvas-toolbar {\n  position: absolute;\n  right: 20px;\n  top: 16px;\n  display: flex;\n  gap: 12px;\n  z-index: 10;\n}\n\n.toolbar-btn {\n  height: 36px;\n  padding: 0 20px;\n  border-radius: 6px;\n  font-size: 14px;\n  font-weight: 500;\n  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);\n  transition: all 0.2s ease;\n}\n\n.toolbar-btn:hover {\n  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);\n  transform: translateY(-1px);\n}\n\n.toolbar-btn-default {\n  background: white;\n  color: #333;\n  border: 1px solid #e0e0e0;\n}\n\n.toolbar-btn-default:hover {\n  border-color: #1890ff;\n  color: #1890ff;\n}\n</style>\n\n\n"
  },
  {
    "path": "apps/web-antd/src/packages/workflow-designer/components/CommonNodeHeader.vue",
    "content": "<script setup lang=\"ts\">\nimport { inject } from 'vue'\nimport { Dropdown, Menu, MenuItem } from 'ant-design-vue'\nimport SvgIcon from '../components/SvgIcon.vue'\nimport { getIconByComponentName, getIconClassByComponentName } from '../utils/workflow-util'\n\ninterface Props { wfNode: Workflow.WorkflowNode }\nconst props = defineProps<Props>()\n\nconst emit = defineEmits<{ (e: 'deleteNode', nodeUuid: string): void }>()\nconst injectedDelete = inject<(uuid: string) => void>('wfOnDeleteNode')\n\nfunction handleMenuClick(info: { key: string | number }) {\n  const key = String(info.key)\n  if (key === 'delete') {\n    // 兼容两种触发：向上冒泡 或 直接注入调用\n    if (injectedDelete) injectedDelete(props.wfNode.uuid)\n    else emit('deleteNode', props.wfNode.uuid)\n  }\n}\n</script>\n\n<template>\n  <div class=\"w-full flex border-b divide-gray-400 pb-3 mb-3 font-bold text-base text-center items-center justify-center\">\n    <div class=\"w-6 mr-2\">\n      <SvgIcon class=\"text-xl\" :class=\"getIconClassByComponentName(wfNode.wfComponent.name)\" :icon=\"getIconByComponentName(wfNode.wfComponent.name)\" />\n    </div>\n    <div class=\"flex-1 max-h-6 overflow-hidden text-nowrap\">{{ wfNode.title }}</div>\n    <div class=\"w-6 ml-2\" @click.stop>\n      <Dropdown \n        v-if=\"wfNode.wfComponent.name !== 'Start'\" \n        :trigger=\"['hover']\"\n        placement=\"bottomRight\"\n      >\n        <div class=\"dropdown-trigger\">\n          <SvgIcon class=\"cursor-pointer hover:text-blue-500 transition-colors\" icon=\"ri:more-fill\" />\n        </div>\n        <template #overlay>\n          <Menu @click=\"handleMenuClick\">\n            <MenuItem key=\"delete\">\n              <div class=\"flex items-center gap-2\">\n                <SvgIcon icon=\"ri:delete-bin-line\" class=\"text-base\" />\n                <span>删除</span>\n              </div>\n            </MenuItem>\n          </Menu>\n        </template>\n      </Dropdown>\n    </div>\n  </div>\n</template>\n\n\n"
  },
  {
    "path": "apps/web-antd/src/packages/workflow-designer/components/RunDetail.vue",
    "content": "<script lang=\"ts\" setup>\nimport { nextTick, onUnmounted, reactive, ref, watch } from 'vue'\nimport { Button, Input, InputNumber, Switch, Tabs, Upload, message } from 'ant-design-vue'\nimport type { UploadFile } from 'ant-design-vue'\nimport RuntimeNodes from './RuntimeNodes.vue'\nimport SvgIcon from './SvgIcon.vue'\nimport { workflowRun, workflowRuntimeResume, getUploadAction,workflowApi } from '#/api/aiflow'\nimport { useWfStore } from '#/packages/workflow-designer/store'\n\ninterface Props {\n  workflow: any;\n}\ninterface TabObj {\n  name: string;\n  tab: string;\n  defaultTab: string;\n}\ninterface Emit {\n  (e: 'runDone'): void;\n  (e: 'runError', errorMsg: string): void;\n}\n\nconst props = defineProps<Props>();\nconst emit = defineEmits<Emit>();\nconst headers: Record<string, string> = { Authorization: '' };\nconst wfStore = useWfStore();\nconst token = ref<string>('');\nconst submitting = ref<boolean>(wfStore.submitting);\nconst startNode = ref<any>(null);\nconst wfRuntimeUuid = ref<string>('');\nconst runtimeNodes = reactive<any[]>([]);\nconst runtimeErrorMsg = ref<string>('');\nconst userInputs = ref<any[]>([]);\nconst errorMsg = ref<string>('');\nconst currWfUuid = props.workflow.uuid;\nconst showCurrentExecution = ref<boolean>(false);\nconst tabObj = ref<TabObj>({\n  name: 'runtimes',\n  defaultTab: '流程执行详情',\n  tab: '流程执行详情 ↓',\n});\nconst fileListLength = ref(0);\nconst uploadRef = ref<any>(null);\nconst fileList = ref<UploadFile[]>([]);\nconst uploadedFileUuids = ref<string[]>([]);\nconst humanFeedback = ref<boolean>(false);\nconst humanFeedbackTip = ref<string>('');\nconst humanFeedbackContent = ref<string>('');\n// 组件ID到组件名称的映射\nconst componentIdToNameMap = ref<Record<string, string>>({});\nlet controller = new AbortController()\nfunction onKeydown(e: KeyboardEvent) {\n  if (e.key === 'Enter' && !e.shiftKey) {\n    e.preventDefault();\n    if (!submitting.value) run();\n  }\n}\n\nasync function uploadBeforeRun() {\n  uploadedFileUuids.value = [];\n  // Antd的Upload组件自动提交，无需手动调用submit\n}\n\nasync function resetInputs() {\n  fileList.value = [];\n  uploadedFileUuids.value = [];\n  userInputs.value.forEach((input: any) => {\n    input.content.value = null;\n  });\n}\n\nasync function run() {\n  if (submitting.value) return;\n\n  for (const input of userInputs.value) {\n    if (\n      input.required &&\n      input.content.type === 4 &&\n      input.content.value === null &&\n      fileListLength.value === 0\n    ) {\n      message.warning('请上传文件');\n      return;\n    }\n  }\n\n  if (\n    fileListLength.value > 0 &&\n    uploadedFileUuids.value.length !== fileListLength.value\n  ) {\n    uploadBeforeRun();\n    return;\n  } else {\n    // 若存在文件输入但未走上传控件回调，直接将已上传的uuid写入\n    const fileInput = userInputs.value.find(\n      (input: any) => input.content.type === 4,\n    );\n    if (\n      fileInput &&\n      (!fileInput.content.value || fileInput.content.value.length === 0)\n    ) {\n      fileInput.content.value = uploadedFileUuids.value;\n    }\n  }\n\n  if (\n    userInputs.value.some(\n      (input: any) => input.required && input.content.value === null,\n    )\n  ) {\n    message.warning('请输入所有必填参数');\n    return;\n  }\n\n  submitting.value = true;\n  showCurrentExecution.value = true;\n  tabObj.value.tab = showCurrentExecution.value\n    ? `${tabObj.value.defaultTab} ↓`\n    : `${tabObj.value.defaultTab} ↑`;\n\n  controller = new AbortController();\n  try {\n    wfRuntimeUuid.value = '';\n    const nodeUuidToRuntimeNodeUuid = new Map<string, string>();\n    runtimeNodes.splice(0, runtimeNodes.length);\n    await workflowRun({\n      options: { uuid: currWfUuid, inputs: userInputs.value },\n      signal: controller.signal,\n      startCallback: (wfRuntimeJson) => {\n        if (!wfRuntimeJson) {\n          message.error('启动失败');\n          return;\n        }\n        const wfRuntime = JSON.parse(wfRuntimeJson);\n        wfRuntime.input = {};\n        userInputs.value.forEach((item: any) => {\n          wfRuntime.input[item.name] = { ...item.content };\n        });\n        wfRuntimeUuid.value = wfRuntime.uuid;\n        wfStore.appendWfRuntimes(currWfUuid, [wfRuntime]);\n      },\n      thinkingDataReceived: (chunk) => {\n        console.log('Thinking data received:', chunk);\n      },\n      messageReceived: (chunk, event) => {\n        const eventName = event || '';\n        try {\n          if (eventName.includes('[NODE_RUN_')) {\n            const nodeUuid = eventName\n              .replace('[NODE_RUN_', '')\n              .replace(']', '');\n            const runtimeNode = JSON.parse(chunk);\n            nodeUuidToRuntimeNodeUuid.set(nodeUuid, runtimeNode.uuid);\n            // 补充节点元信息（标题、组件），用于 UI 展示\n            const wfNodeMeta = (props.workflow?.nodes || []).find(\n              (n: any) => n.uuid === nodeUuid,\n            );\n            if (wfNodeMeta) {\n              (runtimeNode as any).nodeTitle = wfNodeMeta.title;\n              // 设置组件信息，支持两种数据结构\n              if (wfNodeMeta.wfComponent) {\n                (runtimeNode as any).wfComponent = wfNodeMeta.wfComponent;\n              } else if (wfNodeMeta.workflowComponentId) {\n                (runtimeNode as any).workflowComponentId =\n                  wfNodeMeta.workflowComponentId;\n              }\n            }\n            wfStore.appendRuntimeNode(wfRuntimeUuid.value, runtimeNode);\n            runtimeNodes.push(runtimeNode);\n          } else if (eventName.includes('[NODE_CHUNK_')) {\n            console.log('NODE_CHUNK_', eventName, chunk);\n            const nodeUuid = eventName\n              .replace('[NODE_CHUNK_', '')\n              .replace(']', '');\n            const runtimeNodeUuid =\n              nodeUuidToRuntimeNodeUuid.get(nodeUuid) || '';\n            wfStore.appendChunkToRuntimeNode(\n              wfRuntimeUuid.value,\n              runtimeNodeUuid,\n              chunk,\n            );\n            const hit = runtimeNodes.find(\n              (n: any) => n.uuid === runtimeNodeUuid,\n            );\n            if (hit) {\n              (hit as any).chunks = ((hit as any).chunks || '') + chunk;\n            }\n          } else if (eventName.includes('[NODE_INPUT_')) {\n            const nodeUuid = eventName\n              .replace('[NODE_INPUT_', '')\n              .replace(']', '');\n            const runtimeNodeUuid =\n              nodeUuidToRuntimeNodeUuid.get(nodeUuid) || '';\n            wfStore.appendInputToRuntimeNode(\n              wfRuntimeUuid.value,\n              runtimeNodeUuid,\n              chunk,\n            );\n            const hit = runtimeNodes.find(\n              (n: any) => n.uuid === runtimeNodeUuid,\n            );\n            if (hit) {\n              const payload = JSON.parse(chunk);\n              (hit as any).input = {\n                ...(hit as any).input,\n                [payload.name]: payload.content,\n              };\n            }\n          } else if (eventName.includes('[NODE_OUTPUT_')) {\n            const nodeUuid = eventName\n              .replace('[NODE_OUTPUT_', '')\n              .replace(']', '');\n            const runtimeNodeUuid =\n              nodeUuidToRuntimeNodeUuid.get(nodeUuid) || '';\n            wfStore.appendOutputToRuntimeNode(\n              wfRuntimeUuid.value,\n              runtimeNodeUuid,\n              chunk,\n            );\n            const hit = runtimeNodes.find(\n              (n: any) => n.uuid === runtimeNodeUuid,\n            );\n            if (hit) {\n              const payload = JSON.parse(chunk);\n              (hit as any).output = {\n                ...(hit as any).output,\n                [payload.name]: payload.content,\n              };\n            }\n          } else if (eventName.includes('[NODE_WAIT_FEEDBACK_BY_')) {\n            humanFeedback.value = true;\n            humanFeedbackTip.value = chunk || '';\n            message.info(humanFeedbackTip.value);\n          }\n        } catch (error) {\n          console.error(error);\n        }\n      },\n      doneCallback: (chunk) => {\n        nextTick(() => {\n          submitting.value = false;\n          resetInputs();\n          wfStore.updateSuccess(currWfUuid, wfRuntimeUuid.value, chunk);\n          runtimeErrorMsg.value = '';\n          message.success('执行成功');\n          emit('runDone');\n        });\n      },\n      errorCallback: (error) => {\n        submitting.value = false;\n        resetInputs();\n        message.error(`系统提示：${error}`);\n        wfStore.updateErrorMsg(currWfUuid, wfRuntimeUuid.value, error);\n        runtimeErrorMsg.value = error || '';\n        emit('runError', error);\n      },\n    });\n  } catch (error: any) {\n    const errorMessage = error?.message ?? '执行出错';\n    message.error(errorMessage);\n    submitting.value = false;\n  }\n}\n\nasync function resume() {\n  submitting.value = true;\n  try {\n    await workflowRuntimeResume({\n      runtimeUuid: wfRuntimeUuid.value,\n      feedbackContent: humanFeedbackContent.value,\n    });\n  } catch (e) {\n    message.error(`系统提示：${e}`);\n  } finally {\n    humanFeedback.value = false;\n    humanFeedbackTip.value = '';\n    humanFeedbackContent.value = '';\n  }\n}\n\nasync function handleFileListChange(info: any) {\n  const ret = await workflowApi.uploadFile(info.file.originFileObj);\n\n  if (ret?.uploadVos && ret.uploadVos.length) {\n    info.file.status = 'done';\n    fileList.value = info.fileList;\n    fileListLength.value = info.fileList.length;\n    uploadedFileUuids.value.push(ret.uploadVos[0].filePath);\n      if (uploadedFileUuids.value.length === fileListLength.value) {\n        run();\n      }\n  }\n\n  // console.log('info', info, uploadedFileUuids);\n  // if (info.file.status === 'done') {\n  //   const res = info.file.response;\n  //   if (res && res.success) {\n  //     uploadedFileUuids.value.push(res.data.uuid);\n  //     if (uploadedFileUuids.value.length === fileListLength.value) {\n  //       run();\n  //     }\n  //   }\n  // }\n}\n\nfunction handleStop() {\n  if (wfStore.wfUuidToWfRuntimeLoading.get(currWfUuid)) controller.abort();\n  submitting.value = false;\n}\n\nfunction handleClick() {\n  showCurrentExecution.value = !showCurrentExecution.value;\n  tabObj.value.tab = showCurrentExecution.value\n    ? `${tabObj.value.defaultTab} ↓`\n    : `${tabObj.value.defaultTab} ↑`;\n}\n\n// function findStartNodeFromWorkflow() {\n//   return Array.isArray(props.workflow?.nodes)\n//     ? props.workflow.nodes.find((n: any) => {\n//         // 优先检查 wfComponent.name（编辑时的数据结构）\n//         if (n?.wfComponent?.name === 'Start') return true;\n//         // 检查 workflowComponentId（后端返回的数据结构）\n//         // 后端返回的是字符串，需要转换为数字比较\n//         if (Number(n?.workflowComponentId) === 1) return true;\n//         return false;\n//       })\n//     : null\n// }\n\n// 根据 workflowComponentId 获取组件名称\nfunction getComponentNameByWorkflowComponentId(\n  workflowComponentId: number | string,\n): string {\n  const id = String(workflowComponentId);\n  return componentIdToNameMap.value[id] || 'Unknown';\n}\nfunction findStartNodeFromWorkflow() {\n  return Array.isArray(props.workflow?.nodes)\n    ? props.workflow.nodes.find((n: any) => {\n        // 优先检查 wfComponent.name（编辑时的数据结构）\n        if (n?.wfComponent?.name === 'Start') return true;\n        // 检查 workflowComponentId（后端返回的数据结构）\n        // 使用映射函数判断组件类型\n        if (n?.workflowComponentId !== undefined) {\n          const componentName = getComponentNameByWorkflowComponentId(\n            n.workflowComponentId,\n          );\n          if (componentName === 'Start') return true;\n        }\n        // 备用方案：检查节点标题是否包含\"开始\"\n        if (n?.title && (n.title === '开始' || n.title.includes('开始'))) {\n          return true;\n        }\n        return false;\n      })\n    : null;\n}\n\nfunction rebuildUserInputs() {\n  const storeStart = wfStore.getStartNode(props.workflow.uuid)\n  const localStart = findStartNodeFromWorkflow()\n  startNode.value = storeStart || localStart || null\n  if (startNode.value && Array.isArray(startNode.value.inputConfig?.user_inputs)) {\n    userInputs.value = startNode.value.inputConfig.user_inputs.map((input: any) => ({\n      uuid: input.uuid,\n      name: input.name,\n      content: { title: input.title, value: null, type: input.type },\n      required: input.required,\n    }))\n  } else {\n    userInputs.value = [];\n  }\n}\n// 获取组件列表并构建映射\nasync function fetchWorkflowComponents() {\n  try {\n    const response = await workflowApi.workflowComponents();\n    // 处理可能的响应格式：可能是直接数组，也可能是包装对象\n    const components = Array.isArray(response)\n      ? response\n      : response?.data || [];\n    const map: Record<string, string> = {};\n    if (Array.isArray(components)) {\n      components.forEach((component: any) => {\n        // 使用 id 作为 key，name 作为 value\n        const id = String(component.id);\n        map[id] = component.name;\n      });\n    }\n    componentIdToNameMap.value = map;\n    console.log('组件ID映射:', map);\n  } catch (error) {\n    console.error('获取组件列表失败:', error);\n    // 如果获取失败，使用空映射，后续会使用备用方案\n    componentIdToNameMap.value = {};\n  }\n}\n// 初始化：获取组件列表并重建用户输入\nasync function init() {\n  await fetchWorkflowComponents();\n  rebuildUserInputs();\n}\n\ninit();\n\nwatch(() => props.workflow, () => rebuildUserInputs(), { deep: true })\n\nheaders.Authorization = token.value;\nonUnmounted(() => {\n  if (wfStore.wfUuidToWfRuntimeLoading.get(currWfUuid)) controller.abort();\n});\n</script>\n\n<template>\n  <div class=\"z-10 m-auto w-full max-w-screen-xl\" @keydown=\"onKeydown\">\n    <Tabs :active-key=\"tabObj.name\" type=\"line\" @click=\"handleClick\">\n      <Tabs.TabPane key=\"runtimes\" :tab=\"tabObj.tab\" />\n    </Tabs>\n    <transition name=\"collapse\">\n      <div\n        v-show=\"showCurrentExecution\"\n        class=\"mb-2 max-h-[500px] overflow-y-auto\"\n      >\n        <RuntimeNodes\n          :nodes=\"runtimeNodes\"\n          :workflow=\"workflow\"\n          :error-msg=\"runtimeErrorMsg\"\n          :token=\"token\"\n        />\n        <div class=\"sticky bottom-0 left-0 flex justify-center\">\n          <Button v-show=\"submitting\" size=\"small\" @click=\"handleStop\">\n            <template #icon><SvgIcon icon=\"ri:stop-circle-line\" /></template>\n            停止请求\n          </Button>\n        </div>\n      </div>\n    </transition>\n    <div v-if=\"errorMsg\">{{ errorMsg }}</div>\n    <div\n      class=\"flex max-h-[300px] flex-col items-center justify-between space-y-2 overflow-y-auto\"\n    >\n      <template v-if=\"!humanFeedback\">\n        <div\n          v-for=\"(userInput, idx) in userInputs\"\n          :key=\"`${idx}_${userInput.name}`\"\n          class=\"flex w-full\"\n        >\n          <div class=\"min-w-24\">{{ userInput.content.title }}</div>\n          <Input.TextArea\n            v-if=\"userInput.content.type === 1\"\n            v-model:value=\"userInput.content.value\"\n            :auto-size=\"{ minRows: 1, maxRows: 5 }\"\n          />\n          <InputNumber\n            v-if=\"userInput.content.type === 2\"\n            v-model:value=\"userInput.content.value\"\n            class=\"w-full\"\n          />\n          <div v-if=\"userInput.content.type === 3\" />\n<Upload v-if=\"userInput.content.type === 4\" ref=\"uploadRef\" multiple :max-count=\"startNode?.inputConfig.user_inputs.find((item: any) => item.uuid === userInput.uuid)?.limit || 10\" :headers=\"headers\" :file-list=\"fileList\" @change=\"handleFileListChange\">\n          <!-- <Upload v-if=\"userInput.content.type === 4\" ref=\"uploadRef\" multiple :action=\"getUploadAction()\" :max-count=\"startNode?.inputConfig.user_inputs.find((item: any) => item.uuid === userInput.uuid)?.limit || 10\" :headers=\"headers\" :file-list=\"fileList\" @change=\"handleFileListChange\"> -->\n            <!-- <Upload.Dragger> -->\n              <div style=\"font-size: 16px\">点击或者拖动文件到该区域来上传</div>\n              <div style=\"margin: 4px 0 0 0; color: #999\">文件格式: TXT、PDF、DOC、DOCX、XLS、XLXS、PPT、PPTX；文件大小：不超过10M</div>\n            <!-- </Upload.Dragger> -->\n          </Upload>\n          <Switch\n            v-if=\"userInput.content.type === 5\"\n            v-model:checked=\"userInput.content.value\"\n          />\n        </div>\n        <div\n          class=\"flex w-full items-center justify-between text-xs text-gray-400\"\n          @keydown.enter=\"run\"\n        >\n          <div>按 Enter 提交，Shift + Enter 换行</div>\n          <Button\n            type=\"primary\"\n            :disabled=\"submitting\"\n            :loading=\"submitting\"\n            @click=\"run\"\n            >提交</Button\n          >\n        </div>\n      </template>\n      <template v-if=\"humanFeedback\">\n        <div class=\"flex w-full flex-col space-y-2 p-2\">\n          <div class=\"flex rounded-md bg-gray-100 px-2 py-1\">\n            <div class=\"text-base text-red-500\">\n              流程已暂停，等待用户输入中...\n            </div>\n          </div>\n          <div class=\"flex w-full flex-col\">\n            <div v-if=\"humanFeedbackTip\" class=\"text-sm leading-8\">\n              提示：{{ humanFeedbackTip }}\n            </div>\n            <Input.TextArea\n              v-model:value=\"humanFeedbackContent\"\n              :auto-size=\"{ minRows: 2, maxRows: 5 }\"\n            />\n          </div>\n          <div class=\"flex justify-end\">\n            <Button type=\"primary\" @click=\"resume\">提交</Button>\n          </div>\n        </div>\n      </template>\n    </div>\n  </div>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/packages/workflow-designer/components/RuntimeNodes.vue",
    "content": "<script lang=\"ts\" setup>\nimport { computed } from 'vue'\nimport { Image } from 'ant-design-vue'\nimport SvgIcon from './SvgIcon.vue'\nimport { getIconByComponentName, getIconClassByComponentName } from '../utils/workflow-util'\n\ninterface Props {\n  nodes: any[]\n  workflow: any\n  errorMsg: string\n  token?: string\n}\nconst props = defineProps<Props>()\n\nconst prologue = computed(() => {\n  const startNode = (props.workflow?.nodes || []).find((n: any) => n.wfComponent?.name === 'Start')\n  return (startNode?.nodeConfig || {}).prologue || ''\n})\n\n// 根据 workflowComponentId 获取组件名称\nfunction getComponentNameByWorkflowComponentId(workflowComponentId: number | string): string {\n  const id = Number(workflowComponentId)\n  switch (id) {\n    case 1: return 'Start'\n    case 2: return 'End'\n    case 3: return 'Answer'\n    case 4: return 'Classifier'\n    case 5: return 'KeywordExtractor'\n    case 6: return 'KnowledgeRetrieval'\n    case 7: return 'DocumentExtractor'\n    case 8: return 'FaqExtractor'\n    case 9: return 'Switcher'\n    case 10: return 'Template'\n    case 11: return 'Dalle3'\n    case 12: return 'TongyiWanx'\n    case 13: return 'Google'\n    case 14: return 'HumanFeedback'\n    case 15: return 'MailSend'\n    case 16: return 'HttpRequest'\n    default: return 'Start'\n  }\n}\n\nfunction getRealFileUrl(fileUrl: string) {\n  if (!fileUrl.includes('http') && !fileUrl.includes('/api')) return `/api${fileUrl}`\n  return fileUrl\n}\n</script>\n\n<template>\n  <div>\n    <div v-if=\"errorMsg\" class=\"py-2 text-red-500\">错误：{{ errorMsg }}</div>\n    <div v-else-if=\"nodes.length === 0\" class=\"text-center py-2 text-neutral-400\">无内容</div>\n    <div v-show=\"prologue\" class=\"p-2\">{{ prologue }}</div>\n    <div v-for=\"node in nodes\" :key=\"node.uuid\" class=\"flex flex-col space-y-2 border border-gray-200 p-2 m-2 rounded-md\" :title=\"node.nodeTitle\" :name=\"node.uuid\">\n      <!-- 调试信息 -->\n      <!-- <div class=\"text-xs text-gray-500\">\n        Debug: wfComponent={{ node.wfComponent }}, workflowComponentId={{ node.workflowComponentId }}\n      </div> -->\n      <div class=\"flex items-center space-x-1 bg-gray-100 px-2 py-1 rounded-md\">\n        <SvgIcon \n          v-if=\"node.wfComponent || node.workflowComponentId\" \n          class=\"text-base\" \n          :class=\"node.wfComponent ? getIconClassByComponentName(node.wfComponent.name) : getIconClassByComponentName(getComponentNameByWorkflowComponentId(node.workflowComponentId))\" \n          :icon=\"node.wfComponent ? getIconByComponentName(node.wfComponent.name) : getIconByComponentName(getComponentNameByWorkflowComponentId(node.workflowComponentId))\" \n        />\n        <div class=\"text-base\">{{ node.nodeTitle || '找不到节点标题' }}</div>\n      </div>\n      <div class=\"flex flex-col space-y-2\">\n        <div class=\"text-base border-b border-gray-200 py-1\">输入</div>\n        <div v-for=\"(content, name) in node.input\" :key=\"`input_${name}`\" class=\"flex\">\n          <div class=\"min-w-24 pr-2\">{{ name }}</div>\n          <div>{{ content.value || '无内容' }}</div>\n        </div>\n        <div class=\"text-base border-b border-gray-200 py-1\">输出</div>\n        <!-- 优先展示流式增量（chunks），用于未产出最终输出时的实时渲染 -->\n        <div v-if=\"node.chunks\" class=\"flex\">\n          <!-- <div class=\"min-w-24 pr-2\">回复</div> -->\n          <div class=\"whitespace-pre-wrap break-words\">{{ node.chunks }}</div>\n        </div>\n        <div v-else v-for=\"(content, name) in node.output\" :key=\"`onput_${name}`\" class=\"flex\">\n          <template v-if=\"content.type === 4\">\n            <Image.PreviewGroup>\n              <Image v-for=\"url in content.value\" :key=\"url\" :src=\"`${getRealFileUrl(url)}?token=${token || ''}`\" :width=\"100\" />\n            </Image.PreviewGroup>\n          </template>\n          <template v-else>\n            <div class=\"min-w-24 pr-2\">{{ name }}</div>\n            <div class=\"whitespace-pre-wrap break-words\">{{ content.value || '无内容' }}</div>\n          </template>\n        </div>\n      </div>\n    </div>\n  </div>\n</template>\n\n\n"
  },
  {
    "path": "apps/web-antd/src/packages/workflow-designer/components/SvgIcon.vue",
    "content": "<script setup lang=\"ts\">\nimport { VbenIcon } from '@vben/icons'\n\ninterface Props { icon: string }\ndefineProps<Props>()\n</script>\n\n<template>\n  <VbenIcon :icon=\"icon\" />\n</template>\n\n\n"
  },
  {
    "path": "apps/web-antd/src/packages/workflow-designer/components/WfVariableSelector.vue",
    "content": "<script setup lang=\"ts\">\nimport { h, ref, computed } from 'vue'\nimport { Select, Button, Collapse, Input } from 'ant-design-vue'\nimport type { VNodeChild } from 'vue'\nimport { emptyWorkflowInfo } from '../utils/workflow-util'\nimport { getIconByComponentName, getIconClassByComponentName } from '../utils/workflow-util'\nimport SvgIcon from './SvgIcon.vue'\nimport type { WorkflowInfo, WorkflowNode } from '../types/index.d'\n\ninterface Props {\n  workflow: WorkflowInfo\n  wfNode: WorkflowNode\n  excludeNodes: string[]\n  whiteListComponents?: string[]\n  whiteListUserInputTypes?: number[]\n}\nconst props = withDefaults(defineProps<Props>(), {\n  workflow: () => emptyWorkflowInfo(),\n  whiteListComponents: () => [],\n})\n\n// 本组件内部状态（自动写回节点）\nconst selectedVars = ref<string[]>([])\nconst keysRef = ref<string[]>([])\n\nfunction renderDropdownLabel(option: any): VNodeChild {\n  if (option.options) return option.label as string\n  const val = String(option.value || '')\n  const nodeUuid = val.split('::')[0]\n  const componentName = (props.workflow.nodes || []).find(item => item.uuid === nodeUuid)?.wfComponent?.name || ''\n  return [\n    h('div', { class: 'flex items-center' }, {\n      default: () => [\n        h(SvgIcon, { icon: getIconByComponentName(componentName), class: getIconClassByComponentName(componentName) }),\n        h('div', { class: 'ml-1.5' }, { default: () => option.label as string }),\n      ],\n    }),\n  ]\n}\n\nconst options = computed(() => {\n  const userInputOptions: any[] = []\n  const componentOutputOptions: any[] = []\n  const nodes = props.workflow.nodes || []\n  for (let i = 0; i < nodes.length; i++) {\n    const node = nodes[i]\n    if (!node || !node.uuid || !node.wfComponent) continue\n    if ((props.excludeNodes || []).includes(node.uuid) || node.wfComponent.name === 'End') continue\n    if ((props.whiteListComponents || []).length > 0 && !(props.whiteListComponents || []).includes(node.wfComponent.name)) continue\n    const inputConfig = node.inputConfig || { user_inputs: [], ref_inputs: [] }\n    if (node.wfComponent.name === 'Start') {\n      const arr = Array.isArray(inputConfig.user_inputs) ? inputConfig.user_inputs : []\n      for (let j = 0; j < arr.length; j++) {\n        const userInput: any = arr[j]\n        if ((props.whiteListUserInputTypes || []).length > 0 && !(props.whiteListUserInputTypes || []).includes(userInput.type)) continue\n        userInputOptions.push({ label: userInput.title, value: `${node.uuid}::${userInput.name}` })\n      }\n    } else {\n      componentOutputOptions.push({ label: node.title, value: `${node.uuid}::output` })\n    }\n  }\n  return [\n    { label: '用户的输入', options: userInputOptions },\n    { label: '节点的输出', options: componentOutputOptions },\n  ]\n})\n\n// function getLabelByValue(val: string): string { /* 保留备选 */ return '' }\n\n// 初始化：从节点自身读取\nfunction rebuildSelectedVars() {\n  const src = ((props.wfNode as any)?.inputConfig?.ref_inputs || []) as any[]\n  selectedVars.value = src.map((x) => `${x.node_uuid || ''}::${x.node_param_name || ''}`)\n  keysRef.value = src.map((x) => x.name || '')\n  syncKeysLength()\n}\n\nrebuildSelectedVars()\n\nfunction two(n: number) { return String(n).padStart(2, '0') }\nfunction defaultKeyAt(idx: number) { return `var_${two(idx + 1)}` }\nfunction nextAutoKey(list: string[]) {\n  const nums = list\n    .map((k) => (k && /^var_(\\d+)$/.test(k) ? Number((k.match(/^var_(\\d+)$/) as RegExpMatchArray)[1]) : 0))\n  const max = nums.length ? Math.max(...nums) : 0\n  return `var_${two(max + 1)}`\n}\nfunction syncKeysLength() {\n  const targetLen = selectedVars.value.length\n  if (!Array.isArray(keysRef.value)) keysRef.value = []\n  while (keysRef.value.length < targetLen) keysRef.value.push(nextAutoKey(keysRef.value))\n  while (keysRef.value.length > targetLen) keysRef.value.pop()\n}\n\nfunction toRefDef(val: string): { node_uuid: string; node_param_name: string } {\n  const vs = String(val || '').split('::')\n  return { node_uuid: vs[0] || '', node_param_name: vs[1] || '' }\n}\n\nfunction handleSelectAt(index: number, value: string) {\n  const arr = selectedVars.value.slice()\n  arr[index] = value\n  selectedVars.value = arr\n  // 写回节点\n  const payload = selectedVars.value.map(toRefDef)\n  const next = payload.map((m, i) => ({ name: keysRef.value[i] || `var_${i + 1}`, node_uuid: m.node_uuid, node_param_name: m.node_param_name }))\n  if (!(props.wfNode as any).inputConfig) (props.wfNode as any).inputConfig = { user_inputs: [], ref_inputs: [] }\n  ;(props.wfNode.inputConfig as any).ref_inputs = next\n}\n\nfunction addVariable() {\n  selectedVars.value = [...selectedVars.value, '']\n  keysRef.value = [...keysRef.value, nextAutoKey(keysRef.value)]\n  handleSelectAt(selectedVars.value.length - 1, '')\n}\n\nfunction removeVariable(index: number) {\n  const arr = selectedVars.value.slice()\n  arr.splice(index, 1)\n  selectedVars.value = arr\n  const ks = keysRef.value.slice()\n  ks.splice(index, 1)\n  keysRef.value = ks\n  const payload = selectedVars.value.map(toRefDef)\n  const next = payload.map((m, i) => ({ name: keysRef.value[i] || `var_${i + 1}`, node_uuid: m.node_uuid, node_param_name: m.node_param_name }))\n  if (!(props.wfNode as any).inputConfig) (props.wfNode as any).inputConfig = { user_inputs: [], ref_inputs: [] }\n  ;(props.wfNode.inputConfig as any).ref_inputs = next\n}\n</script>\n\n<template>\n  <Collapse :default-active-key=\"['vars']\">\n    <Collapse.Panel key=\"vars\" header=\"输入\">\n      <div class=\"flex flex-col gap-2\">\n        <div v-for=\"(sv, idx) in selectedVars\" :key=\"idx\" class=\"flex items-center gap-2\">\n          <div class=\"min-w-36 text-gray-600 flex items-center justify-between pr-2\">\n            <Input v-model:value=\"keysRef[idx]\" placeholder=\"var_xxx\" size=\"small\" />\n          </div>\n          <Select\n            :value=\"sv\"\n            :show-arrow=\"true\"\n            :options=\"options\"\n            @update:value=\"(val: string) => handleSelectAt(idx, val)\"\n            class=\"flex-1\"\n          >\n            <template #optionRender=\"{ option }\">\n              <component :is=\"() => renderDropdownLabel(option)\" />\n            </template>\n          </Select>\n          <Button size=\"small\" title=\"删除\" @click=\"removeVariable(idx)\">\n            <SvgIcon icon=\"carbon:delete\" />\n          </Button>\n        </div>\n        <div>\n          <Button size=\"small\" type=\"dashed\" @click=\"addVariable\">+ 新增变量</Button>\n        </div>\n      </div>\n    </Collapse.Panel>\n  </Collapse>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/packages/workflow-designer/components/edges/CustomEdge.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { EdgeProps, Position } from '@vue-flow/core'\nimport { EdgeLabelRenderer, getBezierPath, useVueFlow } from '@vue-flow/core'\nimport type { CSSProperties } from 'vue'\nimport { computed } from 'vue'\n\ninterface CustomEdgeProps<T = any> extends EdgeProps<T> {\n  id: string\n  sourceX: number\n  sourceY: number\n  targetX: number\n  targetY: number\n  sourcePosition: Position\n  targetPosition: Position\n  data: T\n  markerEnd: string\n  style?: CSSProperties\n}\n\nconst props = defineProps<CustomEdgeProps>()\n\nconst { removeEdges } = useVueFlow()\n\nconst path = computed(() => getBezierPath(props))\n</script>\n\n<script lang=\"ts\">\nexport default {\n  inheritAttrs: false,\n}\n</script>\n\n<template>\n  <path :id=\"id\" :style=\"style\" class=\"vue-flow__edge-path\" :d=\"path[0]\" :marker-end=\"markerEnd\" />\n\n  <EdgeLabelRenderer>\n    <div\n      :style=\"{\n        pointerEvents: 'all',\n        position: 'absolute',\n        transform: `translate(-50%, -50%) translate(${path[1]}px,${path[2]}px)`,\n      }\"\n      class=\"nodrag nopan\"\n    >\n      <button class=\"edgebutton\" @click=\"removeEdges(id)\">\n        ×\n      </button>\n    </div>\n  </EdgeLabelRenderer>\n</template>\n\n<style>\n.edgebutton {\n  border-radius: 999px;\n  cursor: pointer;\n}\n.edgebutton:hover {\n  box-shadow: 0 0 0 2px pink, 0 0 0 4px #f05f75;\n}\n</style>\n"
  },
  {
    "path": "apps/web-antd/src/packages/workflow-designer/components/edges/CustomEdge2.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { EdgeProps, Position } from '@vue-flow/core'\nimport { BezierEdge } from '@vue-flow/core'\n\ninterface CustomData { text: string }\n\ninterface CustomEdgeProps extends EdgeProps<CustomData> {\n  source: string\n  target: string\n  sourceHandleId?: string\n  targetHandleId?: string\n  id: string\n  sourceX: number\n  sourceY: number\n  targetX: number\n  targetY: number\n  sourcePosition: Position\n  targetPosition: Position\n  markerEnd: string\n  data: CustomData\n}\n\ndefineProps<CustomEdgeProps>()\n</script>\n\n<script lang=\"ts\">\nexport default { inheritAttrs: false }\n</script>\n\n<template>\n  <BezierEdge\n    :label=\"data.text\"\n    :source-x=\"sourceX\"\n    :source-y=\"sourceY\"\n    :target-x=\"targetX\"\n    :target-y=\"targetY\"\n    :source-position=\"sourcePosition\"\n    :target-position=\"targetPosition\"\n    :marker-end=\"markerEnd\"\n    :label-style=\"{ fill: 'white' }\"\n    :label-show-bg=\"true\"\n    :label-bg-style=\"{ fill: 'red' }\"\n    :label-bg-padding=\"[2, 4]\"\n    :label-bg-border-radius=\"2\"\n  />\n</template>\n\n\n"
  },
  {
    "path": "apps/web-antd/src/packages/workflow-designer/components/edges/SpecialEdge.vue",
    "content": "<script setup lang=\"ts\">\nimport { computed } from 'vue'\nimport { BaseEdge, EdgeLabelRenderer, getBezierPath, type EdgeProps } from '@vue-flow/core'\n\nconst props = defineProps<EdgeProps>()\nconst path = computed(() => getBezierPath(props))\n</script>\n\n<script lang=\"ts\">\nexport default { inheritAttrs: false }\n</script>\n\n<template>\n  <BaseEdge :path=\"path[0]\" />\n  <EdgeLabelRenderer>\n    <div :style=\"{ pointerEvents: 'none', position: 'absolute', transform: `translate(-50%, -50%) translate(${path[1]}px,${path[2]}px)` }\" class=\"nodrag nopan\" />\n  </EdgeLabelRenderer>\n</template>\n\n\n"
  },
  {
    "path": "apps/web-antd/src/packages/workflow-designer/components/nodes/AnswerNode.vue",
    "content": "<script setup lang=\"ts\">\nimport { Handle, Position } from '@vue-flow/core'\nimport type { NodeProps } from '@vue-flow/core'\nimport CommonNodeHeader from '../../components/CommonNodeHeader.vue'\nimport SvgIcon from '../../components/SvgIcon.vue'\n\ndefineProps<NodeProps>()\n</script>\n\n<template>\n  <div class=\"flex flex-col w-full\">\n    <Handle type=\"target\" :position=\"Position.Left\" />\n    <Handle type=\"source\" :position=\"Position.Right\" />\n    <CommonNodeHeader :wf-node=\"data\" />\n    <div clas=\"flex-1 flex-col\">\n      <div class=\"content_line flex items-center px-2\">\n        <SvgIcon class=\"mx-1.5\" icon=\"mdi:robot\" />\n        {{ data.nodeConfig?.model_name }}\n      </div>\n    </div>\n  </div>\n</template>\n\n\n"
  },
  {
    "path": "apps/web-antd/src/packages/workflow-designer/components/nodes/ClassifierNode.vue",
    "content": "<script setup lang=\"ts\">\nimport type { NodeProps } from '@vue-flow/core'\nimport NodeShell from './NodeShell.vue'\n\ndefineProps<NodeProps>()\n</script>\n\n<template>\n  <NodeShell :data=\"data\" />\n</template>\n\n\n"
  },
  {
    "path": "apps/web-antd/src/packages/workflow-designer/components/nodes/Dalle3Node.vue",
    "content": "<script setup lang=\"ts\">\nimport type { NodeProps } from '@vue-flow/core'\nimport NodeShell from './NodeShell.vue'\n\ndefineProps<NodeProps>()\n</script>\n\n<template>\n  <NodeShell :data=\"data\" />\n</template>\n\n\n"
  },
  {
    "path": "apps/web-antd/src/packages/workflow-designer/components/nodes/DocumentExtractorNode.vue",
    "content": "<script setup lang=\"ts\">\nimport type { NodeProps } from '@vue-flow/core'\nimport NodeShell from './NodeShell.vue'\n\ndefineProps<NodeProps>()\n</script>\n\n<template>\n  <NodeShell :data=\"data\" />\n</template>\n\n\n"
  },
  {
    "path": "apps/web-antd/src/packages/workflow-designer/components/nodes/EndNode.vue",
    "content": "<script setup lang=\"ts\">\nimport { Handle, Position } from '@vue-flow/core'\nimport type { NodeProps } from '@vue-flow/core'\nimport CommonNodeHeader from '../../components/CommonNodeHeader.vue'\n\ndefineProps<NodeProps>()\n</script>\n\n<template>\n  <div class=\"flex flex-col w-full\">\n    <Handle type=\"target\" :position=\"Position.Left\" />\n    <CommonNodeHeader :wf-node=\"data\" />\n    <div class=\"flex-1 flex-col\">\n      <div class=\"leading-6 bg-gray-100 mb-1 px-1 text-left text-xs line-clamp-3\">\n        {{ data.nodeConfig?.result }}\n      </div>\n    </div>\n  </div>\n</template>\n\n\n"
  },
  {
    "path": "apps/web-antd/src/packages/workflow-designer/components/nodes/FaqExtractorNode.vue",
    "content": "<script setup lang=\"ts\">\nimport type { NodeProps } from '@vue-flow/core'\nimport NodeShell from './NodeShell.vue'\n\ndefineProps<NodeProps>()\n</script>\n\n<template>\n  <NodeShell :data=\"data\" />\n</template>\n\n\n"
  },
  {
    "path": "apps/web-antd/src/packages/workflow-designer/components/nodes/GoogleNode.vue",
    "content": "<script setup lang=\"ts\">\nimport { Handle, Position } from '@vue-flow/core'\nimport type { NodeProps } from '@vue-flow/core'\nimport CommonNodeHeader from '../../components/CommonNodeHeader.vue'\n\ndefineProps<NodeProps>()\n</script>\n\n<template>\n  <div class=\"flex flex-col w-full\">\n    <Handle type=\"target\" :position=\"Position.Left\" />\n    <Handle type=\"source\" :position=\"Position.Right\" />\n    <CommonNodeHeader :wf-node=\"data\" />\n    <div class=\"flex-1 flex-col\">\n      <div class=\"content_line text-left px-3\">国家和地区： {{ data.nodeConfig?.country || 'cn' }}</div>\n      <div class=\"content_line text-left px-3\">语言： {{ data.nodeConfig?.language || 'zh-cn' }}</div>\n      <div class=\"content_line text-left px-3\">提取数量： {{ data.nodeConfig?.top_n ?? 5 }}</div>\n    </div>\n  </div>\n</template>\n\n\n"
  },
  {
    "path": "apps/web-antd/src/packages/workflow-designer/components/nodes/HumanFeedbackNode.vue",
    "content": "<script setup lang=\"ts\">\nimport { Handle, Position } from '@vue-flow/core'\nimport type { NodeProps } from '@vue-flow/core'\nimport CommonNodeHeader from '../CommonNodeHeader.vue'\nimport SvgIcon from '../SvgIcon.vue'\n\ndefineProps<NodeProps>()\n</script>\n\n<template>\n  <div class=\"flex flex-col w-full\">\n    <Handle type=\"target\" :position=\"Position.Left\" />\n    <Handle type=\"source\" :position=\"Position.Right\" />\n    <CommonNodeHeader :wf-node=\"data\" />\n    <div clas=\"flex-1 flex-col\">\n      <div class=\"content_line flex items-center px-2\">\n        <SvgIcon class=\"mx-1.5\" icon=\"mdi:human\" />\n        {{ data.nodeConfig?.model_name }}\n      </div>\n    </div>\n  </div>\n</template>\n\n\n"
  },
  {
    "path": "apps/web-antd/src/packages/workflow-designer/components/nodes/KeywordExtractorNode.vue",
    "content": "<script setup lang=\"ts\">\nimport type { NodeProps } from '@vue-flow/core'\nimport NodeShell from './NodeShell.vue'\n\ndefineProps<NodeProps>()\n</script>\n\n<template>\n  <NodeShell :data=\"data\" />\n</template>\n\n\n"
  },
  {
    "path": "apps/web-antd/src/packages/workflow-designer/components/nodes/NodeShell.vue",
    "content": "<script setup lang=\"ts\">\nimport { Handle, Position } from '@vue-flow/core'\nimport CommonNodeHeader from '../../components/CommonNodeHeader.vue'\nimport { computed } from 'vue'\n\ninterface Props {\n  data: any\n  hasTarget?: boolean\n  hasSource?: boolean\n  extra?: string\n}\nconst props = withDefaults(defineProps<Props>(), { hasTarget: true, hasSource: true, extra: '' })\n\nfunction cut(val: string, n = 30) {\n  if (!val) return ''\n  return val.length > n ? `${val.slice(0, n)}...` : val\n}\n\nconst lines = computed(() => {\n  const n = (props.data?.wfComponent?.name || '').toLowerCase()\n  const c = props.data?.nodeConfig || {}\n  switch (n) {\n    case 'answer':\n      return [`${c.model_name || ''}`]\n    case 'documentextractor':\n      return [cut(c.source || ''), cut(c.pattern || '')].filter(Boolean)\n    case 'keywordextractor':\n      return [`关键词数量： ${c.top_n ?? 5}`, `模型： ${c.model_name || ''}`]\n    case 'faqextractor':\n      return [`抽取数量： ${c.top_n ?? 5}`, `模型： ${c.model_name || ''}`]\n    case 'knowledgeretrieval':\n      return [\n        `知识库： ${c.knowledge_base_name || c.knowledge_base_uuid || ''}`,\n        `数量： ${c.top_n ?? 3}  分数： ${c.score ?? 0.6}`,\n        `严格模式： ${c.is_strict ? '是' : '否'}`,\n      ]\n    case 'switcher':\n      return [`分支数： ${Array.isArray(c.cases) ? c.cases.length : 0}`]\n    case 'template':\n      return [cut(c.content || c.prompt || '')]\n    case 'httprequest':\n      return [`${c.method || 'GET'} ${cut(c.url || '')}`]\n    case 'mailsend':\n      return [`收件人： ${cut(c.to_mails || '')}`, `主题： ${cut(c.subject || '')}`]\n    case 'humanfeedback':\n      return [cut(c.tip || '')]\n    case 'google':\n      return [`国家和地区： ${c.country || 'cn'}`, `语言： ${c.language || 'zh-cn'}`, `提取数量： ${c.top_n ?? 5}`]\n    case 'dalle3':\n      return [`尺寸： ${c.size || ''}`, `质量： ${c.quality || ''}`]\n    case 'tongyiwanx':\n      return [`模型： ${c.model_name || ''}`, `尺寸： ${c.size || ''}`]\n    default:\n      return props.extra ? [props.extra] : []\n  }\n})\n</script>\n\n<template>\n  <div class=\"flex flex-col w-full\">\n    <Handle v-if=\"hasTarget\" type=\"target\" :position=\"Position.Left\" />\n    <Handle v-if=\"hasSource\" type=\"source\" :position=\"Position.Right\" />\n    <CommonNodeHeader :wf-node=\"data\" />\n    <div v-for=\"(line, idx) in lines\" :key=\"idx\" class=\"content_line text-left px-3\">{{ line }}</div>\n    <div v-if=\"!lines.length && extra\" class=\"content_line text-left px-3\">{{ extra }}</div>\n  </div>\n  \n</template>\n\n\n"
  },
  {
    "path": "apps/web-antd/src/packages/workflow-designer/components/nodes/StartNode.vue",
    "content": "<script setup lang=\"ts\">\nimport { Handle, Position } from '@vue-flow/core'\nimport type { NodeProps } from '@vue-flow/core'\nimport CommonNodeHeader from '../../components/CommonNodeHeader.vue'\nimport SvgIcon from '../../components/SvgIcon.vue'\n\ndefineProps<NodeProps>()\n</script>\n\n<template>\n  <div class=\"flex flex-col w-full\">\n    <Handle type=\"source\" :position=\"Position.Right\" />\n    <CommonNodeHeader :wf-node=\"data\" />\n    <div class=\"flex-1 flex-col\">\n      <div v-for=\"userInputDef in data.inputConfig.user_inputs\" :key=\"userInputDef.uuid\" class=\"content_line flex justify-items-start px-2\">\n        <div class=\"flex-none w-4 content-center mr-1\">\n          <SvgIcon v-if=\"userInputDef.type === 1\" icon=\"carbon:string-text\" />\n          <SvgIcon v-else-if=\"userInputDef.type === 2\" icon=\"carbon:string-integer\" />\n          <SvgIcon v-else-if=\"userInputDef.type === 3\" icon=\"carbon:list-boxes\" />\n          <SvgIcon v-else-if=\"userInputDef.type === 4\" icon=\"carbon:list-dropdown\" />\n          <SvgIcon v-else-if=\"userInputDef.type === 5\" icon=\"carbon:boolean\" />\n        </div>\n        <div class=\"w-24 overflow-hidden mr-1\">{{ userInputDef.name }}</div>\n        <div class=\"flex-1 overflow-hidden\">{{ userInputDef.title }}</div>\n      </div>\n    </div>\n  </div>\n</template>\n\n\n"
  },
  {
    "path": "apps/web-antd/src/packages/workflow-designer/components/nodes/SwitcherNode.vue",
    "content": "<script setup lang=\"ts\">\nimport { computed } from 'vue';\nimport { Handle, Position } from '@vue-flow/core';\nimport type { NodeProps } from '@vue-flow/core';\nimport CommonNodeHeader from '../CommonNodeHeader.vue';\n\nconst props = defineProps<NodeProps>();\n\n// 获取分支配置\nconst nodeConfig = computed(() => (props.data?.nodeConfig || {}) as any);\n\n// 获取所有分支（用于生成Handle）\nconst cases = computed(() => {\n  return Array.isArray(nodeConfig.value.cases) ? nodeConfig.value.cases : [];\n});\n\n// 计算每个Handle的垂直位置\nfunction getHandlePosition(index: number, total: number) {\n  if (total === 0) return 50;\n  if (total === 1) return 50;\n\n  // 如果是最后一个（默认分支），固定在50%位置\n  if (index === total - 1) {\n    return 50;\n  }\n\n  // 其他分支均匀分布在15%到85%之间\n  const step = 70 / total;\n  return 15 + step * (index + 0.5);\n}\n\n// 截断文本\nfunction cut(val: string, n = 20) {\n  if (!val) return '';\n  return val.length > n ? `${val.slice(0, n)}...` : val;\n}\n\n// 获取条件摘要\nfunction getConditionSummary(caseItem: any) {\n  if (!caseItem.conditions || caseItem.conditions.length === 0) {\n    return '未配置条件';\n  }\n  const first = caseItem.conditions[0];\n  const count = caseItem.conditions.length;\n  const summary = `${first.node_param_name || '参数'} ${first.operator || ''} ${cut(first.value || '', 10)}`;\n  return count > 1 ? `${summary} +${count - 1}` : summary;\n}\n</script>\n\n<template>\n  <div class=\"switcher-node\">\n    <!-- 输入Handle -->\n    <Handle type=\"target\" :position=\"Position.Left\" class=\"handle-target\" />\n\n    <!-- 节点头部 -->\n    <CommonNodeHeader :wf-node=\"data\" />\n\n    <!-- 节点内容 -->\n    <div class=\"node-content\">\n      <div class=\"info-line\">\n        <span class=\"label\">分支数量:</span>\n        <span class=\"value\">{{ cases.length }}</span>\n      </div>\n      <div class=\"info-line\">\n        <span class=\"label\">逻辑:</span>\n        <span class=\"value\">{{\n          cases[0]?.operator?.toUpperCase() || 'AND'\n        }}</span>\n      </div>\n    </div>\n\n    <!-- 分支列表 -->\n    <div v-if=\"cases.length > 0\" class=\"branches-list\">\n      <div\n        v-for=\"(caseItem, index) in cases\"\n        :key=\"caseItem.uuid\"\n        class=\"branch-item\"\n        :style=\"{ top: `${getHandlePosition(index, cases.length + 1)}%` }\"\n      >\n        <div class=\"branch-label\">\n          <span class=\"branch-number\">{{ index + 1 }}</span>\n          <span class=\"branch-condition\">{{\n            getConditionSummary(caseItem)\n          }}</span>\n        </div>\n        <!-- 每个分支的输出Handle -->\n        <Handle\n          :id=\"caseItem.uuid\"\n          type=\"source\"\n          :position=\"Position.Right\"\n          class=\"handle-source branch-handle\"\n          :style=\"{ top: '50%', transform: 'translateY(-50%)' }\"\n        />\n      </div>\n    </div>\n\n    <!-- 默认分支 -->\n    <div class=\"default-branch-wrapper\">\n      <div\n        class=\"default-branch\"\n        :style=\"{\n          top: `${getHandlePosition(cases.length, cases.length + 1)}%`,\n        }\"\n      >\n        <div class=\"branch-label default\">\n          <span class=\"branch-number\">默认</span>\n          <span class=\"branch-condition\">其他情况</span>\n        </div>\n        <Handle\n          id=\"default\"\n          type=\"source\"\n          :position=\"Position.Right\"\n          class=\"handle-source default-handle\"\n          :style=\"{ top: '50%', transform: 'translateY(-50%)' }\"\n        />\n      </div>\n    </div>\n  </div>\n</template>\n\n<style scoped>\n.switcher-node {\n  min-width: 280px;\n  background: white;\n  border: 2px solid #3b82f6;\n  border-radius: 8px;\n  padding: 0;\n  position: relative;\n  box-shadow: 0 2px 8px rgba(59, 130, 246, 0.15);\n  cursor: pointer; /* 明确指示节点可点击 */\n}\n\n.node-content {\n  padding: 8px 12px;\n  border-bottom: 1px solid #f3f4f6;\n}\n\n.info-line {\n  display: flex;\n  justify-content: space-between;\n  font-size: 12px;\n  padding: 2px 0;\n}\n\n.info-line .label {\n  color: #6b7280;\n  font-weight: 500;\n}\n\n.info-line .value {\n  color: #111827;\n  font-weight: 600;\n}\n\n.branches-list {\n  position: relative;\n  min-height: 100px;\n  padding: 8px 0;\n}\n\n.default-branch-wrapper {\n  position: relative;\n  min-height: 60px;\n  padding: 12px 0;\n  margin-top: 8px;\n  border-top: 1px dashed #e5e7eb;\n  background: linear-gradient(to bottom, transparent, #f8fafc);\n}\n\n.branch-item,\n.default-branch {\n  position: absolute;\n  right: 0;\n  width: 100%;\n  padding: 6px 40px 6px 12px;\n  display: flex;\n  align-items: center;\n  transform: translateY(-50%);\n  pointer-events: none; /* 允许点击事件穿透到父节点 */\n}\n\n.branch-label {\n  display: flex;\n  align-items: center;\n  gap: 6px;\n  font-size: 11px;\n  flex: 1;\n  pointer-events: none; /* 允许点击事件穿透 */\n}\n\n.branch-number {\n  display: inline-flex;\n  align-items: center;\n  justify-content: center;\n  min-width: 20px;\n  height: 20px;\n  background: linear-gradient(135deg, #3b82f6, #2563eb);\n  color: white;\n  border-radius: 4px;\n  font-weight: 600;\n  font-size: 10px;\n  padding: 0 4px;\n  box-shadow: 0 1px 3px rgba(59, 130, 246, 0.3);\n}\n\n.branch-label.default {\n  opacity: 0.9;\n}\n\n.branch-label.default .branch-number {\n  background: linear-gradient(135deg, #6b7280, #4b5563);\n  box-shadow: 0 1px 3px rgba(107, 114, 128, 0.3);\n  min-width: 44px;\n  padding: 0 8px;\n}\n\n.branch-condition {\n  color: #4b5563;\n  overflow: hidden;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n  flex: 1;\n}\n\n.branch-label.default .branch-condition {\n  color: #6b7280;\n  font-style: italic;\n}\n\n.handle-target {\n  left: -8px;\n  width: 12px;\n  height: 12px;\n  background: #3b82f6;\n  border: 2px solid white;\n  box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2);\n}\n\n.handle-source {\n  right: -8px;\n  width: 12px;\n  height: 12px;\n  background: #3b82f6;\n  border: 2px solid white;\n  box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2);\n}\n\n.default-handle {\n  background: #6b7280;\n  box-shadow: 0 0 0 2px rgba(107, 114, 128, 0.2);\n}\n\n.branch-handle,\n.default-handle {\n  pointer-events: auto; /* Handle 需要响应鼠标事件 */\n}\n\n.branch-handle:hover,\n.default-handle:hover {\n  width: 14px;\n  height: 14px;\n  right: -9px;\n  box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.3);\n}\n\n.default-handle:hover {\n  box-shadow: 0 0 0 3px rgba(107, 114, 128, 0.3);\n}\n\n/* 连接时的高亮效果 */\n.handle-source.connecting {\n  background: #2563eb;\n  box-shadow: 0 0 0 4px rgba(37, 99, 235, 0.3);\n}\n</style>\n"
  },
  {
    "path": "apps/web-antd/src/packages/workflow-designer/components/nodes/TemplateNode.vue",
    "content": "<script setup lang=\"ts\">\nimport type { NodeProps } from '@vue-flow/core'\nimport NodeShell from './NodeShell.vue'\n\ndefineProps<NodeProps>()\n</script>\n\n<template>\n  <NodeShell :data=\"data\" />\n</template>\n\n\n"
  },
  {
    "path": "apps/web-antd/src/packages/workflow-designer/components/nodes/TestNode.vue",
    "content": "<script setup lang=\"ts\">\nimport { Handle, Position } from '@vue-flow/core'\nimport type { NodeProps } from '@vue-flow/core'\nimport CommonNodeHeader from '../../components/CommonNodeHeader.vue'\nimport SvgIcon from '../../components/SvgIcon.vue'\n\ndefineProps<NodeProps>()\n</script>\n\n<template>\n  <div class=\"flex flex-col w-full\">\n    <Handle type=\"target\" :position=\"Position.Left\" />\n    <Handle type=\"source\" :position=\"Position.Right\" />\n    <CommonNodeHeader :wf-node=\"data\" />\n    <div clas=\"flex-1 flex-col\">\n      <div class=\"content_line flex items-center px-2\">\n        <SvgIcon class=\"mx-1.5\" icon=\"mdi:robot\" />\n        {{ data.nodeConfig?.model_name }}\n      </div>\n    </div>\n  </div>\n</template>\n\n\n"
  },
  {
    "path": "apps/web-antd/src/packages/workflow-designer/components/nodes/TongyiwanxNode.vue",
    "content": "<script setup lang=\"ts\">\nimport type { NodeProps } from '@vue-flow/core'\nimport NodeShell from './NodeShell.vue'\n\ndefineProps<NodeProps>()\n</script>\n\n<template>\n  <NodeShell :data=\"data\" />\n</template>\n\n\n"
  },
  {
    "path": "apps/web-antd/src/packages/workflow-designer/docs/README.md",
    "content": "# Workflow Designer (Standalone)\n\n可移植的流程编排页面与组件集合，已去除鉴权与项目全局依赖，便于在其他项目中以“分包”方式直接复用。\n\n## 用法\n\n1. 在你的项目中引入并注册组件：\n\n```vue\n<template>\n  <WorkflowDesigner\n    :workflow=\"workflow\"\n    :wf-components=\"wfComponents\"\n    @save=\"handleSave\"\n    @run=\"handleRun\"\n  />\n</template>\n\n<script setup lang=\"ts\">\nimport WorkflowDesigner from '@/packages/workflow-designer/StandaloneWorkflowDesigner.vue'\nimport type { Workflow } from '@/packages/workflow-designer/types'\n\nconst workflow = /* 你的工作流数据，符合 Workflow.WorkflowInfo */\nconst wfComponents = /* 可用组件清单，符合 Workflow.WorkflowComponent[] */\n\nfunction handleSave(payload: Workflow.WorkflowInfo) {\n  // 自行持久化\n}\n\nfunction handleRun(payload: { workflow: Workflow.WorkflowInfo }) {\n  // 自行触发运行逻辑\n}\n</script>\n```\n\n2. 必要依赖：\n- vue 3\n- naive-ui（本包 UI 使用）\n- @vue-flow/core 与 @vue-flow/background\n\n3. 可选：若你已有 `SvgIcon` 组件，可替换 `components/SvgIcon.vue` 的实现。\n\n## 说明\n\n- 不包含鉴权、用户判断、接口请求等逻辑；对外通过 emits 暴露 `save`、`run` 事件。\n- 完整样式与交互来自原工作流模块，做了最小化改造以去除外部依赖。\n\n## 扩展指引（新增节点/边、接入后端）\n\n### 新增一个节点（Node）\n1) 最小化接入\n- 在宿主页面（如 `src/views/workflow/index.vue`）把 `{ name, title }` 追加到传入的 `wfComponents` 列表，即可出现在左侧面板并参与拖拽：\n  - 例如：`{ name: 'MyNode', title: '我的节点' }`\n- 默认配置两种方式（有优先级）：\n  - 方式 A（优先级最高）：在 `properties/YourNodeProperty.vue` 中导出\n    - `export function getDefaultNodeConfig(workflow) { return {/* 默认配置 */} }`\n    - 示例：`properties/TestNodeProperty.vue`\n  - 方式 B（集中维护）：在 `properties/defaults.ts` 中配置\n    - `export const propertyDefaultGetters = { yournode: (workflow) => ({ /* 默认配置 */ }) }`\n  - 兜底：若 A/B 都未提供，回退为空对象 `{}`。\n- 在 `components/nodes/` 下创建 `MyNode.vue`（可选）。无需手动注册，系统会自动扫描注册：\n  - 文件名 `MyNode.vue` 会被转换成 `mynode` 键名并注入 `nodeTypes`。\n  - 如果没有提供该文件，会自动回退到通用外观 `NodeShell`。\n\n#### 节点的图标与配色（可选，但强烈建议配置）\n左侧组件面板、节点头部会调用两个方法来渲染图标与配色：\n\n- `getIconByComponentName(name: string)` → 返回图标名称（如 `carbon:http`）\n- `getIconClassByComponentName(name: string)` → 返回 Tailwind/类名，用于给图标着色\n\n这两个方法位于 `packages/workflow-designer/utils/workflow-util.ts`，内部根据“节点名的小写”做 `switch` 映射：\n\n```ts\n// 文件：packages/workflow-designer/utils/workflow-util.ts\nexport function getIconByComponentName(name: string) {\n  switch (name.toLowerCase()) {\n    case 'httprequest': return 'carbon:http'\n    // ... 其他节点\n    default: return '' // 未配置时不显示图标\n  }\n}\n\nexport function getIconClassByComponentName(name: string) {\n  switch (name.toLowerCase()) {\n    case 'httprequest': return 'text-slate-800'\n    // ... 其他节点\n    default: return '' // 未配置时使用默认颜色\n  }\n}\n```\n\n新增节点时如需自定义图标和颜色，请：\n1) 以节点的 `name` 转小写为 `case` 值，分别在上述两个方法中添加一条映射；\n2) 图标使用 `Iconify` 风格的标识（本包默认的 `SvgIcon` 支持）；\n3) 颜色类名可用现有的 Tailwind 色值类（或你项目中可用的类名）。\n\n不配置时不影响功能，只是左侧面板与节点标题处不显示/不着色。\n\n2) 专属属性面板（可选）\n- 在 `packages/workflow-designer/properties/` 下新增 `YourNodeProperty.vue`，只需编辑 `wfNode.nodeConfig` 相关字段。\n- 无需修改任何分支或注册代码：系统会自动扫描 `*NodeProperty.vue` 并按文件名映射到节点类型：\n  - 例如：`HttpRequestNodeProperty.vue` → 键名 `httprequest`，当节点 `name === 'HttpRequest'` 时会自动渲染。\n- 若未提供专属面板，自动回退为 `GenericNodeProperty.vue`，按 `nodeConfig` 动态渲染表单。\n\n3) 连接点（Handle）与连线\n- 连接点应在各自的节点组件中渲染：\n  - `<Handle type=\"target\" :position=\"Position.Left\" />`\n  - `<Handle type=\"source\" :position=\"Position.Right\" />`\n  - 示例可参考 `components/nodes/AnswerNode.vue`。\n\n### 新增一种边（Edge）\n- 在 `components/edges/` 下新增边组件（参考 `SpecialEdge.vue`）。无需手动注册：\n  - 文件名 `MyEdge.vue` 会被转换成 `myedge` 键名并注入 `edgeTypes`。\n- 创建/更新/删除边：\n  - `createNewEdge` / `updateEdgeBySourceHandle` / `deleteEdgesBySourceHandle` 可复用，按需调用。\n\n### 节点中调用后端的规范建议\n- 建议由“各节点的 Property 组件”各自发起所需请求；注意使用自己项目的请求实例发起请求，可以保证权限问题。\n- 请求结果写入 `wfNode.nodeConfig`，与后端字段一一对应，便于在 `@save` 时一次性提交。\n- 鉴权、token 等细节全部由宿主的请求实例处理，子包不关心用户态信息。\n\n### 命名与约定\n- 节点 `name` 使用帕斯卡命名（如 `HttpRequest`），自动注册时将文件 `HttpRequestNode.vue` 转为小写键 `httprequest`。\n- 属性面板文件命名：`<Name>NodeProperty.vue`（如 `HttpRequestNodeProperty.vue`）。\n- `nodeConfig` 字段应尽量与后端模型字段一致；默认值在 `createNewNode` 中集中维护，便于落盘/回显。\n- 不在子包中引入项目自有 Store/权限/路由等全局耦合内容；如需共享状态，请通过 `props / provide/inject` 注入。\n\n### 拖拽面板说明\n- 左侧面板通过宿主传入的 `wfComponents` 渲染，拖拽开始事件集中在 `onPaletteDragStart` 方法内：\n  - 通过 `event.dataTransfer.setData('application/vueflow', component.name)` 传递节点类型。\n  - 放置（drop）逻辑集中在 `onDrop` 方法中，最终由 `createNewNode` 完成数据与画布 UI 的创建。\n\n### 类型扩展\n- 如需新增类型，请在 `types/index.d.ts` 内补充：\n  - `WorkflowComponent` 新类型（必要时）\n  - 对应节点 `nodeConfig` 的 TS 结构（可选，通用面板也支持任意结构）\n\n### 测试与演示\n- 打开页面 `src/views/workflow/index.vue`，此页已集成 `StandaloneWorkflowDesigner`。\n- 在该文件中：\n  - 将你的节点 `{ name, title }` 追加到 `wfComponents` 数组即可出现在左侧拖拽面板。\n  - 在本页的 `workflow.nodes` 中按需加入一条样例节点，便于直接查看渲染与属性面板。\n  - 如有自定义节点/边组件，分别放到 `packages/workflow-designer/components/nodes/*Node.vue` 与 `components/edges/*Edge.vue`，系统会自动扫描注册。\n\n\n"
  },
  {
    "path": "apps/web-antd/src/packages/workflow-designer/docs/WorkflowDesigner树状架构分析.md",
    "content": "# Workflow Designer 树状架构分析\n\n## 1) 包结构树\n\n```text\nworkflow-designer/\n├─ StandaloneWorkflowDesigner.vue        # 画布与主交互容器（核心）\n├─ index.ts                              # 默认导出组件\n├─ ARCHITECTURE.md                       # 架构说明（文字版）\n├─ ARCHITECTURE-TREE.md                  # 架构说明（树状版，本文）\n│\n├─ components/                           # 可视化节点/边与通用组件\n│  ├─ nodes/\n│  │  ├─ AnswerNode.vue\n│  │  ├─ ClassifierNode.vue\n│  │  ├─ Dalle3Node.vue\n│  │  ├─ DocumentExtractorNode.vue\n│  │  ├─ EndNode.vue\n│  │  ├─ FaqExtractorNode.vue\n│  │  ├─ GoogleNode.vue\n│  │  ├─ KeywordExtractorNode.vue\n│  │  ├─ NodeShell.vue                   # 未提供专属节点时的通用外观\n│  │  ├─ StartNode.vue\n│  │  ├─ SwitcherNode.vue\n│  │  ├─ TemplateNode.vue\n│  │  ├─ TestNode.vue\n│  │  └─ TongyiwanxNode.vue\n│  ├─ edges/\n│  │  ├─ CustomEdge.vue\n│  │  ├─ CustomEdge2.vue\n│  │  └─ SpecialEdge.vue\n│  ├─ CommonNodeHeader.vue\n│  ├─ RunDetail.vue\n│  ├─ RuntimeNodes.vue\n│  ├─ SvgIcon.vue\n│  └─ WfVariableSelector.vue\n│\n├─ panels/\n│  └─ RightPanel.vue                     # 右侧属性面板（自动解析专属面板）\n│\n├─ properties/                           # 节点属性面板与默认值\n│  ├─ AnswerNodeProperty.vue\n│  ├─ ClassifierNodeProperty.vue\n│  ├─ KeywordExtractorNodeProperty.vue\n│  ├─ StartNodeProperty.vue\n│  ├─ TestNodeProperty.vue\n│  ├─ GenericNodeProperty.vue            # 通用（动态）属性面板\n│  └─ defaults.ts                        # 默认配置集中映射表\n│\n├─ store/\n│  └─ index.ts                           # Pinia Store（可选集成）\n│\n├─ types/\n│  └─ index.d.ts                         # Workflow/Node/Edge 等类型定义\n│\n└─ utils/\n   └─ workflow-util.ts                   # 节点/边创建、默认值/图标映射、辅助函数\n```\n\n## 2) 核心关系树（模块依赖与数据流）\n\n```text\nStandaloneWorkflowDesigner.vue\n├─ Props\n│  ├─ workflow: WorkflowInfo\n│  ├─ wfComponents: WorkflowComponent[]\n│  ├─ componentIdMap: Record<number, string>\n│  └─ saving?: boolean\n├─ Emits\n│  ├─ save(workflow)\n│  ├─ run({ workflow })\n│  └─ deleteNode(nodeUuid)\n├─ 自动注册\n│  ├─ nodes: import.meta.glob(components/nodes/*Node.vue) → nodeTypes\n│  └─ edges: import.meta.glob(components/edges/*Edge.vue) → edgeTypes\n├─ 组合\n│  └─ RightPanel.vue（属性面板）\n├─ 工具依赖\n│  └─ utils/workflow-util.ts（createNewNode/createNewEdge/...）\n└─ 数据层\n   ├─ 输入：WorkflowInfo（业务层）\n   ├─ 派生：UIWorkflow（画布层）\n   └─ 同步：拖拽/连线/删除/保存 → 写回 WorkflowInfo\n\nRightPanel.vue\n├─ 自动匹配属性组件：properties/<Name>NodeProperty.vue（文件名 → 小写键）\n└─ 回退：GenericNodeProperty.vue\n\nproperties/\n├─ <Name>NodeProperty.vue（专属面板，可导出 getDefaultNodeConfig）\n├─ GenericNodeProperty.vue（通用表单）\n└─ defaults.ts（propertyDefaultGetters：集中默认值表）\n\nutils/workflow-util.ts\n├─ emptyWorkflowInfo / emptyWorkflowNode / createUuid\n├─ createNewNode / createNewEdge / updateEdgeBySourceHandle / deleteEdgesBySourceHandle\n├─ getIconByComponentName / getIconClassByComponentName\n└─ 默认值决策链：专属导出 > defaults.ts > 空对象\n\ntypes/index.d.ts\n├─ WorkflowInfo（nodes/edges/deleteNodes/deleteEdges/...）\n├─ WorkflowNode（wfComponent/workflowComponentId/inputConfig/nodeConfig/...）\n├─ WorkflowEdge（uuid/sourceNodeUuid/targetNodeUuid/...）\n└─ UIWorkflow（画布层 nodes/edges）\n\nstore/index.ts（可选）\n├─ state：工作流列表/运行实例/组件清单/活动态等\n├─ getters：按 uuid/id 查找工作流、起始节点、运行节点\n└─ actions：增删改查、运行态维护、路由跳转注入 setWorkflowDesignerNavigator\n```\n\n## 3) 事件与时序树（关键交互）\n\n```text\n初始化\n└─ onMounted → renderGraph → fitView\n\n拖拽新增节点\n└─ onPaletteDragStart → onDrop → createNewNode → 选中节点 → 显示 RightPanel\n\n连线/改线/删线\n├─ onConnect → createNewEdge（同步 UI 与 WorkflowInfo）\n├─ updateEdgeBySourceHandle（按源 Handle 改线）\n└─ onEdgeClick → 删除边（记录 deleteEdges 去重）\n\n节点交互\n├─ onNodeClick（选择并显示属性面板）\n├─ onNodeDragStop（写回 positionX/Y）\n└─ 删除节点 → 级联删除相关边 → 记录 deleteNodes/deleteEdges\n\n保存/运行\n├─ onSave → 同步画布坐标 → 去重 deleteEdges → emits.save(workflow)\n└─ onRun  → emits.run({ workflow })\n```\n\n## 4) 扩展点树（约定优先）\n\n```text\n新增节点（Node）\n├─ 宿主传入 wfComponents 追加 { name, title }\n├─ 可选：components/nodes/<Name>Node.vue（自动注册），否则回退 NodeShell\n└─ 属性面板：properties/<Name>NodeProperty.vue（可导出 getDefaultNodeConfig）\n\n新增边（Edge）\n└─ components/edges/<Name>Edge.vue（自动注册；通过 edgeTypes 使用）\n\n默认值\n└─ 优先级：专属面板导出 > properties/defaults.ts > {}\n\n图标/颜色\n└─ utils/workflow-util.ts：getIconByComponentName / getIconClassByComponentName\n\n类型扩展\n└─ types/index.d.ts：补充 WorkflowComponent、节点 nodeConfig 类型等\n```\n\n## 5) 对外 API（宿主集成）\n\n- Props：`workflow`、`wfComponents`、`componentIdMap`、`saving`\n- Emits：`save(workflow)`、`run({ workflow })`、`deleteNode(nodeUuid)`\n- Store（可选）：`setWorkflowDesignerNavigator()` 注入路由跳转\n"
  },
  {
    "path": "apps/web-antd/src/packages/workflow-designer/docs/WorkflowDesigner详细树状架构.md",
    "content": "# Workflow Designer 详细树状架构（带注释）\n\n## 📁 包结构树（完整目录）\n\n```text\nworkflow-designer/                                    # 工作流设计器包根目录\n│\n├─ 📄 StandaloneWorkflowDesigner.vue                  # 🎯 核心组件：画布与主交互容器\n│   │   # 功能：拖拽画布、节点连线、属性编辑、保存运行\n│   │   # 依赖：Vue Flow、Naive UI、内部工具函数\n│   │   # 对外：Props(workflow/wfComponents/componentIdMap/saving) + Emits(save/run/deleteNode)\n│   │\n├─ 📄 index.ts                                        # 📤 包入口：默认导出主组件\n│   │   # 导出：StandaloneWorkflowDesigner + 所有类型定义\n│   │\n├─ 📄 README.md                                       # 📖 使用说明与扩展指南\n│   │   # 内容：基本用法、依赖说明、新增节点/边/属性面板的详细步骤\n│   │\n├─ 📄 ARCHITECTURE.md                                 # 🏗️ 架构说明（文字版）\n│   │   # 内容：组件职责、数据流、集成方式、设计取舍\n│   │\n├─ 📄 ARCHITECTURE-TREE.md                           # 🌳 架构说明（树状版）\n│   │   # 内容：包结构、核心关系、事件时序、扩展点\n│   │\n└─ 📄 ARCHITECTURE-DETAILED-TREE.md                  # 🌲 详细树状架构（本文）\n│       # 内容：完整目录结构 + 详细功能注释\n│\n├─ 📁 components/                                     # 🧩 可视化组件集合\n│   │   # 职责：节点渲染、边渲染、通用UI组件\n│   │\n│   ├─ 📁 nodes/                                      # 🔘 节点组件目录\n│   │   │   # 约定：<Name>Node.vue → 自动注册为 name.toLowerCase()\n│   │   │   # 回退：未提供时使用 NodeShell.vue 通用外观\n│   │   │\n│   │   ├─ 📄 AnswerNode.vue                          # 💬 回答节点：AI对话响应\n│   │   │   # 功能：显示AI回答内容，支持流式输出\n│   │   │   # 配置：prompt模板、模型选择、输出格式\n│   │   │\n│   │   ├─ 📄 ClassifierNode.vue                      # 🏷️ 分类节点：智能分类判断\n│   │   │   # 功能：根据输入内容进行多分类判断\n│   │   │   # 配置：分类类别、阈值设置、目标节点映射\n│   │   │\n│   │   ├─ 📄 Dalle3Node.vue                          # 🎨 图像生成节点：DALL-E 3\n│   │   │   # 功能：根据文本描述生成图像\n│   │   │   # 配置：提示词、图像尺寸、质量等级\n│   │   │\n│   │   ├─ 📄 DocumentExtractorNode.vue              # 📄 文档提取节点\n│   │   │   # 功能：从文档中提取结构化信息\n│   │   │   # 配置：提取规则、输出格式、字段映射\n│   │   │\n│   │   ├─ 📄 EndNode.vue                             # 🏁 结束节点：流程终点\n│   │   │   # 功能：标记工作流结束，收集最终结果\n│   │   │   # 配置：结果展示格式、输出变量\n│   │   │\n│   │   ├─ 📄 FaqExtractorNode.vue                    # ❓ FAQ提取节点\n│   │   │   # 功能：从知识库中提取相关问答对\n│   │   │   # 配置：相似度阈值、返回数量、知识库选择\n│   │   │\n│   │   ├─ 📄 GoogleNode.vue                          # 🔍 谷歌搜索节点\n│   │   │   # 功能：执行谷歌搜索并返回结果\n│   │   │   # 配置：搜索关键词、国家地区、语言、结果数量\n│   │   │\n│   │   ├─ 📄 KeywordExtractorNode.vue                # 🔑 关键词提取节点\n│   │   │   # 功能：从文本中提取关键词\n│   │   │   # 配置：提取算法、关键词数量、过滤条件\n│   │   │\n│   │   ├─ 📄 NodeShell.vue                           # 🐚 通用节点外壳（兜底组件）\n│   │   │   # 功能：当未提供专属节点组件时的通用外观\n│   │   │   # 特点：显示节点标题、图标、基本样式\n│   │   │\n│   │   ├─ 📄 StartNode.vue                           # 🚀 开始节点：流程起点\n│   │   │   # 功能：工作流入口，接收初始输入\n│   │   │   # 配置：欢迎语、输入字段定义、变量初始化\n│   │   │\n│   │   ├─ 📄 SwitcherNode.vue                        # 🔀 条件分支节点\n│   │   │   # 功能：根据条件执行不同分支\n│   │   │   # 配置：条件表达式、分支映射、默认分支\n│   │   │\n│   │   ├─ 📄 TemplateNode.vue                        # 📝 模板节点：文本模板处理\n│   │   │   # 功能：使用模板引擎处理文本内容\n│   │   │   # 配置：模板内容、变量替换、输出格式\n│   │   │\n│   │   ├─ 📄 TestNode.vue                            # 🧪 测试节点：调试用\n│   │   │   # 功能：用于测试和调试工作流\n│   │   │   # 配置：测试数据、验证规则、输出格式\n│   │   │\n│   │   └─ 📄 TongyiwanxNode.vue                       # 🎨 通义万相节点：阿里图像生成\n│   │       # 功能：使用通义万相生成图像\n│   │       # 配置：提示词、图像参数、风格设置\n│   │\n│   ├─ 📁 edges/                                      # 🔗 边组件目录\n│   │   │   # 约定：<Name>Edge.vue → 自动注册为 name.toLowerCase()\n│   │   │   # 用途：定义节点间的连接线样式和行为\n│   │   │\n│   │   ├─ 📄 CustomEdge.vue                          # 🎨 自定义边样式1\n│   │   │   # 功能：自定义连接线外观和动画效果\n│   │   │   # 特点：可配置颜色、粗细、动画类型\n│   │   │\n│   │   ├─ 📄 CustomEdge2.vue                         # 🎨 自定义边样式2\n│   │   │   # 功能：另一种自定义连接线样式\n│   │   │   # 特点：不同的视觉效果和交互行为\n│   │   │\n│   │   └─ 📄 SpecialEdge.vue                          # ⭐ 特殊边：默认连接线\n│   │       # 功能：工作流中默认使用的连接线样式\n│   │       # 特点：带动画效果、支持点击删除\n│   │\n│   ├─ 📄 CommonNodeHeader.vue                       # 📋 通用节点头部组件\n│   │   # 功能：统一的节点标题栏样式\n│   │   # 特点：图标、标题、操作按钮的标准化布局\n│   │\n│   ├─ 📄 RunDetail.vue                              # 📊 运行详情组件\n│   │   # 功能：显示工作流运行时的详细信息\n│   │   # 内容：执行状态、节点输出、错误信息、性能指标\n│   │\n│   ├─ 📄 RuntimeNodes.vue                           # ⚡ 运行时节点组件\n│   │   # 功能：实时显示工作流执行过程中的节点状态\n│   │   # 特点：动态更新、状态指示、进度显示\n│   │\n│   ├─ 📄 SvgIcon.vue                                # 🎯 SVG图标组件\n│   │   # 功能：统一的图标渲染组件\n│   │   # 特点：支持Iconify图标库、可自定义颜色和大小\n│   │\n│   └─ 📄 WfVariableSelector.vue                     # 🔧 工作流变量选择器\n│       # 功能：选择和管理工作流中的变量\n│       # 特点：变量类型识别、作用域管理、引用追踪\n│\n├─ 📁 panels/                                        # 🎛️ 面板组件目录\n│   │   # 职责：提供各种功能面板和侧边栏\n│   │\n│   └─ 📄 RightPanel.vue                             # 📋 右侧属性面板\n│       # 功能：显示和编辑选中节点的属性\n│       # 特点：自动匹配专属属性组件、支持节点重命名\n│       # 回退：未找到专属面板时使用通用属性表单\n│\n├─ 📁 properties/                                    # ⚙️ 属性配置目录\n│   │   # 职责：节点属性面板和默认值配置\n│   │   # 约定：<Name>NodeProperty.vue → 自动映射到节点类型\n│   │\n│   ├─ 📄 AnswerNodeProperty.vue                     # 💬 回答节点属性面板\n│   │   # 功能：配置AI回答的参数和选项\n│   │   # 配置项：提示词、模型选择、温度、最大长度等\n│   │\n│   ├─ 📄 ClassifierNodeProperty.vue                 # 🏷️ 分类节点属性面板\n│   │   # 功能：配置分类器的参数和类别\n│   │   # 配置项：分类类别、阈值、目标节点映射等\n│   │\n│   ├─ 📄 KeywordExtractorNodeProperty.vue           # 🔑 关键词提取属性面板\n│   │   # 功能：配置关键词提取的参数\n│   │   # 配置项：提取算法、关键词数量、过滤规则等\n│   │\n│   ├─ 📄 StartNodeProperty.vue                      # 🚀 开始节点属性面板\n│   │   # 功能：配置工作流入口的参数\n│   │   # 配置项：欢迎语、输入字段、变量初始化等\n│   │\n│   ├─ 📄 TestNodeProperty.vue                       # 🧪 测试节点属性面板\n│   │   # 功能：配置测试节点的参数\n│   │   # 配置项：测试数据、验证规则、输出格式等\n│   │\n│   ├─ 📄 GenericNodeProperty.vue                     # 🔧 通用属性面板（兜底）\n│   │   # 功能：当未提供专属属性面板时的通用表单\n│   │   # 特点：动态渲染nodeConfig中的所有字段\n│   │\n│   └─ 📄 defaults.ts                                # 📋 默认配置集中表\n│       # 功能：集中管理所有节点的默认配置\n│       # 结构：propertyDefaultGetters[name] = (workflow) => config\n│       # 优先级：专属面板导出 > 此表 > 空对象\n│\n├─ 📁 store/                                         # 🗄️ 状态管理目录\n│   │   # 职责：Pinia store，可选的状态管理\n│   │   # 用途：工作流列表、运行实例、跨视图状态\n│   │\n│   └─ 📄 index.ts                                   # 🏪 Pinia Store定义\n│       # 功能：管理工作流和运行时的状态\n│       # 状态：工作流列表、运行实例、组件清单、活动状态\n│       # 方法：CRUD操作、运行态维护、路由跳转注入\n│\n├─ 📁 types/                                         # 📝 类型定义目录\n│   │   # 职责：TypeScript类型定义\n│   │   # 内容：工作流、节点、边、UI结构等类型\n│   │\n│   └─ 📄 index.d.ts                                 # 📋 核心类型定义\n│       # 类型：WorkflowInfo、WorkflowNode、WorkflowEdge、WorkflowComponent\n│       # 用途：提供类型安全和IDE智能提示\n│\n└─ 📁 utils/                                         # 🛠️ 工具函数目录\n    │   # 职责：通用工具函数和业务逻辑\n    │   # 内容：节点/边操作、默认值处理、图标映射等\n    │\n    └─ 📄 workflow-util.ts                          # 🔧 工作流工具函数\n        # 功能：节点/边创建、更新、删除、默认值注入\n        # 工具：图标映射、UUID生成、深拷贝、输入类型转换\n        # 核心：createNewNode、createNewEdge、getDefaultNodeConfig\n```\n\n## 🔄 数据流与状态管理树\n\n```text\n数据层架构\n│\n├─ 📊 业务数据层（WorkflowInfo）\n│   ├─ nodes: WorkflowNode[]                         # 节点数据（业务层）\n│   │   ├─ uuid: string                              # 节点唯一标识\n│   │   ├─ wfComponent: WorkflowComponent            # 组件信息\n│   │   ├─ workflowComponentId?: number               # 后端组件ID（兼容）\n│   │   ├─ nodeConfig: Record<string, any>           # 节点配置参数\n│   │   ├─ inputConfig: NodeIOConfig                 # 输入配置\n│   │   ├─ outputConfig: Record<string, any>        # 输出配置\n│   │   └─ positionX/Y: number                       # 节点坐标\n│   │\n│   ├─ edges: WorkflowEdge[]                         # 边数据（业务层）\n│   │   ├─ uuid: string                              # 边唯一标识\n│   │   ├─ sourceNodeUuid: string                    # 源节点ID\n│   │   ├─ targetNodeUuid: string                    # 目标节点ID\n│   │   └─ sourceHandle?: string                     # 源连接点\n│   │\n│   ├─ deleteNodes: string[]                         # 待删除节点ID列表\n│   └─ deleteEdges: string[]                         # 待删除边ID列表\n│\n├─ 🎨 UI渲染层（UIWorkflow）\n│   ├─ nodes: VueFlowNode[]                          # 画布节点（UI层）\n│   │   ├─ id: string                                # 对应业务层uuid\n│   │   ├─ type: string                              # 节点类型（小写）\n│   │   ├─ data: WorkflowNode                        # 业务数据引用\n│   │   └─ position: {x, y}                          # 画布坐标\n│   │\n│   └─ edges: VueFlowEdge[]                          # 画布边（UI层）\n│       ├─ id: string                                # 对应业务层uuid\n│       ├─ source: string                            # 源节点ID\n│       ├─ target: string                            # 目标节点ID\n│       ├─ type: string                              # 边类型\n│       └─ data: WorkflowEdge                       # 业务数据引用\n│\n└─ 🔄 同步机制\n    ├─ 坐标同步：UI层 → 业务层（拖拽结束时）\n    ├─ 数据同步：业务层 → UI层（workflow变化时）\n    ├─ 删除同步：UI操作 → 业务层deleteNodes/deleteEdges\n    └─ 保存同步：业务层 → 宿主应用（emit save事件）\n```\n\n## 🎯 组件注册与映射树\n\n```text\n自动注册机制\n│\n├─ 🔘 节点组件注册\n│   ├─ 扫描路径：components/nodes/*Node.vue\n│   ├─ 注册方式：import.meta.glob('./components/nodes/*Node.vue', { eager: true })\n│   ├─ 键名转换：<Name>Node.vue → name.toLowerCase()\n│   │   ├─ AnswerNode.vue → 'answer'\n│   │   ├─ ClassifierNode.vue → 'classifier'\n│   │   ├─ StartNode.vue → 'start'\n│   │   └─ ...（其他节点）\n│   │\n│   ├─ 回退机制：未找到专属组件 → NodeShell.vue\n│   └─ 使用方式：nodeTypes[componentName.toLowerCase()]\n│\n├─ 🔗 边组件注册\n│   ├─ 扫描路径：components/edges/*Edge.vue\n│   ├─ 注册方式：import.meta.glob('./components/edges/*Edge.vue', { eager: true })\n│   ├─ 键名转换：<Name>Edge.vue → name.toLowerCase()\n│   │   ├─ CustomEdge.vue → 'custom'\n│   │   ├─ CustomEdge2.vue → 'custom2'\n│   │   └─ SpecialEdge.vue → 'special'\n│   │\n│   └─ 使用方式：edgeTypes[edgeType]（创建边时指定type）\n│\n└─ ⚙️ 属性面板注册\n    ├─ 扫描路径：properties/*NodeProperty.vue\n    ├─ 注册方式：import.meta.glob('../properties/*NodeProperty.vue', { eager: true })\n    ├─ 键名转换：<Name>NodeProperty.vue → name.toLowerCase()\n    │   ├─ AnswerNodeProperty.vue → 'answer'\n    │   ├─ ClassifierNodeProperty.vue → 'classifier'\n    │   └─ ...（其他属性面板）\n    │\n    ├─ 回退机制：未找到专属面板 → GenericNodeProperty.vue\n    └─ 使用方式：propertyMap[nodeType] || GenericNodeProperty\n```\n\n## 🔧 默认值配置树\n\n```text\n默认值注入机制（三级优先级）\n│\n├─ 🥇 第一优先级：专属属性面板导出\n│   ├─ 位置：properties/<Name>NodeProperty.vue\n│   ├─ 导出：export function getDefaultNodeConfig(workflow: WorkflowInfo)\n│   ├─ 示例：AnswerNodeProperty.vue 中导出回答节点的默认配置\n│   └─ 优势：节点专属、可访问workflow上下文\n│\n├─ 🥈 第二优先级：集中配置表\n│   ├─ 位置：properties/defaults.ts\n│   ├─ 结构：propertyDefaultGetters[name] = (workflow) => config\n│   ├─ 示例：\n│   │   ├─ start: (wf) => ({ prologue: '欢迎使用工作流' })\n│   │   ├─ answer: (wf) => ({ prompt: '', model_name: 'gpt-3.5-turbo' })\n│   │   └─ classifier: (wf) => ({ categories: [] })\n│   │\n│   └─ 优势：集中管理、易于维护\n│\n├─ 🥉 第三优先级：内置默认表（已注释，不建议使用）\n│   ├─ 位置：utils/workflow-util.ts（注释部分）\n│   ├─ 结构：simpleDefaults[name] = config\n│   └─ 状态：已注释，保留作为参考\n│\n└─ 🔄 兜底机制：空对象 {}\n    ├─ 触发：所有优先级都未提供配置时\n    ├─ 结果：nodeConfig = {}\n    └─ 影响：仍可正常渲染和保存，只是没有默认值\n```\n\n## 🎨 图标与样式映射树\n\n```text\n视觉映射系统\n│\n├─ 🎯 图标映射（getIconByComponentName）\n│   ├─ answer → 'carbon:question-answering'          # 问答图标\n│   ├─ classifier → 'carbon:type-pattern'            # 分类图标\n│   ├─ start → 'carbon:play-outline'                 # 开始图标\n│   ├─ end → 'carbon:closed-caption'                 # 结束图标\n│   ├─ httprequest → 'carbon:http'                   # HTTP请求图标\n│   ├─ dalle3 → 'solar:pallete-2-linear'             # 图像生成图标\n│   └─ ...（其他节点图标）\n│\n├─ 🎨 颜色映射（getIconClassByComponentName）\n│   ├─ answer → 'text-green-800'                     # 绿色\n│   ├─ classifier → 'text-violet-900'                # 紫色\n│   ├─ start → 'text-blue-900'                       # 蓝色\n│   ├─ end → 'text-orange-800'                       # 橙色\n│   ├─ httprequest → 'text-slate-800'                # 灰色\n│   ├─ dalle3 → 'text-fuchsia-700'                   # 紫红色\n│   └─ ...（其他节点颜色）\n│\n└─ 🔧 扩展方式\n    ├─ 新增节点时在工具函数中添加映射\n    ├─ 图标使用Iconify格式：'collection:icon-name'\n    ├─ 颜色使用Tailwind类名：'text-color-shade'\n    └─ 未配置时显示默认样式\n```\n\n## 🚀 对外集成API树\n\n```text\n宿主应用集成接口\n│\n├─ 📥 Props（输入）\n│   ├─ workflow: WorkflowInfo                        # 工作流数据\n│   │   ├─ 来源：后端API或新建\n│   │   ├─ 内容：nodes、edges、deleteNodes、deleteEdges\n│   │   └─ 更新：通过emit save事件持久化\n│   │\n│   ├─ wfComponents: WorkflowComponent[]             # 可用组件清单\n│   │   ├─ 用途：决定左侧面板显示内容\n│   │   ├─ 结构：{ name: string, title: string, remark?: string }\n│   │   └─ 影响：自动注册节点类型\n│   │\n│   ├─ componentIdMap: Record<number, string>        # 组件ID映射\n│   │   ├─ 用途：后端只返回workflowComponentId时的兼容\n│   │   ├─ 结构：{ 1: 'Start', 2: 'Answer', ... }\n│   │   └─ 回退：未映射时显示'Unknown'\n│   │\n│   └─ saving?: boolean                              # 保存状态\n│       ├─ 用途：控制保存按钮loading/禁用状态\n│       ├─ 默认：false\n│       └─ 建议：宿主在保存过程中设为true\n│\n├─ 📤 Emits（输出）\n│   ├─ save(workflow: WorkflowInfo)                  # 保存事件\n│   │   ├─ 触发：用户点击保存按钮\n│   │   ├─ 数据：完整的workflow对象（含坐标同步）\n│   │   └─ 职责：宿主负责持久化到后端\n│   │\n│   ├─ run(payload: { workflow: WorkflowInfo })     # 运行事件\n│   │   ├─ 触发：用户点击运行按钮\n│   │   ├─ 数据：包含workflow的运行参数\n│   │   └─ 职责：宿主负责触发工作流执行\n│   │\n│   └─ deleteNode(nodeUuid: string)                   # 节点删除事件\n│       ├─ 触发：节点被删除时\n│       ├─ 数据：被删除节点的UUID\n│       └─ 用途：宿主可监听进行额外处理\n│\n└─ 🗄️ Store（可选）\n    ├─ setWorkflowDesignerNavigator(fn)               # 路由注入\n    │   ├─ 用途：避免直接耦合宿主路由\n    │   ├─ 参数：路由跳转函数\n    │   └─ 使用：store内部调用进行页面跳转\n    │\n    └─ useWfStore()                                   # Pinia Store\n        ├─ 状态：工作流列表、运行实例、组件清单\n        ├─ 方法：CRUD操作、运行态维护\n        └─ 注意：主组件不强制依赖，宿主可选使用\n```\n\n## 🔄 关键交互时序树\n\n```text\n用户操作流程\n│\n├─ 🚀 初始化流程\n│   ├─ 1. 宿主传入workflow和wfComponents\n│   ├─ 2. 组件onMounted触发\n│   ├─ 3. 自动扫描nodes/edges组件并注册\n│   ├─ 4. 调用renderGraph()转换数据\n│   ├─ 5. 校验节点/边有效性\n│   ├─ 6. 写入uiWorkflow并渲染画布\n│   └─ 7. 调用fitView()适应画布\n│\n├─ 🎯 拖拽新增节点流程\n│   ├─ 1. 用户从左侧面板拖拽组件\n│   ├─ 2. onPaletteDragStart设置拖拽数据\n│   ├─ 3. 拖拽到画布触发onDrop\n│   ├─ 4. 计算画布坐标位置\n│   ├─ 5. 调用createNewNode创建节点\n│   │   ├─ 生成UUID和默认配置\n│   │   ├─ 推入workflow.nodes\n│   │   └─ 推入uiWorkflow.nodes\n│   ├─ 6. 自动选中新节点\n│   └─ 7. 显示RightPanel属性面板\n│\n├─ 🔗 连线操作流程\n│   ├─ 1. 用户拖拽节点连接点\n│   ├─ 2. Vue Flow触发onConnect事件\n│   ├─ 3. 调用createNewEdge创建边\n│   │   ├─ 生成边UUID\n│   │   ├─ 推入workflow.edges\n│   │   └─ 推入uiWorkflow.edges\n│   └─ 4. 画布显示连接线\n│\n├─ ⚙️ 属性编辑流程\n│   ├─ 1. 用户点击节点选中\n│   ├─ 2. onNodeClick触发\n│   ├─ 3. 设置selectedWfNode\n│   ├─ 4. 显示RightPanel\n│   ├─ 5. 自动匹配专属属性组件\n│   │   ├─ 查找properties/<Name>NodeProperty.vue\n│   │   └─ 未找到则使用GenericNodeProperty\n│   ├─ 6. 用户编辑nodeConfig\n│   └─ 7. 实时更新workflow.nodes中的数据\n│\n├─ 💾 保存流程\n│   ├─ 1. 用户点击保存按钮\n│   ├─ 2. onSave触发\n│   ├─ 3. 同步画布坐标到workflow.nodes\n│   ├─ 4. 去重deleteEdges数组\n│   ├─ 5. emit('save', workflow)\n│   └─ 6. 宿主接收并持久化到后端\n│\n└─ 🗑️ 删除操作流程\n    ├─ 删除节点：\n    │   ├─ 1. 调用onDeleteNode\n    │   ├─ 2. 从workflow.nodes中移除\n    │   ├─ 3. 从uiWorkflow.nodes中移除\n    │   ├─ 4. 级联删除相关边\n    │   ├─ 5. 记录到deleteEdges\n    │   └─ 6. emit('deleteNode', nodeUuid)\n    │\n    └─ 删除边：\n        ├─ 1. 用户点击边\n        ├─ 2. onEdgeClick触发\n        ├─ 3. 从workflow.edges中移除\n        ├─ 4. 从uiWorkflow.edges中移除\n        └─ 5. 记录到deleteEdges\n```\n\n## 🎯 扩展开发指南树\n\n```text\n扩展开发步骤\n│\n├─ 🔘 新增节点类型\n│   ├─ 1. 在wfComponents中添加组件定义\n│   │   └─ { name: 'MyNode', title: '我的节点' }\n│   │\n│   ├─ 2. 可选：创建专属节点组件\n│   │   ├─ 文件：components/nodes/MyNode.vue\n│   │   ├─ 内容：节点外观、Handle连接点\n│   │   └─ 回退：未提供则使用NodeShell\n│   │\n│   ├─ 3. 可选：创建专属属性面板\n│   │   ├─ 文件：properties/MyNodeProperty.vue\n│   │   ├─ 内容：编辑wfNode.nodeConfig\n│   │   └─ 回退：未提供则使用GenericNodeProperty\n│   │\n│   ├─ 4. 可选：提供默认配置\n│   │   ├─ 方式A：在属性面板中导出getDefaultNodeConfig\n│   │   ├─ 方式B：在defaults.ts中添加映射\n│   │   └─ 回退：空对象{}\n│   │\n│   └─ 5. 可选：添加图标和颜色映射\n│       ├─ 在getIconByComponentName中添加case\n│       └─ 在getIconClassByComponentName中添加case\n│\n├─ 🔗 新增边类型\n│   ├─ 1. 创建边组件文件\n│   │   └─ components/edges/MyEdge.vue\n│   │\n│   ├─ 2. 实现边的外观和交互\n│   │   ├─ 样式：颜色、粗细、动画\n│   │   └─ 交互：点击、悬停效果\n│   │\n│   └─ 3. 在创建边时指定type\n│       └─ createNewEdge({ ..., type: 'myedge' })\n│\n└─ 📝 类型扩展\n    ├─ 1. 在types/index.d.ts中扩展类型\n    │   ├─ 新增WorkflowComponent类型\n    │   └─ 定义节点nodeConfig的TypeScript接口\n    │\n    └─ 2. 提供类型安全的开发体验\n        ├─ IDE智能提示\n        ├─ 编译时类型检查\n        └─ 重构时的类型安全\n```\n\n## 🏗️ 架构设计原则树\n\n```text\n设计原则与取舍\n│\n├─ 🎯 核心原则\n│   ├─ 最小耦合：不包含鉴权、路由、接口等全局逻辑\n│   ├─ 约定优先：通过文件命名约定自动注册组件\n│   ├─ 分层设计：业务层与UI层分离，提高可控性\n│   └─ 扩展友好：新增节点/边/面板成本最低\n│\n├─ 🔄 数据流设计\n│   ├─ 单向数据流：workflow → uiWorkflow → 用户操作 → workflow\n│   ├─ 状态同步：UI层变化实时写回业务层\n│   ├─ 删除标记：通过deleteNodes/deleteEdges标记删除\n│   └─ 坐标管理：以画布为准，保存时同步到业务层\n│\n├─ 🧩 组件设计\n│   ├─ 自动注册：扫描文件系统，按约定注册组件\n│   ├─ 回退机制：未提供专属组件时使用通用组件\n│   ├─ 组合模式：主组件组合各种子组件\n│   └─ 职责单一：每个组件只负责特定功能\n│\n├─ 🔧 工具函数设计\n│   ├─ 纯函数：工具函数无副作用，易于测试\n│   ├─ 默认值链：三级优先级的默认值注入\n│   ├─ 类型安全：提供完整的TypeScript类型定义\n│   └─ 错误处理：优雅处理异常情况\n│\n└─ 🎨 用户体验设计\n    ├─ 即时反馈：操作立即反映在UI上\n    ├─ 状态保持：页面刷新后状态不丢失\n    ├─ 错误恢复：异常情况下能正常回退\n    └─ 性能优化：避免不必要的重渲染\n```\n"
  },
  {
    "path": "apps/web-antd/src/packages/workflow-designer/docs/图标说明.md",
    "content": "# 工作流设计器图标说明\n\n## 📍 图标来源\n\n工作流设计器中的图标主要来自两个地方：\n\n### 1. **组件库和节点图标** 🎨\n\n这些图标用于：\n- 左侧组件库面板中的组件图标\n- 画布中节点头部的图标\n\n**图标配置位置：** `apps/web-antd/src/packages/workflow-designer/utils/workflow-util.ts`\n\n#### 图标名称配置函数：`getIconByComponentName(name: string)`\n\n```typescript\nexport function getIconByComponentName(name: string) {\n  switch (name.toLowerCase()) {\n    case 'answer': return 'carbon:question-answering'\n    case 'classifier': return 'carbon:type-pattern'\n    case 'knowledgeretrieval': return 'carbon:connect-target'\n    case 'documentextractor': return 'carbon:ibm-knowledge-catalog-standard'\n    case 'keywordextractor': return 'carbon:api-key'\n    case 'faqextractor': return 'fluent-mdl2:book-answers'\n    case 'switcher': return 'oui:logstash-if'\n    case 'template': return 'carbon:prompt-template'\n    case 'dalle3': return 'solar:pallete-2-linear'\n    case 'tongyiwanx': return 'solar:pallete-2-linear'\n    case 'google': return 'ri:google-line'\n    case 'humanfeedback': return 'covid:transmission-virus-human-transmit-2'\n    case 'mailsend': return 'carbon:mail-all'\n    case 'httprequest': return 'carbon:http'\n    case 'end': return 'carbon:closed-caption'\n    case 'start': return 'carbon:play-outline'\n    default: return ''\n  }\n}\n```\n\n#### 图标颜色配置函数：`getIconClassByComponentName(name: string)`\n\n```typescript\nexport function getIconClassByComponentName(name: string) {\n  switch (name.toLowerCase()) {\n    case 'answer': return 'text-green-800'\n    case 'classifier': return 'text-violet-900'\n    case 'knowledgeretrieval': return 'text-stone-900'\n    case 'documentextractor': return 'text-rose-900'\n    case 'keywordextractor': return 'text-cyan-900'\n    case 'faqextractor': return 'text-teal-600'\n    case 'switcher': return 'text-yellow-900'\n    case 'template': return 'text-sky-800'\n    case 'dalle3': return 'text-fuchsia-700'\n    case 'tongyiwanx': return 'text-fuchsia-700'\n    case 'google': return 'text-emerald-900'\n    case 'humanfeedback': return 'text-zinc-800'\n    case 'mailsend': return 'text-amber-800'\n    case 'httprequest': return 'text-slate-800'\n    case 'end': return 'text-orange-800'\n    case 'start': return 'text-blue-900'\n    default: return ''\n  }\n}\n```\n\n**使用示例：**\n```vue\n<SvgIcon \n  :icon=\"getIconByComponentName(component.name)\" \n  :class=\"getIconClassByComponentName(component.name)\" \n/>\n```\n\n### 2. **UI交互图标** 🎯\n\n这些图标用于界面交互元素，来自 `@vben/icons` 套件：\n\n#### 常用图标列表：\n\n| 功能 | 图标名称 | 使用位置 |\n|------|---------|---------|\n| 删除 | `ri:delete-bin-line` | 节点删除菜单 |\n| 更多操作 | `ri:more-fill` | 节点头部三点菜单 |\n| 左箭头 | `ri:arrow-left-s-line` | 收起组件库 |\n| 右箭头 | `ri:arrow-right-s-line` | 展开组件库 |\n| 加号 | `Plus` | 新建工作流 |\n| 编辑 | `UserRoundPen` | 编辑工作流 |\n| 关闭 | `X` | 删除工作流 |\n| 锁定 | `LockKeyhole` | 未发布状态 |\n| 外链 | `ExternalLink` | 已发布状态 |\n| 左箭头 | `ChevronLeft` | 收起侧边栏 |\n| 右箭头 | `ChevronRight` | 展开侧边栏 |\n\n**使用示例：**\n```vue\n// 方式1：直接使用@vben/icons组件\nimport { Plus } from '@vben/icons'\n<Plus class=\"text-base\" />\n\n// 方式2：通过SvgIcon组件\n<SvgIcon icon=\"ri:delete-bin-line\" />\n```\n\n## 🔧 如何添加新的组件图标\n\n当你添加新的工作流组件时，需要配置相应的图标：\n\n### 步骤1：选择图标\n\n访问 [Iconify](https://icon-sets.iconify.design/) 查找合适的图标：\n- **Carbon Design System**: `carbon:xxx` - IBM设计系统，风格统一\n- **Remix Icon**: `ri:xxx` - 简洁现代\n- **Solar Icons**: `solar:xxx` - 设计精美\n- **Fluent MDL2**: `fluent-mdl2:xxx` - 微软设计语言\n\n### 步骤2：添加图标配置\n\n编辑 `utils/workflow-util.ts`，在两个函数中添加配置：\n\n```typescript\n// 1. 添加图标名称\nexport function getIconByComponentName(name: string) {\n  switch (name.toLowerCase()) {\n    // ... 其他配置\n    case 'mynewnode': return 'carbon:my-icon'  // 👈 添加这行\n    default: return ''\n  }\n}\n\n// 2. 添加图标颜色（使用Tailwind CSS颜色类）\nexport function getIconClassByComponentName(name: string) {\n  switch (name.toLowerCase()) {\n    // ... 其他配置\n    case 'mynewnode': return 'text-blue-600'  // 👈 添加这行\n    default: return ''\n  }\n}\n```\n\n### 步骤3：可选颜色方案\n\n推荐使用的Tailwind颜色类：\n- `text-blue-900` - 深蓝色（适合主要功能）\n- `text-green-800` - 深绿色（适合AI/回答类）\n- `text-violet-900` - 紫色（适合分类/判断）\n- `text-rose-900` - 玫瑰红（适合提取/处理）\n- `text-cyan-900` - 青色（适合数据相关）\n- `text-amber-800` - 琥珀色（适合通知/邮件）\n- `text-slate-800` - 灰色（适合通用功能）\n\n## 📦 图标组件\n\n### SvgIcon 组件\n\n**位置：** `apps/web-antd/src/packages/workflow-designer/components/SvgIcon.vue`\n\n**实现：**\n```vue\n<script setup lang=\"ts\">\nimport { VbenIcon } from '@vben/icons'\n\ninterface Props { icon: string }\ndefineProps<Props>()\n</script>\n\n<template>\n  <VbenIcon :icon=\"icon\" />\n</template>\n```\n\n### VbenIcon\n\n底层使用 `@vben/icons` 包中的 `VbenIcon` 组件，支持：\n- ✅ Iconify 所有图标集\n- ✅ 按需加载\n- ✅ TypeScript 支持\n- ✅ 自动颜色和尺寸继承\n\n## 🎨 样式定制\n\n图标样式可以通过以下方式定制：\n\n```vue\n<!-- 大小 -->\n<SvgIcon icon=\"ri:xxx\" class=\"text-xl\" />      <!-- 20px -->\n<SvgIcon icon=\"ri:xxx\" class=\"text-2xl\" />     <!-- 24px -->\n<SvgIcon icon=\"ri:xxx\" class=\"text-base\" />    <!-- 16px -->\n\n<!-- 颜色 -->\n<SvgIcon icon=\"ri:xxx\" class=\"text-blue-500\" />\n<SvgIcon icon=\"ri:xxx\" class=\"text-red-600\" />\n\n<!-- Hover效果 -->\n<SvgIcon icon=\"ri:xxx\" class=\"hover:text-blue-500 transition-colors\" />\n\n<!-- 动画 -->\n<SvgIcon icon=\"ri:xxx\" class=\"transform hover:scale-110 transition-transform\" />\n```\n\n## 📝 最佳实践\n\n1. **保持风格统一**：优先选择同一图标集（如Carbon Design）\n2. **语义化命名**：图标应该直观表达组件功能\n3. **颜色区分**：不同类型的组件使用不同颜色\n4. **尺寸适配**：组件库用20px，节点头部用24px\n5. **性能优化**：图标按需加载，不会影响初始加载速度\n\n## 🔍 调试技巧\n\n如果图标不显示：\n1. 检查图标名称是否正确（区分大小写）\n2. 确认图标在 Iconify 中存在\n3. 检查 `getIconByComponentName` 函数是否返回正确值\n4. 查看浏览器控制台是否有加载错误\n\n## 📚 参考资源\n\n- [Iconify 图标搜索](https://icon-sets.iconify.design/)\n- [Carbon Design Icons](https://carbondesignsystem.com/guidelines/icons/library/)\n- [Remix Icon](https://remixicon.com/)\n- [Tailwind CSS Colors](https://tailwindcss.com/docs/customizing-colors)\n- [@vben/icons 文档](https://doc.vben.pro/guide/essentials/icons.html)\n\n"
  },
  {
    "path": "apps/web-antd/src/packages/workflow-designer/docs/流程编排架构说明.md",
    "content": "### Workflow Designer（Standalone）架构说明\n\n本包提供一个与业务最小耦合的可视化流程编排设计器，面向宿主应用以“分包”方式复用。核心目标：\n- 只负责前端可视化编排（画布、节点、连线、表单属性），不包含鉴权/路由/接口等全局逻辑\n- 通过 props/emits 对外暴露最小 API，宿主负责数据持久化与运行触发\n- 通过“约定优于配置”的自动扫描注册，降低扩展成本（节点、边、属性面板、默认值）\n\n\n## 依赖与技术栈\n- Vue 3 + `<script setup>`\n- Naive UI（侧栏、按钮、输入等）\n- Vue Flow（画布、节点与边渲染、拖拽、连线）\n- Pinia（可选：内部 store 提供运行时查看与跨视图状态）\n\n\n## 目录结构（关键）\n- `StandaloneWorkflowDesigner.vue`：核心画布与主交互容器\n- `components/`\n  - `nodes/`：具体节点组件（如 `AnswerNode.vue`、`StartNode.vue`）；`NodeShell.vue` 为通用外观兜底\n  - `edges/`：边组件（如 `SpecialEdge.vue`）\n  - 其他通用组件：`CommonNodeHeader.vue`、`SvgIcon.vue`、`RunDetail.vue`、`RuntimeNodes.vue` 等\n- `panels/RightPanel.vue`：右侧属性面板，自动解析专属属性组件或回退为通用面板\n- `properties/`：\n  - `*NodeProperty.vue`：各节点专属属性面板（文件名约定映射）\n  - `GenericNodeProperty.vue`：通用（动态）属性表单\n  - `defaults.ts`：节点默认配置集中表（函数映射）\n- `utils/workflow-util.ts`：数据模型构造、节点/边创建与更新、默认值/图标映射等工具\n- `types/index.d.ts`：工作流、节点、边、UI 结构等类型定义\n- `store/index.ts`：Pinia store（工作流/运行时列表与详情的辅助状态管理，路由跳转注入）\n- `index.ts`：对外默认导出 `StandaloneWorkflowDesigner`\n\n\n## 核心组件与职责\n\n### StandaloneWorkflowDesigner.vue（主容器）\n- 对外 Props：\n  - `workflow: WorkflowInfo`：当前工作流（含 `nodes`、`edges` 等）\n  - `wfComponents: WorkflowComponent[]`：可用组件清单（决定左侧面板与自动注册键）\n  - `componentIdMap: Record<number, string>`：将后端 `workflowComponentId` 映射为组件名（容错兼容）\n  - `saving?: boolean`：保存按钮 loading/禁用态\n- 对外 Emits：\n  - `save(workflow)`：保存（宿主负责持久化）\n  - `run({ workflow })`：运行（宿主负责触发执行）\n  - `deleteNode(nodeUuid)`：节点删除回调（便于宿主二次处理）\n- 自动注册：\n  - 通过 `import.meta.glob('./components/nodes/*Node.vue', { eager: true })` 扫描节点文件\n  - 键名约定：`<Name>Node.vue` → `name.toLowerCase()`；若未提供具体节点组件，回退至 `NodeShell`\n  - 类似方式扫描 `components/edges/*Edge.vue` 注入 `edgeTypes`\n- 渲染流程：\n  - `renderGraph()` 将 `workflow.nodes/edges` 解析为画布 `uiWorkflow.nodes/edges`\n  - 校验节点/边有效性（缺字段/孤立边跳过），统一写入 UI 层数据\n  - 支持根据 `workflowComponentId` 用 `componentIdMap` 反推组件名，兼容后端不同返回形态\n  - `watch(workflow, { deep: true })` 变更时重算渲染\n  - 初次 `onMounted` 渲染并 `fitView()`\n- 交互与同步：\n  - 拖拽组件到画布：`onPaletteDragStart` → `onDrop` → `createNewNode`\n  - 连线：`onConnect` → `createNewEdge`\n  - 选中节点：`onNodeClick` → 显示 `RightPanel`\n  - 拖拽节点结束：写回 `positionX/Y`\n  - 删除边：点击边触发删除，并维护 `deleteEdges`（用于后端同步删除）\n  - 保存：`onSave()` 先将画布坐标写回，再对 `deleteEdges` 去重，最后 `emit('save')`\n\n### RightPanel.vue（右侧属性面板）\n- 自动扫描 `properties/*NodeProperty.vue`：文件名 `<Name>NodeProperty.vue` → 小写 `name` 作为键\n- 根据当前选中节点的 `wfComponent.name` 映射到专属属性组件；若无则回退 `GenericNodeProperty`\n- 标题区支持重命名节点标题，图标/色彩通过工具函数映射\n\n### properties（属性配置体系）\n- 专属属性面板：`<Name>NodeProperty.vue`，仅需关心并修改 `wfNode.nodeConfig`\n- 默认值注入（三级优先级）：\n  1) 专属属性面板中可导出 `getDefaultNodeConfig(workflow)`（最高优先级）\n  2) `properties/defaults.ts` 中集中表 `propertyDefaultGetters`\n  3) `utils/workflow-util.ts` 里保留了“内置简易默认表”的注释示例（不建议使用）\n- 未提供时回退空对象 `{}`，仍可渲染并保存\n\n### utils/workflow-util.ts\n- 基本构造：`emptyWorkflowInfo()`、`emptyWorkflowNode()`、`createUuid()`\n- 节点/边操作：\n  - `createNewNode(workflow, uiWorkflow, component, position)`：\n    - 初始化 `WorkflowNode`，按默认规则注入 `nodeConfig` 并推入 UI 层\n  - `createNewEdge({ workflow, uiWorkflow, source, sourceHandle, target })`：\n    - 创建逻辑边与 UI 边（含 `type: 'special'`、`animated: true`）\n  - `updateEdgeBySourceHandle(...)` / `deleteEdgesBySourceHandle(...)`：更新/删除边并维护 UI 与 `deleteEdges`\n- 图标与样式：`getIconByComponentName`、`getIconClassByComponentName`\n- 其他：`deepClone`（优先 `structuredClone`，降级 JSON）\n\n### types/index.d.ts\n- 核心类型：\n  - `WorkflowInfo`：包含 `nodes`、`edges`、`deleteNodes`/`deleteEdges` 等\n  - `WorkflowNode`：`wfComponent | workflowComponentId` 二选一/并存，`inputConfig`/`nodeConfig`/`outputConfig`，`positionX/Y`\n  - `WorkflowEdge`：`uuid`、`sourceNodeUuid`、`targetNodeUuid`、可选 `sourceHandle`\n  - `WorkflowComponent`：可用组件清单项（左侧面板来源）\n  - `UIWorkflow`：画布层节点/边（与业务层分离）\n\n### store/index.ts（可选集成）\n- Pinia store，用于：\n  - 维护“我的/公共”工作流列表、当前激活工作流、组件清单、运行实例列表等\n  - 提供根据 `uuid/id` 获取工作流、起始节点、运行时节点等的 getters\n  - 提供新增/更新/删除工作流、维护运行实例、向运行节点追加输入/输出等 actions\n  - 通过 `setWorkflowDesignerNavigator` 注入导航方法，避免直接耦合宿主路由\n- 注意：主组件并不强依赖此 store；宿主可自由选择是否使用\n\n\n## 数据流与状态\n- 双层数据模型：\n  - 业务/持久化层：`workflow`（由宿主传入与保存）\n  - 画布/UI 层：`uiWorkflow`（仅用于渲染，提高稳定性与可控性）\n- 同步策略：\n  - 坐标以画布为准写回 `workflow.nodes`\n  - 监听节点选择、拖拽、连线等事件，实时维护两层数据一致\n  - 删除边/节点时维护 `workflow.deleteEdges/deleteNodes`，便于后端同步删除\n\n\n## 与宿主应用的集成方式\n- 传入：\n  - `workflow`：后端读取或新建的流程数据\n  - `wfComponents`：允许拖拽的组件清单（决定左侧面板 + 节点类型注册）\n  - `componentIdMap`：当仅有 `workflowComponentId` 时映射到组件名\n  - `saving`：保存按钮 Loading/禁用态\n- 事件：\n  - `save(workflow)`：宿主自行持久化（新增/更新/删除节点与边）\n  - `run({ workflow })`：宿主自行触发运行\n  - `deleteNode(uuid)`：可选监听，用于联动其他 UI\n\n\n## 扩展点与约定\n- 新增节点：\n  - 在宿主传入 `wfComponents` 中追加 `{ name, title }` 即可出现在左侧\n  - 可在 `components/nodes/` 新建 `<Name>Node.vue`，自动注册；未提供则回退 `NodeShell`\n  - 属性面板：在 `properties/` 新建 `<Name>NodeProperty.vue` 或在 `defaults.ts` 提供默认值\n- 新增边：在 `components/edges/` 新建 `<Name>Edge.vue`，自动注册并可通过 `type` 指定使用\n- 图标与样式：在 `getIconByComponentName` 与 `getIconClassByComponentName` 中添加映射\n- 类型扩展：在 `types/index.d.ts` 中扩展 `WorkflowComponent`、各节点 `nodeConfig` 类型\n- 命名约定：\n  - 节点组件：`<Name>Node.vue`；属性面板：`<Name>NodeProperty.vue`\n  - 自动注册键：文件名去后缀转小写，需与 `wfComponents[i].name.toLowerCase()` 对齐\n\n\n## 关键交互时序（简化）\n1) 宿主传入 `workflow` 与 `wfComponents` → 主组件 `onMounted` 扫描节点/边并 `renderGraph()`\n2) `renderGraph()` 校验与转换 `workflow.*` → 写入 `uiWorkflow.*` → `fitView()`\n3) 拖拽左侧组件到画布：`onDrop()` → `createNewNode()` → 选中并显示 `RightPanel`\n4) 连线：`onConnect()` → `createNewEdge()`；点击边可删除并维护 `deleteEdges`\n5) 右侧属性面板：自动匹配 `<Name>NodeProperty.vue`，否则使用 `GenericNodeProperty`\n6) 保存：`onSave()` 将坐标同步回 `workflow`、对 `deleteEdges` 去重 → `emit('save')`\n\n\n## 运行态（可选能力）\n- 组件：`RunDetail.vue`、`RuntimeNodes.vue`\n- store 动作：记录/更新运行实例、为运行节点累积输入/输出、状态更新等\n- 用途：在详情页中展示某次运行的节点轨迹与产出；不影响设计器核心功能\n\n\n## 设计取舍与注意事项\n- 划分 UI 层与业务层数据，避免直接在画布对象上做复杂引用变更\n- 自动扫描 + 约定命名，降低新增节点/边/面板的接入复杂度\n- 仅保留必要 UI 依赖；鉴权/路由/请求由宿主应用接管\n- 仅允许一个 `Start` 节点（拖拽时校验）\n- 保存前对 `deleteEdges` 去重，防止重复 ID\n- 当后端仅返回 `workflowComponentId` 时，利用 `componentIdMap` 还原组件名，保持渲染稳定\n\n\n## 快速参考（API 与文件）\n- 对外组件：`StandaloneWorkflowDesigner`（默认导出）\n- Props：`workflow`、`wfComponents`、`componentIdMap`、`saving`\n- Emits：`save(workflow)`、`run({ workflow })`、`deleteNode(nodeUuid)`\n- 新增节点/边：\n  - 节点：`components/nodes/<Name>Node.vue`（可选）+ `wfComponents` 项\n  - 边：`components/edges/<Name>Edge.vue`\n- 属性面板与默认值：`properties/<Name>NodeProperty.vue`、`properties/defaults.ts`\n- 工具与类型：`utils/workflow-util.ts`、`types/index.d.ts`\n- Store（可选）：`store/index.ts`，路由注入 `setWorkflowDesignerNavigator`\n\n\n"
  },
  {
    "path": "apps/web-antd/src/packages/workflow-designer/index.ts",
    "content": "import WorkflowDesigner from './StandaloneWorkflowDesigner.vue'\n\nexport default WorkflowDesigner\nexport * from './types/index.d'\n\n\n"
  },
  {
    "path": "apps/web-antd/src/packages/workflow-designer/panels/RightPanel.vue",
    "content": "<script setup lang=\"ts\">\nimport { computed, onMounted, ref, watch } from 'vue'\nimport { Input } from 'ant-design-vue'\nimport SvgIcon from '../components/SvgIcon.vue'\nimport { getIconByComponentName, getIconClassByComponentName } from '../utils/workflow-util'\nimport GenericNodeProperty from '../properties/GenericNodeProperty.vue'\nimport type { WorkflowInfo, UIWorkflow, WorkflowNode } from '../types/index.d'\n\ninterface Props {\n  workflow: WorkflowInfo\n  uiWorkflow: UIWorkflow\n  hidePropertyPanel: boolean\n  wfNode?: WorkflowNode\n}\nconst props = withDefaults(defineProps<Props>(), {\n  hidePropertyPanel: false,\n  wfNode: undefined,\n})\n\nconst nodeTitle = ref<string>('')\n\nwatch(() => props.wfNode, (val) => { nodeTitle.value = val?.title || '' }, { immediate: true })\nwatch(nodeTitle, (val) => { if (props.wfNode) props.wfNode.title = val }, { immediate: false })\n\nconst innerHeight = window.innerHeight < 800 ? 800 : window.innerHeight\nonMounted(() => { nodeTitle.value = props.wfNode?.title || '' })\n\n// 自动扫描专属属性面板：文件名 <Name>NodeProperty.vue → 键名为小写 name\nconst propertyModules = import.meta.glob('../properties/*NodeProperty.vue', {\n  eager: true,\n  import: 'default',\n}) as Record<string, any>\n\nfunction toPropertyKey(path: string) {\n  const file = path.substring(path.lastIndexOf('/') + 1)\n  return file.replace(/NodeProperty\\.vue$/, '').toLowerCase()\n}\n\nconst propertyMap = Object.fromEntries(\n  Object.entries(propertyModules).map(([p, mod]) => [toPropertyKey(p), mod]),\n)\n\nconst resolvedPropertyComponent = computed(() => {\n  const name = props.wfNode?.wfComponent?.name?.toLowerCase()\n  if (!name) return GenericNodeProperty\n  return propertyMap[name] || GenericNodeProperty\n})\n</script>\n\n<template>\n  <div class=\"absolute right-0 top-20 bg-white rounded-lg shadow-xl\">\n    <div v-if=\"!hidePropertyPanel && wfNode\" class=\"px-3 pt-5 h-full\" style=\"width:600px\">\n      <div class=\"w-full flex flex-col border-b divide-gray-400 pb-3 mb-5\">\n        <div class=\"text-3xl flex items-center h-10 mb-2\">\n          <SvgIcon class=\"mt-1 mr-2\" :class=\"getIconClassByComponentName(wfNode?.wfComponent?.name || '')\" :icon=\"getIconByComponentName(wfNode?.wfComponent?.name || '')\" />\n          <Input v-model:value=\"nodeTitle\" placeholder=\"节点名称\" class=\"h-8 border-gray-100\" style=\"font-size: 1rem;line-height: 1.5rem;font-weight: 700;\" />\n        </div>\n        <div class=\"text-sm text-gray-500\">组件功能：{{ wfNode?.wfComponent?.remark || '' }}</div>\n      </div>\n      <div class=\"overflow-y-auto\" :style=\"`height:${innerHeight - 250}px`\">\n        <component :is=\"resolvedPropertyComponent\" :workflow=\"workflow\" :ui-workflow=\"uiWorkflow\" :wf-node=\"wfNode\" />\n      </div>\n    </div>\n  </div>\n</template>\n\n\n"
  },
  {
    "path": "apps/web-antd/src/packages/workflow-designer/properties/AnswerNodeProperty.vue",
    "content": "<script setup lang=\"ts\">\nimport { onMounted, ref, watch } from 'vue'\nimport { Input, Select } from 'ant-design-vue'\nimport type { WorkflowInfo, WorkflowNode } from '../types/index.d'\nimport { requestClient } from '#/api/request'\nimport WfVariableSelector from '../components/WfVariableSelector.vue'\n\ninterface Props {\n  workflow: WorkflowInfo\n  wfNode: WorkflowNode\n}\nconst props = defineProps<Props>()\nconst nodeConfig = props.wfNode.nodeConfig as any\nif (nodeConfig.category === undefined) nodeConfig.category = ''\n\n// 模型下拉选项\nconst modelOptions = ref<Array<{ label: string; value: string; category?: string }>>([]);\n\nasync function fetchModels() {\n  try {\n    const res: any = await requestClient.get('/system/model/list', { params: {} })\n    const records = (res?.records || res?.rows || res || []) as Array<any>\n    modelOptions.value = records.map((m: any) => ({\n      label: m.modelName,\n      value: m.modelName,\n      category: m.category ?? '',\n    }))\n  } catch (e) {\n    modelOptions.value = []\n  }\n}\n\nonMounted(() => { fetchModels() })\n\n// 监听编码变化，自动写回可读名称，保证画布节点始终展示名称\nwatch(() => nodeConfig.model_name, (val) => {\n  if (!val) { nodeConfig.model_name = ''; return }\n  const hit = modelOptions.value.find(opt => opt.value === String(val))\n  nodeConfig.model_name = hit ? hit.label : String(val)\n  // 同步保存分类到 nodeConfig.category（来自后端的 category 字段）\n  nodeConfig.category = hit?.category ?? ''\n})\n</script>\n\n\n\n<template>\n  <div class=\"flex flex-col w-full\">\n    <!-- 放在第一位：变量选择器（多变量，写入 inputConfig.ref_inputs） -->\n    <WfVariableSelector :workflow=\"workflow\" :wf-node=\"wfNode\" :exclude-nodes=\"[wfNode.uuid]\" />\n    <div class=\"mt-2\">\n      <div class=\"text-sm mb-1\">模型名</div>\n      <Select v-model:value=\"nodeConfig.model_name\" :options=\"modelOptions\" show-search :allow-clear=\"true\" placeholder=\"请选择模型\" class=\"w-full\" />\n    </div>\n    <div class=\"mt-4\">\n      <div class=\"text-sm mb-1\">提示词<span class=\"text-red-500\">*</span></div>\n      <Input v-model:value=\"nodeConfig.prompt\" type=\"textarea\" :auto-size=\"{ minRows: 3, maxRows: 8 }\" />\n    </div>\n  </div>\n</template>\n\n\n"
  },
  {
    "path": "apps/web-antd/src/packages/workflow-designer/properties/ClassifierNodeProperty.vue",
    "content": "<script setup lang=\"ts\">\nimport { v4 as uuidv4 } from 'uuid'\nimport { Button, Collapse, Input } from 'ant-design-vue'\nimport type { WorkflowInfo, WorkflowNode, UIWorkflow } from '../types/index.d'\nimport { createNewEdge, deleteEdgesBySourceHandle, updateEdgeBySourceHandle } from '../utils/workflow-util'\n\ninterface Category { category_uuid: string; category_name: string; target_node_uuid: string }\n\ninterface Props {\n  workflow: WorkflowInfo\n  uiWorkflow: UIWorkflow\n  wfNode: WorkflowNode\n}\nconst props = defineProps<Props>()\nconst nodeConfig = props.wfNode.nodeConfig as { model_name?: string; categories: Category[] }\n\nfunction onCategoryTargetSelected(category: Category, nodeUuid: string) {\n  category.target_node_uuid = nodeUuid\n  updateEdgeBySourceHandle({ workflow: props.workflow, uiWorkflow: props.uiWorkflow, source: props.wfNode.uuid, sourceHandle: category.category_uuid, target: nodeUuid })\n}\n\nfunction onAdd() {\n  const uuid = uuidv4().replace(/-/g, '')\n  nodeConfig.categories.push({ category_uuid: uuid, category_name: `category_${uuid.slice(0, 5)}`, target_node_uuid: '' })\n  createNewEdge({ workflow: props.workflow, uiWorkflow: props.uiWorkflow, source: props.wfNode.uuid, sourceHandle: uuid, target: '' })\n}\n\nfunction onDeleteCategory(category: Category) {\n  const idx = nodeConfig.categories.findIndex(item => item.category_uuid === category.category_uuid)\n  if (idx >= 0) {\n    deleteEdgesBySourceHandle(props.workflow, props.uiWorkflow, props.wfNode.uuid, category.category_uuid)\n    nodeConfig.categories.splice(idx, 1)\n  }\n}\n</script>\n\n<template>\n  <div class=\"flex flex-col w-full\">\n    <div class=\"mt-2\">\n      <div class=\"text-sm mb-1\">模型名</div>\n      <Input v-model:value=\"nodeConfig.model_name\" placeholder=\"如: classify-model\" />\n    </div>\n    <div class=\"mt-4 flex flex-col\">\n      <div class=\"text-sm mb-1\">类别</div>\n      <Collapse :default-active-key=\"['0']\">\n        <Collapse.Panel v-for=\"(category, idx) in nodeConfig.categories\" :key=\"category.category_uuid\" class=\"border border-gray-200 rounded-md m-2\">\n          <template #header>\n            <div class=\"pl-1 flex justify-between items-center w-full\">\n              <span>分类{{ idx + 1 }}：{{ category.category_name?.substring(0, 30) }}</span>\n              <span v-show=\"nodeConfig.categories.length > 2\" class=\"p-2 cursor-pointer\" @click.stop=\"onDeleteCategory(category)\">X</span>\n            </div>\n          </template>\n          <div class=\"flex flex-col w-full bg-gray-100 px-3\">\n            <div class=\"mt-2\">类别名称</div>\n            <div class=\"mb-2\">\n              <Input v-model:value=\"category.category_name\" type=\"textarea\" :auto-size=\"{ minRows: 1, maxRows: 3 }\" />\n            </div>\n            <div class=\"mb-3\">下一步节点 UUID</div>\n            <div class=\"mb-3\">\n              <Input v-model:value=\"category.target_node_uuid\" placeholder=\"在宿主中实现 UUID 选择器可替换此输入\" @update:value=\"(val: string) => onCategoryTargetSelected(category, val || '')\" />\n            </div>\n          </div>\n        </Collapse.Panel>\n      </Collapse>\n    </div>\n    <br>\n    <Button type=\"dashed\" @click=\"onAdd\">+新增类别</Button>\n  </div>\n</template>\n \n"
  },
  {
    "path": "apps/web-antd/src/packages/workflow-designer/properties/GenericNodeProperty.vue",
    "content": "<script setup lang=\"ts\">\nimport { computed } from 'vue'\nimport { Input, InputNumber, Switch } from 'ant-design-vue'\nimport type { WorkflowInfo, WorkflowNode, UIWorkflow } from '../types/index.d'\n\ninterface Props {\n  workflow: WorkflowInfo\n  uiWorkflow: UIWorkflow\n  wfNode: WorkflowNode\n}\nconst props = defineProps<Props>()\n\nconst entries = computed(() => Object.entries(props.wfNode.nodeConfig || {}))\n\nfunction getVal(key: string) {\n  return (props.wfNode.nodeConfig as any)[key]\n}\n\nfunction setVal(key: string, value: any) {\n  (props.wfNode.nodeConfig as any)[key] = value\n}\n\nfunction toJson(val: any) {\n  try {\n    return JSON.stringify(val, null, 2)\n  } catch (e) {\n    return ''\n  }\n}\n\nfunction setFromJson(key: string, json: string) {\n  try {\n    const obj = JSON.parse(json)\n    setVal(key, obj)\n  } catch (e) {\n    // ignore parse error\n  }\n}\n</script>\n\n<template>\n  <div class=\"px-2 space-y-3\">\n    <div class=\"text-base font-bold\">节点参数</div>\n    <div v-if=\"!entries.length\" class=\"text-neutral-400\">无可编辑参数</div>\n    <div v-for=\"[k, v] in entries\" :key=\"k\" class=\"space-y-1\">\n      <div class=\"text-xs text-neutral-500\">{{ k }}</div>\n      <div v-if=\"typeof v === 'boolean'\">\n        <Switch :checked=\"getVal(k)\" @update:checked=\"(val: boolean) => setVal(k, val)\" />\n      </div>\n      <div v-else-if=\"typeof v === 'number'\">\n        <InputNumber :value=\"getVal(k)\" @update:value=\"(val: number) => setVal(k, val)\" class=\"w-full\" />\n      </div>\n      <div v-else-if=\"typeof v === 'string'\">\n        <Input :value=\"getVal(k)\" @update:value=\"(val: string) => setVal(k, val)\" />\n      </div>\n      <div v-else-if=\"Array.isArray(v)\">\n        <Input type=\"textarea\" :value=\"toJson(getVal(k))\" :auto-size=\"{ minRows: 3, maxRows: 12 }\" @update:value=\"(val: string) => setFromJson(k, val)\" />\n      </div>\n      <div v-else-if=\"typeof v === 'object' && v !== null\">\n        <Input type=\"textarea\" :value=\"toJson(getVal(k))\" :auto-size=\"{ minRows: 3, maxRows: 12 }\" @update:value=\"(val: string) => setFromJson(k, val)\" />\n      </div>\n      <div v-else class=\"text-neutral-400\">Unsupported</div>\n    </div>\n  </div>\n</template>\n\n\n"
  },
  {
    "path": "apps/web-antd/src/packages/workflow-designer/properties/KeywordExtractorNodeProperty.vue",
    "content": "<script setup lang=\"ts\">\nimport { Input, InputNumber } from 'ant-design-vue'\nimport type { WorkflowInfo, WorkflowNode } from '../types/index.d'\n\ninterface Props {\n  workflow: WorkflowInfo\n  wfNode: WorkflowNode\n}\nconst props = defineProps<Props>()\nconst nodeConfig = props.wfNode.nodeConfig as any\n</script>\n\n<template>\n  <div class=\"flex flex-col w-full\">\n    <div class=\"mt-2\">\n      <div class=\"text-sm mb-1\">模型名</div>\n      <Input v-model:value=\"nodeConfig.model_name\" placeholder=\"如: keywords-model\" />\n    </div>\n    <div class=\"mt-4\">\n      <div class=\"text-sm mb-1\">关键词数量(top_n)</div>\n      <InputNumber v-model:value=\"nodeConfig.top_n\" :min=\"1\" :max=\"50\" class=\"w-full\" />\n    </div>\n  </div>\n</template>\n \n"
  },
  {
    "path": "apps/web-antd/src/packages/workflow-designer/properties/StartNodeProperty.vue",
    "content": "<script setup lang=\"ts\">\nimport { computed, h, reactive, ref } from 'vue'\nimport { Button, Collapse, Table, Space, Input, InputNumber, Modal, Select, Switch } from 'ant-design-vue'\nimport { v4 as uuidv4 } from 'uuid'\nimport SvgIcon from '../components/SvgIcon.vue'\nimport { getNameByInputType } from '../utils/workflow-util'\nimport type { WorkflowInfo, WorkflowNode, NodeIODefinition } from '../types/index.d'\n\ninterface Props {\n  workflow: WorkflowInfo\n  wfNode: WorkflowNode\n  uiWorkflow?: any // 仅为消除额外属性告警，无需使用\n}\nconst props = defineProps<Props>()\nconst showModal = ref<boolean>(false)\nconst tmpItem = reactive<NodeIODefinition>({ uuid: '', type: 1, name: '', title: '', required: false, limit: 10, multiple: false })\n\nconst options = [\n  { label: '文本', value: 1 },\n  { label: '数字', value: 2 },\n  { label: '文件', value: 4 },\n  { label: '布尔值', value: 5 },\n]\n\nconst columns = [\n  { title: '变量名', dataIndex: 'name', key: 'name' },\n  { title: '标题', dataIndex: 'title', key: 'title' },\n  { \n    title: '类型', \n    dataIndex: 'type', \n    key: 'type',\n    customRender: ({ record }: { record: { type: number } }) => getNameByInputType(record.type)\n  },\n  { \n    title: '必填', \n    dataIndex: 'required', \n    key: 'required',\n    customRender: ({ record }: { record: { required: boolean } }) => (record.required ? '是' : '否')\n  },\n  {\n    title: '操作',\n    key: 'actions',\n    customRender: ({ record }: { record: NodeIODefinition }) => h('div', { class: 'flex gap-2' }, {\n      default: () => [\n        h(SvgIcon, { icon: 'carbon:edit', class: 'text-base cursor-pointer', onClick: () => onEdit(record) }),\n        h(SvgIcon, { icon: 'carbon:delete', class: 'text-base cursor-pointer', onClick: () => onDelete(record) }),\n      ],\n    }),\n  },\n]\n\nconst submitStatus = computed(() => !!(tmpItem.name && tmpItem.title))\n\n// 防御性初始化：确保 inputConfig 结构存在且为数组，避免新增时报错\nif (!props.wfNode.inputConfig) (props.wfNode as any).inputConfig = { user_inputs: [], ref_inputs: [] }\nif (!Array.isArray(props.wfNode.inputConfig.user_inputs)) (props.wfNode.inputConfig as any).user_inputs = []\nif (!Array.isArray(props.wfNode.inputConfig.ref_inputs)) (props.wfNode.inputConfig as any).ref_inputs = []\n// 后端开始节点可能没有 nodeConfig/prologue 字段，做兼容初始化\nif (!props.wfNode.nodeConfig) (props.wfNode as any).nodeConfig = {}\nif ((props.wfNode.nodeConfig as any).prologue === undefined) (props.wfNode.nodeConfig as any).prologue = ''\n\nfunction onEdit(row: NodeIODefinition) {\n  showModal.value = true\n  const idx = props.wfNode.inputConfig.user_inputs.findIndex(item => item.uuid === row.uuid)\n  const hit = idx > -1 ? props.wfNode.inputConfig.user_inputs[idx] : {\n    uuid: row.uuid,\n    type: row.type,\n    name: row.name,\n    title: row.title,\n    required: row.required,\n    multiple: false,\n    limit: 10,\n  }\n  Object.assign(tmpItem, hit)\n}\n\nfunction onDelete(row: NodeIODefinition) {\n  const idx = props.wfNode.inputConfig.user_inputs.findIndex(item => item.uuid === row.uuid)\n  if (idx > -1) props.wfNode.inputConfig.user_inputs.splice(idx, 1)\n}\n\nfunction onShowModal() {\n  showModal.value = true\n  Object.assign(tmpItem, { uuid: uuidv4().replace(/-/g, ''), type: 1, name: '', title: '', required: false, multiple: false, limit: 10 })\n}\n\nfunction submitForm() {\n  showModal.value = false\n  const idx = props.wfNode.inputConfig.user_inputs.findIndex(item => item.uuid === tmpItem.uuid)\n  if (idx > -1) {\n    // 使用替换避免 TS 关于可能为 undefined 的报错\n    props.wfNode.inputConfig.user_inputs.splice(idx, 1, { ...tmpItem })\n  } else {\n    props.wfNode.inputConfig.user_inputs.push({ ...tmpItem })\n    Object.assign(tmpItem, { uuid: '', type: 1, name: '', title: '', required: false, multiple: false, limit: 10 })\n  }\n}\n</script>\n\n<template>\n  <div class=\"flex flex-col w-full space-y-1\">\n    <div>\n      <div class=\"text-xl mb-1\">开场白</div>\n      <div>\n        <Input v-model:value=\"(wfNode.nodeConfig as any).prologue\" type=\"textarea\" :auto-size=\"{ minRows: 2, maxRows: 6 }\" />\n      </div>\n    </div>\n    <br />\n    <Collapse :default-active-key=\"['1']\">\n      <Collapse.Panel key=\"1\" header=\"输入\" class=\"border border-gray-200 rounded-md m-2 px-3 pb-3\">\n        <Table :columns=\"columns\" :data-source=\"wfNode.inputConfig.user_inputs\" :pagination=\"false\" />\n      </Collapse.Panel>\n    </Collapse>\n    <br />\n    <Button @click=\"onShowModal\">+新增</Button>\n  </div>\n  <Modal v-model:open=\"showModal\" title=\"变量设置\" width=\"600px\" :footer=\"null\">\n    <div class=\"flex flex-col w-full justify-between space-y-4\">\n      <div>类型<Select v-model:value=\"tmpItem.type\" :options=\"options\" class=\"w-full\" /></div>\n      <div>名称<Input v-model:value=\"tmpItem.name\" :maxlength=\"50\" show-count /></div>\n      <div>标题（显示名称）<Input v-model:value=\"tmpItem.title\" :maxlength=\"50\" show-count /></div>\n      <div>是否必须<Switch v-model:checked=\"tmpItem.required\" size=\"small\" /></div>\n      <div v-if=\"tmpItem.type === 3\">多选<Switch v-model:checked=\"tmpItem.multiple\" /></div>\n      <div v-if=\"tmpItem.type === 4\">最大文件数量<InputNumber v-model:value=\"tmpItem.limit\" class=\"w-full\" /></div>\n      <Space class=\"w-full justify-end\" style=\"margin-top: 20px\">\n        <Button block type=\"primary\" :disabled=\"!submitStatus\" @click=\"submitForm\">确认</Button>\n      </Space>\n    </div>\n  </Modal>\n</template>\n \n"
  },
  {
    "path": "apps/web-antd/src/packages/workflow-designer/properties/SwitcherNodeProperty.vue",
    "content": "<script setup lang=\"ts\">\nimport { computed } from 'vue'\nimport { v4 as uuidv4 } from 'uuid'\nimport { Button, Collapse, Input, Select, Space, message } from 'ant-design-vue'\nimport type { SelectValue } from 'ant-design-vue/es/select'\nimport type { WorkflowInfo, WorkflowNode, UIWorkflow } from '../types/index.d'\nimport { createNewEdge, deleteEdgesBySourceHandle, updateEdgeBySourceHandle } from '../utils/workflow-util'\nimport { conditionOperators, logicOperators } from '../utils/operators'\n\ninterface Condition {\n  node_uuid: string\n  node_param_name: string\n  operator: string\n  value: string\n}\n\ninterface Case {\n  uuid: string\n  operator: 'and' | 'or'\n  target_node_uuid: string\n  conditions: Condition[]\n}\n\ninterface Props {\n  workflow: WorkflowInfo\n  uiWorkflow: UIWorkflow\n  wfNode: WorkflowNode\n}\n\nconst props = defineProps<Props>()\n\nfunction normalizeToString(value: SelectValue): string {\n  if (Array.isArray(value)) return normalizeToString(value[0] ?? '')\n  if (value === null || value === undefined) return ''\n  return String(value)\n}\n\n// 确保 nodeConfig 结构正确\nconst nodeConfig = props.wfNode.nodeConfig as {\n  default_target_node_uuid?: string\n  cases: Case[]\n}\n\n// 防御性初始化\nif (!nodeConfig.cases) {\n  nodeConfig.cases = []\n}\nif (nodeConfig.default_target_node_uuid === undefined) {\n  nodeConfig.default_target_node_uuid = ''\n}\n\n// 获取可用的节点列表（排除自身和Start节点）\nconst availableNodes = computed(() => {\n  return props.workflow.nodes\n    .filter(n => n.uuid !== props.wfNode.uuid && n.wfComponent?.name !== 'Start')\n    .map(n => ({\n      label: n.title || n.wfComponent?.title || n.uuid,\n      value: n.uuid,\n    }))\n})\n\n// 注意：availableParams 暂未使用，但保留供未来功能使用\n// const availableParams = computed(() => {\n//   const startNode = props.workflow.nodes.find(n => n.wfComponent?.name === 'Start')\n//   if (!startNode?.inputConfig?.user_inputs) return []\n//   \n//   return startNode.inputConfig.user_inputs.map(input => ({\n//     label: input.title || input.name,\n//     value: input.name,\n//     nodeUuid: startNode.uuid,\n//   }))\n// })\n\n// 获取所有可选的源节点（包括Start和其他节点的输出）\nconst sourceNodeOptions = computed(() => {\n  const options: Array<{ label: string; value: string }> = []\n  \n  // 添加Start节点的用户输入\n  const startNode = props.workflow.nodes.find(n => n.wfComponent?.name === 'Start')\n  if (startNode) {\n    options.push({\n      label: `${startNode.title || '开始'} (用户输入)`,\n      value: startNode.uuid,\n    })\n  }\n  \n  // 添加其他前置节点\n  props.workflow.nodes\n    .filter(n => n.uuid !== props.wfNode.uuid && n.wfComponent?.name !== 'Start')\n    .forEach(n => {\n      options.push({\n        label: n.title || n.wfComponent?.title || n.uuid,\n        value: n.uuid,\n      })\n    })\n  \n  return options\n})\n\n// 根据选中的节点获取可用参数\nfunction getParamsForNode(nodeUuid: string) {\n  const node = props.workflow.nodes.find(n => n.uuid === nodeUuid)\n  if (!node) return []\n  \n  if (node.wfComponent?.name === 'Start') {\n    return node.inputConfig?.user_inputs?.map(input => ({\n      label: input.title || input.name,\n      value: input.name,\n    })) || []\n  }\n  \n  // 其他节点返回通用输出参数\n  return [\n    { label: '输出结果', value: 'output' },\n    { label: '状态', value: 'status' },\n  ]\n}\n\n// 新增分支\nfunction onAddCase() {\n  const uuid = uuidv4().replace(/-/g, '')\n  const startNode = props.workflow.nodes.find(n => n.wfComponent?.name === 'Start')\n  const firstParam = startNode?.inputConfig?.user_inputs?.[0]?.name || ''\n  \n  const newCase: Case = {\n    uuid,\n    operator: 'and',\n    target_node_uuid: '',\n    conditions: [\n      {\n        node_uuid: startNode?.uuid || '',\n        node_param_name: firstParam,\n        operator: 'contains',\n        value: '',\n      },\n    ],\n  }\n  \n  nodeConfig.cases.push(newCase)\n  // 创建对应的边\n  createNewEdge({\n    workflow: props.workflow,\n    uiWorkflow: props.uiWorkflow,\n    source: props.wfNode.uuid,\n    sourceHandle: uuid,\n    target: '',\n  })\n}\n\n// 删除分支\nfunction onDeleteCase(caseItem: Case) {\n  const idx = nodeConfig.cases.findIndex(item => item.uuid === caseItem.uuid)\n  if (idx >= 0) {\n    deleteEdgesBySourceHandle(props.workflow, props.uiWorkflow, props.wfNode.uuid, caseItem.uuid)\n    nodeConfig.cases.splice(idx, 1)\n  }\n}\n\n// 更新分支目标节点\nfunction onCaseTargetSelected(caseItem: Case, nodeUuid: SelectValue) {\n  const normalized = normalizeToString(nodeUuid)\n  caseItem.target_node_uuid = normalized\n  updateEdgeBySourceHandle({\n    workflow: props.workflow,\n    uiWorkflow: props.uiWorkflow,\n    source: props.wfNode.uuid,\n    sourceHandle: caseItem.uuid,\n    target: normalized,\n  })\n}\n\n// 更新默认分支目标\nfunction onDefaultTargetSelected(nodeUuid: SelectValue) {\n  const normalized = normalizeToString(nodeUuid)\n  nodeConfig.default_target_node_uuid = normalized\n  // 默认分支使用特殊的handle id\n  updateEdgeBySourceHandle({\n    workflow: props.workflow,\n    uiWorkflow: props.uiWorkflow,\n    source: props.wfNode.uuid,\n    sourceHandle: 'default',\n    target: normalized,\n  })\n}\n\n// 新增条件\nfunction onAddCondition(caseItem: Case) {\n  const startNode = props.workflow.nodes.find(n => n.wfComponent?.name === 'Start')\n  const firstParam = startNode?.inputConfig?.user_inputs?.[0]?.name || ''\n  \n  caseItem.conditions.push({\n    node_uuid: startNode?.uuid || '',\n    node_param_name: firstParam,\n    operator: 'contains',\n    value: '',\n  })\n}\n\n// 删除条件\nfunction onDeleteCondition(caseItem: Case, conditionIndex: number) {\n  if (caseItem.conditions.length > 1) {\n    caseItem.conditions.splice(conditionIndex, 1)\n  }\n}\n\n// 当源节点变化时，重置参数选择\nfunction onSourceNodeChange(condition: Condition, nodeUuid: SelectValue) {\n  const normalized = normalizeToString(nodeUuid)\n  condition.node_uuid = normalized\n  const params = getParamsForNode(normalized)\n  condition.node_param_name = params[0]?.value || ''\n}\n\n// 验证条件是否有效\nfunction validateCondition(condition: Condition): { valid: boolean; message?: string } {\n  if (!condition.node_uuid) {\n    return { valid: false, message: '请选择源节点' }\n  }\n  if (!condition.node_param_name) {\n    return { valid: false, message: '请选择参数' }\n  }\n  if (!condition.operator) {\n    return { valid: false, message: '请选择运算符' }\n  }\n  // 对于非 empty/not_empty 运算符，value 不能为空\n  if (!['empty', 'not empty'].includes(condition.operator) && !condition.value) {\n    return { valid: false, message: '请输入比较值' }\n  }\n  return { valid: true }\n}\n\n// 验证分支是否有效\nfunction validateCase(caseItem: Case, index: number): { valid: boolean; message?: string } {\n  if (!caseItem.target_node_uuid) {\n    return { valid: false, message: `分支 ${index + 1} 的目标节点不能为空` }\n  }\n  if (!caseItem.conditions || caseItem.conditions.length === 0) {\n    return { valid: false, message: `分支 ${index + 1} 至少需要一个条件` }\n  }\n  for (let i = 0; i < caseItem.conditions.length; i++) {\n    const condition = caseItem.conditions[i]\n    if (!condition) continue\n    const result = validateCondition(condition)\n    if (!result.valid) {\n      return { valid: false, message: `分支 ${index + 1} 的条件 ${i + 1}: ${result.message}` }\n    }\n  }\n  return { valid: true }\n}\n\n// 检查是否有无效配置\nfunction hasInvalidConfig(caseItem: Case): boolean {\n  return !caseItem.target_node_uuid || \n         caseItem.conditions.some(c => \n           !c.node_uuid || \n           !c.node_param_name || \n           (!['empty', 'not empty'].includes(c.operator) && !c.value)\n         )\n}\n\n// 验证所有配置（用于保存前验证）\nfunction validateAllConfig(): boolean {\n  // 检查是否有分支\n  if (nodeConfig.cases.length === 0) {\n    message.warning('请至少添加一个条件分支')\n    return false\n  }\n  \n  // 验证每个分支\n  for (let i = 0; i < nodeConfig.cases.length; i++) {\n    const caseItem = nodeConfig.cases[i]\n    if (!caseItem) continue\n    const result = validateCase(caseItem, i)\n    if (!result.valid) {\n      message.warning(result.message || '配置无效')\n      return false\n    }\n  }\n  \n  // 验证默认分支\n  if (!nodeConfig.default_target_node_uuid) {\n    message.warning('请为默认分支选择目标节点')\n    return false\n  }\n  \n  return true\n}\n</script>\n\n<template>\n  <div class=\"switcher-property-panel\">\n    <!-- 说明 -->\n    <div class=\"info-card\">\n      <div class=\"info-header\">\n        <span class=\"info-icon\"></span>\n        <span class=\"info-icon\"></span>\n\n        <span class=\"info-title\"> 条件分支说明</span>\n      </div>\n      <ul class=\"info-list\">\n        <li>按顺序评估每个分支的条件</li>\n        <li>第一个满足条件的分支将被执行</li>\n        <li>如果所有分支都不满足，执行默认分支</li>\n        <li>每个分支可以包含多个条件，支持AND/OR逻辑</li>\n      </ul>\n    </div>\n\n    <!-- 分支列表 -->\n    <div class=\"branches-section\">\n      <div class=\"section-header\">\n        <h3 class=\"section-title\">条件分支</h3>\n        <Button size=\"small\" type=\"dashed\" @click=\"onAddCase\">\n          <template #icon>\n            <span>+</span>\n          </template>\n          新增分支\n        </Button>\n      </div>\n\n      <Collapse v-if=\"nodeConfig.cases.length > 0\" :default-active-key=\"['0']\" class=\"branches-collapse\">\n        <Collapse.Panel\n          v-for=\"(caseItem, caseIdx) in nodeConfig.cases\"\n          :key=\"caseItem.uuid\"\n          class=\"branch-collapse-item\"\n        >\n          <template #header>\n            <div class=\"branch-header\">\n              <div class=\"branch-header-left\">\n                <span class=\"branch-number-badge\" :class=\"{ 'invalid': hasInvalidConfig(caseItem) }\">\n                  {{ caseIdx + 1 }}\n                  <span v-if=\"hasInvalidConfig(caseItem)\" class=\"warning-icon\">⚠</span>\n                </span>\n                <span class=\"branch-title\">分支 {{ caseIdx + 1 }}</span>\n              </div>\n              <span class=\"branch-meta\">\n                {{ caseItem.conditions.length }} 个条件 · {{ caseItem.operator.toUpperCase() }}\n              </span>\n            </div>\n          </template>\n          \n          <template #extra>\n            <div class=\"delete-btn\" @click.stop=\"onDeleteCase(caseItem)\">\n              <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n                <path d=\"M3 6h18M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2M10 11v6M14 11v6\"/>\n              </svg>\n            </div>\n          </template>\n\n          <div class=\"branch-content\">\n            <!-- 逻辑运算符 -->\n            <div class=\"form-group\">\n              <label class=\"form-label\">条件组合方式</label>\n              <Select\n                v-model:value=\"caseItem.operator\"\n                :options=\"logicOperators as any\"\n                size=\"small\"\n              />\n            </div>\n\n            <!-- 条件列表 -->\n            <div class=\"conditions-section\">\n              <label class=\"form-label\">条件列表</label>\n              <div\n                v-for=\"(condition, condIdx) in caseItem.conditions\"\n                :key=\"condIdx\"\n                class=\"condition-item\"\n              >\n                <Space direction=\"vertical\" :size=\"12\">\n                  <!-- 源节点选择 -->\n                  <div class=\"field-group\">\n                    <label class=\"field-label\">源节点</label>\n                    <Select\n                      :value=\"condition.node_uuid\"\n                      :options=\"sourceNodeOptions\"\n                      size=\"small\"\n                      placeholder=\"选择节点\"\n                      @update:value=\"(val) => onSourceNodeChange(condition, val)\"\n                    />\n                  </div>\n\n                  <!-- 参数选择 -->\n                  <div class=\"field-group\">\n                    <label class=\"field-label\">参数</label>\n                    <Select\n                      v-model:value=\"condition.node_param_name\"\n                      :options=\"getParamsForNode(condition.node_uuid)\"\n                      size=\"small\"\n                      placeholder=\"选择参数\"\n                    />\n                  </div>\n\n                  <!-- 运算符 -->\n                  <div class=\"field-group\">\n                    <label class=\"field-label\">运算符</label>\n                    <Select\n                      v-model:value=\"condition.operator\"\n                      :options=\"conditionOperators as any\"\n                      size=\"small\"\n                    />\n                  </div>\n\n                  <!-- 比较值 -->\n                  <div v-if=\"!['empty', 'not empty'].includes(condition.operator)\" class=\"field-group\">\n                    <label class=\"field-label\">\n                      比较值\n                      <span v-if=\"!condition.value\" class=\"required-mark\">*</span>\n                    </label>\n                    <Input\n                      v-model:value=\"condition.value\"\n                      :status=\"!condition.value ? 'warning' : undefined\"\n                      size=\"small\"\n                      placeholder=\"输入比较值\"\n                    />\n                    <div v-if=\"!condition.value\" class=\"warning-hint\">\n                      💡 建议填写比较值，空值会匹配任何内容\n                    </div>\n                  </div>\n\n                  <!-- 删除条件按钮 -->\n                  <div v-if=\"caseItem.conditions.length > 1\" class=\"condition-actions\">\n                    <Button\n                      size=\"small\"\n                      type=\"text\"\n                      danger\n                      @click=\"onDeleteCondition(caseItem, condIdx)\"\n                    >\n                      <template #icon>\n                        <span>×</span>\n                      </template>\n                      删除此条件\n                    </Button>\n                  </div>\n                </Space>\n              </div>\n\n              <!-- 添加条件按钮 -->\n              <Button\n                class=\"add-condition-btn\"\n                size=\"small\"\n                type=\"dashed\"\n                block\n                @click=\"onAddCondition(caseItem)\"\n              >\n                <template #icon>\n                  <span>+</span>\n                </template>\n                添加条件\n              </Button>\n            </div>\n\n            <div class=\"divider\"></div>\n\n            <!-- 目标节点 -->\n            <div class=\"form-group\">\n              <label class=\"form-label\">\n                跳转到节点\n                <span v-if=\"!caseItem.target_node_uuid\" class=\"required-mark\">*</span>\n              </label>\n              <Select\n                :value=\"caseItem.target_node_uuid\"\n                :options=\"availableNodes\"\n                :status=\"!caseItem.target_node_uuid ? 'error' : undefined\"\n                size=\"small\"\n                placeholder=\"选择目标节点\"\n                allow-clear\n                @update:value=\"(val) => onCaseTargetSelected(caseItem, val || '')\"\n              />\n              <div v-if=\"!caseItem.target_node_uuid\" class=\"error-hint\">\n                ⚠️ 目标节点不能为空，否则该分支将被跳过\n              </div>\n            </div>\n          </div>\n        </Collapse.Panel>\n      </Collapse>\n\n      <div v-else class=\"empty-state\">\n        <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"48\" height=\"48\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\">\n          <path d=\"M9 11l3 3L22 4\"/>\n          <path d=\"M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11\"/>\n        </svg>\n        <p>暂无分支</p>\n        <span>点击上方按钮添加条件分支</span>\n      </div>\n    </div>\n\n    <!-- 默认分支 -->\n    <div class=\"default-branch-section\">\n      <div class=\"default-branch-card\">\n        <div class=\"default-branch-header\">\n          <div class=\"default-branch-title\">\n            <span class=\"default-icon\">🔄</span>\n            <span>默认分支</span>\n          </div>\n          <span class=\"default-branch-desc\">所有条件都不满足时执行</span>\n        </div>\n        <div class=\"form-group\">\n          <label class=\"form-label\">\n            跳转到节点\n            <span v-if=\"!nodeConfig.default_target_node_uuid\" class=\"required-mark\">*</span>\n          </label>\n          <Select\n            :value=\"nodeConfig.default_target_node_uuid\"\n            :options=\"availableNodes\"\n            :status=\"!nodeConfig.default_target_node_uuid ? 'error' : undefined\"\n            placeholder=\"选择默认目标节点\"\n            allow-clear\n            @update:value=\"(val) => onDefaultTargetSelected(val || '')\"\n          />\n          <div v-if=\"!nodeConfig.default_target_node_uuid\" class=\"error-hint\">\n            ⚠️ 默认分支的目标节点不能为空\n          </div>\n        </div>\n      </div>\n    </div>\n  </div>\n</template>\n\n<style scoped>\n/* 主容器 */\n.switcher-property-panel {\n  padding: 16px;\n  display: flex;\n  flex-direction: column;\n  gap: 20px;\n}\n\n/* 说明卡片 */\n.info-card {\n  background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);\n  border-radius: 8px;\n  padding: 16px;\n  color: white;\n}\n\n.info-header {\n  display: flex;\n  align-items: center;\n  gap: 8px;\n  margin-bottom: 12px;\n}\n\n.info-icon {\n  font-size: 20px;\n}\n\n.info-title {\n  font-size: 15px;\n  font-weight: 600;\n}\n\n.info-list {\n  list-style: none;\n  padding: 0;\n  margin: 0;\n  display: flex;\n  flex-direction: column;\n  gap: 6px;\n}\n\n.info-list li {\n  font-size: 13px;\n  padding-left: 20px;\n  position: relative;\n  line-height: 1.5;\n  opacity: 0.95;\n}\n\n.info-list li::before {\n  content: \"•\";\n  position: absolute;\n  left: 8px;\n  font-weight: bold;\n}\n\n/* 分支区域 */\n.branches-section {\n  display: flex;\n  flex-direction: column;\n  gap: 12px;\n}\n\n.section-header {\n  display: flex;\n  justify-content: space-between;\n  align-items: center;\n}\n\n.section-title {\n  font-size: 16px;\n  font-weight: 600;\n  color: #1f2937;\n  margin: 0;\n}\n\n/* 折叠面板 */\n.branches-collapse {\n  display: flex;\n  flex-direction: column;\n  gap: 12px;\n}\n\n.branch-collapse-item {\n  border: 1px solid #e5e7eb;\n  border-radius: 8px;\n  overflow: hidden;\n  background: white;\n  transition: all 0.2s;\n}\n\n.branch-collapse-item:hover {\n  border-color: #3b82f6;\n  box-shadow: 0 2px 8px rgba(59, 130, 246, 0.15);\n}\n\n:deep(.branch-collapse-item .n-collapse-item__header) {\n  padding: 12px 16px;\n  background: linear-gradient(to right, #f8fafc, #ffffff);\n  border-bottom: 1px solid #e5e7eb;\n}\n\n:deep(.branch-collapse-item .n-collapse-item__content-inner) {\n  padding: 0;\n}\n\n/* 分支头部 */\n.branch-header {\n  display: flex;\n  justify-content: space-between;\n  align-items: center;\n  width: 100%;\n  padding-right: 8px;\n}\n\n.branch-header-left {\n  display: flex;\n  align-items: center;\n  gap: 10px;\n}\n\n.branch-number-badge {\n  display: inline-flex;\n  align-items: center;\n  justify-content: center;\n  min-width: 24px;\n  height: 24px;\n  background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);\n  color: white;\n  border-radius: 6px;\n  font-size: 12px;\n  font-weight: 600;\n  padding: 0 6px;\n  box-shadow: 0 2px 4px rgba(59, 130, 246, 0.2);\n}\n\n.branch-title {\n  font-size: 14px;\n  font-weight: 600;\n  color: #374151;\n}\n\n.branch-meta {\n  font-size: 12px;\n  color: #6b7280;\n  font-weight: 500;\n}\n\n.delete-btn {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  width: 28px;\n  height: 28px;\n  border-radius: 6px;\n  cursor: pointer;\n  color: #ef4444;\n  transition: all 0.2s;\n}\n\n.delete-btn:hover {\n  background: #fee2e2;\n  color: #dc2626;\n}\n\n/* 分支内容 */\n.branch-content {\n  padding: 16px;\n  display: flex;\n  flex-direction: column;\n  gap: 16px;\n  background: linear-gradient(to bottom, #f8fafc, #ffffff);\n}\n\n/* 表单组 */\n.form-group {\n  display: flex;\n  flex-direction: column;\n  gap: 8px;\n}\n\n.form-label {\n  font-size: 13px;\n  font-weight: 600;\n  color: #374151;\n}\n\n/* 条件区域 */\n.conditions-section {\n  display: flex;\n  flex-direction: column;\n  gap: 12px;\n}\n\n.condition-item {\n  background: white;\n  border: 1px solid #e5e7eb;\n  border-radius: 8px;\n  padding: 14px;\n  transition: all 0.2s;\n  position: relative;\n}\n\n.condition-item::before {\n  content: '';\n  position: absolute;\n  left: 0;\n  top: 0;\n  bottom: 0;\n  width: 3px;\n  background: linear-gradient(to bottom, #3b82f6, #8b5cf6);\n  border-radius: 8px 0 0 8px;\n  opacity: 0;\n  transition: opacity 0.2s;\n}\n\n.condition-item:hover {\n  border-color: #cbd5e1;\n  box-shadow: 0 2px 8px rgba(59, 130, 246, 0.08);\n}\n\n.condition-item:hover::before {\n  opacity: 1;\n}\n\n/* 字段组 */\n.field-group {\n  display: flex;\n  flex-direction: column;\n  gap: 6px;\n}\n\n.field-label {\n  font-size: 12px;\n  font-weight: 500;\n  color: #6b7280;\n}\n\n.condition-actions {\n  display: flex;\n  justify-content: flex-end;\n  padding-top: 4px;\n}\n\n/* 添加条件按钮 */\n.add-condition-btn {\n  margin-top: 4px;\n}\n\n/* 分隔线 */\n.divider {\n  height: 1px;\n  background: linear-gradient(to right, transparent, #e5e7eb, transparent);\n  margin: 8px 0;\n}\n\n/* 空状态 */\n.empty-state {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  justify-content: center;\n  padding: 48px 24px;\n  border: 2px dashed #e5e7eb;\n  border-radius: 8px;\n  background: #fafafa;\n  color: #9ca3af;\n}\n\n.empty-state svg {\n  margin-bottom: 12px;\n  opacity: 0.5;\n}\n\n.empty-state p {\n  font-size: 14px;\n  font-weight: 600;\n  margin: 0 0 4px 0;\n  color: #6b7280;\n}\n\n.empty-state span {\n  font-size: 12px;\n}\n\n/* 默认分支区域 */\n.default-branch-section {\n  margin-top: 8px;\n}\n\n.default-branch-card {\n  border: 2px solid #e5e7eb;\n  border-radius: 8px;\n  padding: 16px;\n  background: white;\n  transition: all 0.2s;\n}\n\n.default-branch-card:hover {\n  border-color: #6b7280;\n  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);\n}\n\n.default-branch-header {\n  display: flex;\n  justify-content: space-between;\n  align-items: center;\n  margin-bottom: 16px;\n  padding-bottom: 12px;\n  border-bottom: 1px solid #f3f4f6;\n}\n\n.default-branch-title {\n  display: flex;\n  align-items: center;\n  gap: 8px;\n  font-size: 15px;\n  font-weight: 600;\n  color: #374151;\n}\n\n.default-icon {\n  font-size: 18px;\n}\n\n.default-branch-desc {\n  font-size: 12px;\n  color: #6b7280;\n  font-weight: 500;\n}\n\n/* 必填标记 */\n.required-mark {\n  color: #ef4444;\n  font-weight: bold;\n  margin-left: 4px;\n}\n\n/* 错误提示 */\n.error-hint {\n  font-size: 12px;\n  color: #ef4444;\n  margin-top: 6px;\n  padding: 6px 10px;\n  background: #fee2e2;\n  border-radius: 4px;\n  border-left: 3px solid #ef4444;\n  display: flex;\n  align-items: center;\n  gap: 4px;\n}\n\n/* 警告提示 */\n.warning-hint {\n  font-size: 12px;\n  color: #f59e0b;\n  margin-top: 6px;\n  padding: 6px 10px;\n  background: #fef3c7;\n  border-radius: 4px;\n  border-left: 3px solid #f59e0b;\n  display: flex;\n  align-items: center;\n  gap: 4px;\n}\n\n/* 无效配置的分支徽章 */\n.branch-number-badge.invalid {\n  background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);\n  animation: pulse 2s ease-in-out infinite;\n}\n\n@keyframes pulse {\n  0%, 100% {\n    opacity: 1;\n  }\n  50% {\n    opacity: 0.8;\n  }\n}\n\n.warning-icon {\n  font-size: 10px;\n  margin-left: 2px;\n  animation: shake 0.5s ease-in-out infinite;\n}\n\n@keyframes shake {\n  0%, 100% {\n    transform: translateX(0);\n  }\n  25% {\n    transform: translateX(-2px);\n  }\n  75% {\n    transform: translateX(2px);\n  }\n}\n</style>\n"
  },
  {
    "path": "apps/web-antd/src/packages/workflow-designer/properties/TestNodeProperty.vue",
    "content": "<script setup lang=\"ts\">\nimport { Input, InputNumber } from 'ant-design-vue'\nimport type { WorkflowInfo, WorkflowNode } from '../types/index.d'\n\ninterface Props {\n  workflow: WorkflowInfo\n  wfNode: WorkflowNode\n}\nconst props = defineProps<Props>()\nconst nodeConfig = props.wfNode.nodeConfig as any\n</script>\n\n<script lang=\"ts\">\n// 提供“测试节点”的默认配置，优先级高于集中配置表\nexport function getDefaultNodeConfig(_workflow: any) {\n  return { message: 'hello from Test', count: 1 }\n}\n</script>\n\n<template>\n  <div class=\"flex flex-col w-full\">\n    <div class=\"mt-2\">\n      <div class=\"text-sm mb-1\">消息</div>\n      <Input v-model:value=\"nodeConfig.message\" placeholder=\"例如：hello\" />\n    </div>\n    <div class=\"mt-4\">\n      <div class=\"text-sm mb-1\">次数</div>\n      <InputNumber v-model:value=\"nodeConfig.count\" :min=\"0\" :max=\"999\" class=\"w-full\" />\n    </div>\n  </div>\n</template>\n\n\n"
  },
  {
    "path": "apps/web-antd/src/packages/workflow-designer/properties/defaults.ts",
    "content": "import { v4 as uuidv4 } from 'uuid'\nimport type { WorkflowInfo } from '../types/index.d'\n\nexport type DefaultGetter = (workflow: WorkflowInfo) => any\n\nfunction createUuid() {\n  return uuidv4().replace(/-/g, '')\n}\n// 新建节点后的默认配置表\n// 统一默认配置表：键名为节点 name 的小写\nexport const propertyDefaultGetters: Record<string, DefaultGetter> = {\n  start: () => ({ prologue: '' }),\n  end: () => ({ result: '' }),\n  answer: () => ({ prompt: '', model_name: '' }),\n  template: () => ({ content: '' }),\n  keywordextractor: () => ({ top_n: 5, model_name: '' }),\n  faqextractor: () => ({ top_n: 5, model_name: '' }),\n  knowledgeretrieval: () => ({ knowledge_base_uuid: '', knowledge_base_name: '', score: 0.6, top_n: 3, is_strict: true, default_response: '' }),\n  dalle3: () => ({ prompt: '', size: '1024x1024', quality: 'standard' }),\n  tongyiwanx: () => ({ model_name: '', prompt: '', size: '1024*1024', seed: -1 }),\n  google: () => ({ query: '', country: 'cn', language: 'zh-cn', top_n: 5 }),\n  humanfeedback: () => ({ tip: '' }),\n  mailsend: () => ({ sender_type: 1, cc_mails: '', to_mails: '', subject: '', content: '', smtp: { host: '', port: 465 }, sender: { name: '', mail: '', password: '' } }),\n  httprequest: () => ({ method: 'GET', url: '', content_type: 'text/plain', headers: [{ name: 'Accept', value: '*/*' }], params: [], text_body: '', json_body: {}, form_data_body: [], form_urlencoded_body: [], body: {}, timeout: 10, retry_times: 0, clear_html: false }),\n  classifier: () => ({\n    model_name: '',\n    categories: [\n      { category_uuid: createUuid(), category_name: '', target_node_uuid: '' },\n      { category_uuid: createUuid(), category_name: '', target_node_uuid: '' },\n    ],\n  }),\n  // 条件分支：基于 workflow 的 Start 节点生成两条示例规则\n  switcher: (workflow) => {\n    const startNode = workflow.nodes.find((n) => n.wfComponent?.name === 'Start')\n    const firstParam = startNode?.inputConfig?.user_inputs?.[0]?.name || ''\n    return {\n      default_target_node_uuid: '',\n      cases: [\n        { uuid: createUuid(), operator: 'and', target_node_uuid: '', conditions: [{ node_uuid: startNode?.uuid || '', node_param_name: firstParam, operator: 'contains', value: '' }] },\n        { uuid: createUuid(), operator: 'and', target_node_uuid: '', conditions: [{ node_uuid: startNode?.uuid || '', node_param_name: firstParam, operator: 'contains', value: '' }] },\n      ],\n    }\n  },\n}\n\n\n"
  },
  {
    "path": "apps/web-antd/src/packages/workflow-designer/store/index.ts",
    "content": "import { defineStore } from 'pinia'\nimport { emptyWorkflowInfo } from '../utils/workflow-util'\n\n// 可由宿主应用注入路由跳转方法，提升包的可移植性\nlet navigateTo: null | ((name: string, params: any) => Promise<void> | void) = null\nexport function setWorkflowDesignerNavigator(fn: (name: string, params: any) => Promise<void> | void) {\n  navigateTo = fn\n}\n\nexport const useWfStore = defineStore('wf-store', {\n  state: (): any => {\n    return {\n      showCreateOrEditView: false,\n      createOrEditWfUuid: '',\n      selectedType: 'mine',\n      activeWorkflowInfo: emptyWorkflowInfo(),\n      activeUuid: 'default',\n      wfComponents: [],\n      wfUuidToUIWorkflow: new Map<string, any>(),\n      myWorkflows: [],\n      publicWorkflows: [],\n      loadingMyWorkflows: false,\n      loadingPublicWorkflows: false,\n      wfUuidToWfRuntimeLoading: new Map<string, boolean>(), // is loading workflow instances\n      wfUuidToWfRuntimes: new Map<string, any[]>(),\n      operators: [],\n      submitting: false,\n    }\n  },\n\n  getters: {\n    getWfRuntimes(state: any) {\n      return (wfUuid: string) => {\n        const records = state.wfUuidToWfRuntimes.get(wfUuid)\n        if (records)\n          return records\n\n        return []\n      }\n    },\n    getStartOrFirstNode(state: any) {\n      return (wfUuid: string) => {\n        const wf = this.getWorkflowInfo(wfUuid)\n        if (!wf)\n          return undefined\n        const start = wf.nodes.find((item: any) => item.wfComponent.name === 'Start')\n        if (start)\n          return start\n\n        return wf.nodes[0]\n      }\n    },\n    getStartNode(state: any) {\n      return (wfUuid: string) => {\n        const wf = this.getWorkflowInfo(wfUuid)\n        if (!wf)\n          return undefined\n        return wf.nodes.find((item: any) => item.wfComponent && item.wfComponent.name === 'Start')\n      }\n    },\n    getStartNodeByWfId(state: any) {\n      return (wfId: string) => {\n        const wf = this.getWorkflowInfoById(wfId)\n        if (!wf)\n          return undefined\n        return wf.nodes.find((item: any) => item.wfComponent.name === 'Start')\n      }\n    },\n    getWorkflowInfo(state: any) {\n      return (wfUuid: string) => {\n        const wf = state.myWorkflows.find((item: any) => item.uuid === wfUuid)\n        if (wf)\n          return wf\n        return state.publicWorkflows.find((item: any) => item.uuid === wfUuid)\n      }\n    },\n    getWorkflowInfoById(state: any) {\n      return (id: string) => {\n        const wf = state.myWorkflows.find((item: any) => item.id === id)\n        if (wf)\n          return wf\n        return state.publicWorkflows.find((item: any) => item.id === id)\n      }\n    },\n    getWfComponent(state: any) {\n      return (name: string) => {\n        return state.wfComponents.find((item: any) => item.name === name)\n      }\n    },\n    getOperatorDesc(state: any) {\n      return (name: string) => {\n        return state.operators.find((item: any) => item.name === name)?.desc || ''\n      }\n    },\n    getWfRuntime(state: any) {\n      return (wfRuntimeUuid: string) => {\n        let wfRuntime = null\n        for (const rts of state.wfUuidToWfRuntimes.values()) {\n          wfRuntime = rts.find((item: any) => item.uuid === wfRuntimeUuid)\n          if (wfRuntime)\n            break\n        }\n        if (!wfRuntime) {\n          console.log(`wfRuntime not found: ${wfRuntimeUuid}`)\n          return null\n        }\n        return wfRuntime\n      }\n    },\n    getRuntimeNode(state: any) {\n      return (wfRuntimeUuid: string, runtimeNodeUuid: string) => {\n        const wfRuntime = this.getWfRuntime(wfRuntimeUuid)\n        if (!wfRuntime)\n          return null\n\n        const runtimeNode = (wfRuntime.nodes as any[]).find((item: any) => item.uuid === runtimeNodeUuid)\n        if (!runtimeNode)\n          console.log(`runtimeNode not found: ${runtimeNodeUuid}`)\n\n        return runtimeNode\n      }\n    },\n  },\n\n  actions: {\n    setShowCreateView(status: boolean, wfUuid: string) {\n      console.log(`setShowCreateView: ${status}, ${wfUuid}`)\n      this.showCreateOrEditView = status\n      this.createOrEditWfUuid = wfUuid\n    },\n    setOperators(operators: any[]) {\n      this.operators = operators\n    },\n    setActive(wfUuid: string) {\n      this.activeUuid = wfUuid\n      const selected = this.getWorkflowInfo(wfUuid)\n      if (selected)\n        this.activeWorkflowInfo = selected\n      else\n        console.log(`setActive: ${wfUuid} workflow not found`)\n    },\n    setActiveAndGo(wfUuid: string, defaultViewType?: string) {\n      this.setActive(wfUuid)\n      this.reloadRoute(wfUuid, defaultViewType)\n    },\n    setLoadingMyWorkflows(status: boolean) {\n      this.loadingMyWorkflows = status\n    },\n    setLoadingPublicWorkflows(status: boolean) {\n      this.loadingPublicWorkflows = status\n    },\n    setLoadingRuntimes(currKbUuid: string, status: boolean) {\n      this.wfUuidToWfRuntimeLoading.set(currKbUuid, status)\n    },\n    setWorkflowComponents(components: any[]) {\n      this.wfComponents = components\n    },\n    addWorkflowAndActive(info: any) {\n      this.initWorkflowFields(info)\n      this.myWorkflows.unshift(info)\n      this.setActiveAndGo(info.uuid, 'workflowDefine')\n    },\n    appendWorkflows(infos: any[], isMine: boolean) {\n      const workflows = isMine ? this.myWorkflows : this.publicWorkflows\n      infos.forEach((workflow: any) => {\n        if (workflows.findIndex((wf: any) => wf.uuid === workflow.uuid) !== -1)\n          return\n\n        this.initWorkflowFields(workflow)\n        workflows.push(workflow)\n      })\n    },\n    updateBaseInfo(uuid: string, info: { title: string; remark: string; isPublic: boolean }) {\n      this.myWorkflows.forEach((item: any) => {\n        if (item.uuid === uuid)\n          Object.assign(item, { title: info.title, remark: info.remark, isPublic: info.isPublic })\n      })\n    },\n    initWorkflowFields(workflow: any) {\n      workflow.nodes.forEach((node: any) => {\n        node.workflowUuid = workflow.uuid\n        node.sourceHandleIds = []\n        const wfComponent = this.wfComponents.find((component: any) => component.id === node.workflowComponentId)\n        if (wfComponent)\n          node.wfComponent = wfComponent\n\n        if (!node.inputConfig)\n          node.inputConfig = { user_inputs: [], ref_inputs: [] }\n      })\n      workflow.edges.forEach((edge: any) => {\n        edge.workflowUuid = workflow.uuid\n      })\n      workflow.deleteEdges = []\n      workflow.deleteNodes = []\n    },\n    updateNodesAndEdges(uuid: string, info: any) {\n      this.myWorkflows.forEach((item: any) => {\n        if (item.uuid === uuid) {\n          item.nodes.forEach((node: any) => {\n            const nodeInfo = info.nodes.find((n: any) => n.uuid === node.uuid)\n            if (nodeInfo)\n              Object.assign(node, { ...nodeInfo })\n          })\n          item.edges.forEach((edge: any) => {\n            const edgeInfo = info.edges.find((e: any) => e.uuid === edge.uuid)\n            if (edgeInfo)\n              Object.assign(edge, { ...edgeInfo })\n          })\n        }\n      })\n    },\n    updateNodesAndEdgesId(uuid: string, updatedWorkflow: any) {\n      this.myWorkflows.forEach((item: any) => {\n        if (item.uuid === uuid) {\n          item.nodes.forEach((node: any) => {\n            if (!node.id) {\n              const updatedNodeInfo = updatedWorkflow.nodes.find((updatedNode: any) => updatedNode.uuid === node.uuid)\n              if (updatedNodeInfo)\n                node.id = updatedNodeInfo.id\n            }\n          })\n          item.edges.forEach((edge: any) => {\n            if (!edge.id) {\n              const edgeInfo = updatedWorkflow.edges.find((updatedEdge: any) => updatedEdge.uuid === edge.uuid)\n              if (edgeInfo)\n                edge.id = edgeInfo.id\n            }\n          })\n        }\n      })\n    },\n    setWorkflowPublic(uuid: string, publicOrNot: boolean) {\n      const idx = this.myWorkflows.findIndex((item: { uuid: string }) => item.uuid === uuid)\n      if (idx !== -1)\n        this.myWorkflows[idx].isPublic = publicOrNot\n      if (publicOrNot)\n        this.publicWorkflows.push(this.myWorkflows[idx])\n      else\n        this.publicWorkflows = this.publicWorkflows.filter((item: { uuid: string }) => item.uuid !== uuid)\n    },\n    deleteWorkflow(uuid: string) {\n      const idx = this.myWorkflows.findIndex((item: { uuid: string }) => item.uuid === uuid)\n      if (idx !== -1)\n        this.myWorkflows.splice(idx, 1)\n    },\n    updateWfNodeTitle(wfUuid: string, nodeUuid: string, newNodeTitle: string) {\n      this.getWorkflowInfo(wfUuid)?.nodes.forEach((node: any) => {\n        if (node.uuid === nodeUuid)\n          node.title = newNodeTitle\n      })\n    },\n    updateWfNode(wfUuid: string, nodeUuid: string, newNode: any) {\n      this.getWorkflowInfo(wfUuid)?.nodes.forEach((node: any) => {\n        if (node.uuid === nodeUuid)\n          Object.assign(node, { ...newNode })\n      })\n    },\n    addRefInputToNode(wfUuid: string, nodeUuid: string, newInput: any) {\n      this.getWorkflowInfo(wfUuid)?.nodes.forEach((node: any) => {\n        if (node.uuid === nodeUuid)\n          node.inputConfig.ref_inputs.push(newInput)\n      })\n    },\n    addUserInputToNode(wfUuid: string, nodeUuid: string, newInput: any) {\n      this.getWorkflowInfo(wfUuid)?.nodes.forEach((node: any) => {\n        if (node.uuid === nodeUuid)\n          node.inputConfig.user_inputs.push(newInput)\n      })\n    },\n    deleteRefInput(wfUuid: string, nodeUuid: string, idx: number) {\n      this.getWorkflowInfo(wfUuid)?.nodes.forEach((node: any) => {\n        if (node.uuid === nodeUuid)\n          node.inputConfig.ref_inputs.splice(idx, 1)\n      })\n    },\n    deleteUserInput(wfUuid: string, nodeUuid: string, idx: number) {\n      this.getWorkflowInfo(wfUuid)?.nodes.forEach((node: any) => {\n        if (node.uuid === nodeUuid)\n          node.inputConfig.user_inputs.splice(idx, 1)\n      })\n    },\n    initWfRuntime(wfRuntime: any) {\n      if (!wfRuntime.input)\n        wfRuntime.input = {}\n\n      if (!wfRuntime.output)\n        wfRuntime.output = {}\n\n      wfRuntime.nodes = []\n    },\n    setWfRuntimes(wfUuid: string, wfRuntimes: any[]) {\n      wfRuntimes.forEach((wfRuntime) => {\n        this.initWfRuntime(wfRuntime)\n      })\n      this.wfUuidToWfRuntimes.set(wfUuid, wfRuntimes.reverse())\n    },\n    updateWfRuntimePrologue(wfRuntimeUuid: string, prologue: string) {\n      const wfRuntime = this.getWfRuntime(wfRuntimeUuid)\n      if (!wfRuntime)\n        return\n      if (prologue)\n        wfRuntime.prologue = prologue\n    },\n    setWfRuntimeNodes(wfRuntimeUuid: string, nodes: any[]) {\n      const wfRuntime = this.getWfRuntime(wfRuntimeUuid)\n      if (!wfRuntime)\n        return\n\n      const wfNodes = this.getWorkflowInfoById(wfRuntime.workflowId)?.nodes as any[]\n      if (!wfNodes) {\n        console.error('setWfRuntimeNodes wfNodes not found')\n        return\n      }\n      nodes.forEach((node) => {\n        if (!node.input)\n          node.input = {}\n        if (!node.output)\n          node.output = {}\n        const wfNode = wfNodes.find((n: any) => n.id === node.nodeId)\n        if (!wfNode) {\n          console.error('setWfRuntimeNodes wfNode not found')\n        } else {\n          node.nodeUuid = wfNode.uuid\n          node.nodeTitle = wfNode.title\n          node.wfComponent = wfNode.wfComponent\n        }\n        node.wfRuntimeUuid = wfRuntime.uuid\n        wfRuntime.nodes.push(node)\n      })\n    },\n    unshiftWfRuntimes(wfUuid: string, wfRuntimes: any[]) {\n      wfRuntimes.forEach((wfRuntime) => {\n        this.initWfRuntime(wfRuntime)\n        console.log('appendWfRuntime', wfRuntime)\n      })\n      const records = this.wfUuidToWfRuntimes.get(wfUuid)\n      if (records)\n        records.unshift(...wfRuntimes.reverse())\n      else\n        this.wfUuidToWfRuntimes.set(wfUuid, wfRuntimes.reverse())\n    },\n    appendWfRuntimes(wfUuid: string, wfRuntimes: any[]) {\n      wfRuntimes.forEach((wfRuntime) => {\n        this.initWfRuntime(wfRuntime)\n        console.log('appendWfRuntime', wfRuntime)\n      })\n      const records = this.wfUuidToWfRuntimes.get(wfUuid)\n      if (records)\n        records.push(...wfRuntimes.reverse())\n      else\n        this.wfUuidToWfRuntimes.set(wfUuid, wfRuntimes.reverse())\n    },\n    // 增加节点运行时信息\n    appendRuntimeNode(wfRuntimeUuid: string, runtimeNode: any) {\n      const wfRuntime = this.getWfRuntime(wfRuntimeUuid)\n      if (!wfRuntime)\n        return\n\n      const wfNode = (this.getWorkflowInfoById(wfRuntime.workflowId)?.nodes as any[])?.find((node: any) => node.id === runtimeNode.nodeId)\n      if (wfNode) {\n        runtimeNode.nodeUuid = wfNode.uuid\n        runtimeNode.nodeTitle = wfNode.title\n        runtimeNode.wfComponent = wfNode.wfComponent\n      } else {\n        console.log(`wfNode not found: ${runtimeNode.nodeId}`)\n      }\n      runtimeNode.wfRuntimeUuid = wfRuntime.uuid\n      if (!runtimeNode.input)\n        runtimeNode.input = {}\n\n      if (!runtimeNode.output)\n        runtimeNode.output = {}\n\n      wfRuntime.nodes.push(runtimeNode)\n    },\n    appendInputToRuntimeNode(wfRuntimeUuid: string, runtimeNodeUuid: string, inputJson: string) {\n      const runtimeNode = this.getRuntimeNode(wfRuntimeUuid, runtimeNodeUuid)\n      if (runtimeNode) {\n        // inputJson: {\"name\": \"input1\", \"content\":{\"value\": \"input1\", type: 1}}\n        const obj = JSON.parse(inputJson)\n        runtimeNode.input[obj.name] = obj.content\n      }\n    },\n    appendOutputToRuntimeNode(wfRuntimeUuid: string, runtimeNodeUuid: string, outputJson: string) {\n      const runtimeNode = this.getRuntimeNode(wfRuntimeUuid, runtimeNodeUuid)\n      if (runtimeNode) {\n        const obj = JSON.parse(outputJson)\n        runtimeNode.output[obj.name] = obj.content\n      }\n    },\n    appendChunkToRuntimeNode(wfRuntimeUuid: string, runtimeNodeUuid: string, chunk: string) {\n      const runtimeNode = this.getRuntimeNode(wfRuntimeUuid, runtimeNodeUuid)\n      // runtimeNode.output 格式： {output:'default output', 'output_name1': 'output_content1'}\n      if (runtimeNode)\n        runtimeNode.output.output = runtimeNode.output.output + chunk\n    },\n    deleteWfRuntime(wfUuid: string, wfRuntimeUuid: string) {\n      const wfRuntimes = this.wfUuidToWfRuntimes.get(wfUuid)\n      if (wfRuntimes) {\n        const idx = wfRuntimes.findIndex((inst: { uuid: string }) => inst.uuid === wfRuntimeUuid)\n        if (idx > -1)\n          wfRuntimes.splice(idx, 1)\n      }\n    },\n    updateSuccess(wfUuid: string, wfRuntimeUuid: string, outputJson: string) {\n      if (!wfRuntimeUuid) {\n        console.log('updateSuccess instUuid is empty')\n        return\n      }\n      const wfRuntimes = this.wfUuidToWfRuntimes.get(wfUuid)\n      if (wfRuntimes) {\n        const inst = wfRuntimes.find((inst: { uuid: string }) => inst.uuid === wfRuntimeUuid)\n        if (inst) {\n          inst.status = 3\n          try {\n            inst.output = JSON.parse(outputJson)\n          } catch (e) {\n            console.error(e)\n            console.log('outputJson is not json', outputJson)\n          }\n        }\n      }\n    },\n    updateErrorMsg(wfUuid: string, wfRuntimeUuid: string, errorMsg: string) {\n      if (!wfRuntimeUuid) {\n        console.log('updateSuccess instUuid is empty')\n        return\n      }\n      const wfRuntimes = this.wfUuidToWfRuntimes.get(wfUuid)\n      if (wfRuntimes) {\n        const inst = wfRuntimes.find((inst: { uuid: string }) => inst.uuid === wfRuntimeUuid)\n        if (inst) {\n          inst.status = 4\n          inst.statusRemark = errorMsg || 'error'\n        }\n      }\n    },\n    clearWfRuntimes(wfUuid: string) {\n      this.wfUuidToWfRuntimes.set(wfUuid, [])\n    },\n    deleteNode(wfUuid: string, nodeUuid: string) {\n      // Delete node\n      const wf = this.getWorkflowInfo(wfUuid)\n      if (!wf) {\n        console.log('deleteNode wf not found')\n        return\n      }\n\n      wf.deleteNodes.push(nodeUuid)\n\n      const idx = wf.nodes.findIndex((node: { uuid: string }) => node.uuid === nodeUuid)\n      if (idx > -1)\n        wf.nodes.splice(idx, 1)\n\n      this._deleteEdgesByNodeUuid(wf, nodeUuid)\n      this._deleteUiNode(wfUuid, nodeUuid)\n    },\n    // 删除节点时，删除与之相关的边\n    _deleteEdgesByNodeUuid(workflow: any, deletedNodeUuid: string) {\n      const edges = workflow.edges.filter((edge: { sourceNodeUuid: string; targetNodeUuid: string }) => edge.sourceNodeUuid === deletedNodeUuid || edge.targetNodeUuid === deletedNodeUuid)\n      edges.forEach((edge: { uuid: string }) => {\n        const edgeIdx = workflow.edges.findIndex(\n          (item: { uuid: string }) => item.uuid === edge.uuid,\n        )\n        if (edgeIdx > -1)\n          workflow.edges.splice(edgeIdx, 1)\n\n        workflow.deleteEdges.push(edge.uuid)\n\n        this._deleteUiEdge(workflow.uuid, edge.uuid)\n      })\n    },\n    deleteEdge(wfUuid: string, edgeUuid: string) {\n      // Delete edge\n      const wf = this.getWorkflowInfo(wfUuid)\n      if (!wf) {\n        console.log('deleteEdge wf not found')\n        return\n      }\n      wf.deleteEdges.push(edgeUuid)\n      const idx = wf.edges.findIndex((edge: { uuid: string }) => edge.uuid === edgeUuid)\n      if (idx > -1)\n        wf.edges.splice(idx, 1)\n\n      this._deleteUiEdge(wfUuid, edgeUuid)\n    },\n    _deleteUiNode(wfUuid: string, nodeUuid: string) {\n      const uiWorkflow = this.wfUuidToUIWorkflow.get(wfUuid)\n      if (!uiWorkflow) {\n        console.log('_deleteUiNode uiWorkflow not found')\n        return\n      }\n      const idx = uiWorkflow.nodes.findIndex((node: { id: string }) => node.id === nodeUuid)\n      if (idx > -1)\n        uiWorkflow.nodes.splice(idx, 1)\n    },\n    _deleteUiEdge(wfUuid: string, edgeId: string) {\n      const uiWorkflow = this.wfUuidToUIWorkflow.get(wfUuid)\n      if (!uiWorkflow) {\n        console.log('_deleteUiEdge uiWorkflow not found')\n        return\n      }\n      const idx = uiWorkflow.edges.findIndex((edge: { id: string }) => edge.id === edgeId)\n      if (idx > -1)\n        uiWorkflow.edges.splice(idx, 1)\n    },\n    async reloadRoute(uuid?: string, defaultViewType?: string) {\n      const viewType = !defaultViewType ? 'instanceList' : defaultViewType\n      if (navigateTo) {\n        await navigateTo('WfDetail', { uuid, viewType })\n      } else {\n        console.warn('[workflow-designer] 未注入导航器，无法执行路由跳转', { uuid, viewType })\n      }\n    },\n  },\n})"
  },
  {
    "path": "apps/web-antd/src/packages/workflow-designer/types/index.d.ts",
    "content": "export interface WorkflowInfo {\n  uuid: string\n  title: string\n  remark?: string\n  userUuid?: string\n  isPublic?: boolean\n  nodes: WorkflowNode[]\n  edges: WorkflowEdge[]\n  deleteNodes?: string[]\n  deleteEdges?: string[]\n}\n\nexport interface WorkflowComponent {\n  name: string\n  title: string\n  remark?: string\n  isEnable?: boolean\n}\n\nexport interface NodeIODefinition {\n  uuid: string\n  type: number\n  name: string\n  title: string\n  required: boolean\n  multiple?: boolean\n  limit?: number\n}\n\nexport interface NodeIOConfig {\n  user_inputs: NodeIODefinition[]\n  ref_inputs: any[]\n}\n\nexport interface WorkflowNode {\n  uuid: string\n  title: string\n  workflowUuid: string\n  wfComponent?: WorkflowComponent  // 可选，前端可能没有\n  workflowComponentId?: number      // 后端返回的组件ID（新增节点可缺省，保存时补齐）\n  inputConfig: NodeIOConfig\n  nodeConfig: Record<string, any>\n  outputConfig: Record<string, any>\n  positionX: number\n  positionY: number\n}\n\nexport interface WorkflowEdge {\n  id?: string\n  uuid: string\n  workflowUuid: string\n  sourceNodeUuid: string\n  sourceHandle?: string\n  targetNodeUuid: string\n}\n\nexport interface UIWorkflowNodePosition { x: number; y: number }\n\nexport interface UIWorkflow {\n  nodes: any[]\n  edges: any[]\n}\n\n\n\n"
  },
  {
    "path": "apps/web-antd/src/packages/workflow-designer/utils/operators.ts",
    "content": "/**\n * 条件运算符配置\n */\n\nexport interface OperatorOption {\n  label: string\n  value: string\n  description?: string\n}\n\n// 条件运算符列表\n// 注意: value 值必须与后端 OperatorEnum 完全匹配\nexport const conditionOperators: OperatorOption[] = [\n  { label: '包含', value: 'contains', description: '文本包含' },\n  { label: '不包含', value: 'not contains', description: '文本不包含' },\n  { label: '开始内容是', value: 'start with', description: '以指定内容开始' },\n  { label: '结束内容是', value: 'end with', description: '以指定内容结束' },\n  { label: '为空', value: 'empty', description: '值为空或未定义' },\n  { label: '不为空', value: 'not empty', description: '值存在且不为空' },\n  { label: '等于', value: '=', description: '完全相等' },\n  { label: '不等于', value: '!=', description: '不相等' },\n  { label: '大于', value: '>', description: '数值大于' },\n  { label: '大于或等于', value: '>=', description: '数值大于或等于' },\n  { label: '小于', value: '<', description: '数值小于' },\n  { label: '小于或等于', value: '<=', description: '数值小于或等于' },\n]\n\n// 逻辑运算符列表\nexport const logicOperators: OperatorOption[] = [\n  { label: '且(AND)', value: 'and', description: '所有条件都满足' },\n  { label: '或(OR)', value: 'or', description: '任一条件满足' },\n]\n\n/**\n * 获取运算符显示名称\n */\nexport function getOperatorLabel(value: string): string {\n  const operator = conditionOperators.find(op => op.value === value)\n  return operator?.label || value\n}\n\n/**\n * 获取逻辑运算符显示名称\n */\nexport function getLogicOperatorLabel(value: string): string {\n  const operator = logicOperators.find(op => op.value === value)\n  return operator?.label || value\n}\n"
  },
  {
    "path": "apps/web-antd/src/packages/workflow-designer/utils/workflow-util.ts",
    "content": "import { v4 as uuidv4 } from 'uuid'\nimport type { WorkflowInfo, WorkflowNode, WorkflowEdge, WorkflowComponent, UIWorkflow } from '../types/index.d'\nimport { propertyDefaultGetters } from '../properties/defaults'\n\nfunction deepClone<T>(value: T): T {\n  try { return structuredClone(value) } catch { return JSON.parse(JSON.stringify(value)) }\n}\n\nexport function emptyWorkflowInfo(): WorkflowInfo {\n  return {\n    uuid: 'default',\n    title: '',\n    nodes: [],\n    edges: [],\n    deleteNodes: [],\n    deleteEdges: [],\n  }\n}\n\nexport function emptyWorkflowNode(): WorkflowNode {\n  return {\n    uuid: createUuid(),\n    title: '',\n    workflowUuid: 'default',\n    wfComponent: { name: 'Start', title: '开始' },\n    inputConfig: { user_inputs: [], ref_inputs: [] },\n    nodeConfig: {},\n    outputConfig: {},\n    positionX: 0,\n    positionY: 0,\n  }\n}\n\nfunction createUuid() {\n  return uuidv4().replace(/-/g, '')\n}\n\n// 默认配置：优先读取面板自定义导出，其次读取集中配置表，最后内置表\nconst propertyModules = import.meta.glob('../properties/*NodeProperty.vue', { eager: true }) as Record<string, any>\nfunction toPropertyKey(path: string) {\n  const file = path.substring(path.lastIndexOf('/') + 1)\n  return file.replace(/NodeProperty\\.vue$/, '').toLowerCase()\n}\nconst propertySelfGetters: Record<string, (workflow: WorkflowInfo) => any> = {}\nfor (const [p, mod] of Object.entries(propertyModules)) {\n  const key = toPropertyKey(p)\n  const getter = (mod as any)?.getDefaultNodeConfig as ((wf: WorkflowInfo) => any) | undefined\n  if (typeof getter === 'function') propertySelfGetters[key] = getter\n}\n\nfunction getDefaultNodeConfig(name: string, workflow: WorkflowInfo) {\n  const key = (name || '').toLowerCase()\n  // 1) 面板自身导出\n  const selfGetter = propertySelfGetters[key]\n  if (selfGetter) return selfGetter(workflow)\n  // 2) 集中配置表\n  const tableGetter = propertyDefaultGetters[key]\n  if (tableGetter) return tableGetter(workflow)\n\n  // 3) 内置简易默认表,可在defaults.ts中添加其他默认配置,不建议使用该内置配置表\n  // const simpleDefaults: Record<string, any> = {\n  //   start: { prologue: '' },\n  //   end: { result: '' },\n  //   answer: { prompt: '', model_name: '' },\n  //   template: { content: '' },\n  //   keywordextractor: { top_n: 5, model_name: '' },\n  //   faqextractor: { top_n: 5, model_name: '' },\n  //   knowledgeretrieval: { knowledge_base_uuid: '', knowledge_base_name: '', score: 0.6, top_n: 3, is_strict: true, default_response: '' },\n  //   dalle3: { prompt: '', size: '1024x1024', quality: 'standard' },\n  //   tongyiwanx: { model_name: '', prompt: '', size: '1024*1024', seed: -1 },\n  //   google: { query: '', country: 'cn', language: 'zh-cn', top_n: 5 },\n  //   humanfeedback: { tip: '' },\n  //   mailsend: { sender_type: 1, cc_mails: '', to_mails: '', subject: '', content: '', smtp: { host: '', port: 465 }, sender: { name: '', mail: '', password: '' } },\n  //   httprequest: { method: 'GET', url: '', content_type: 'text/plain', headers: [{ name: 'Accept', value: '*/*' }], params: [], text_body: '', json_body: {}, form_data_body: [], form_urlencoded_body: [], body: {}, timeout: 10, retry_times: 0, clear_html: false },\n  //   classifier: { categories: [ { category_uuid: createUuid(), category_name: '', target_node_uuid: '' }, { category_uuid: createUuid(), category_name: '', target_node_uuid: '' } ] },\n  // }\n\n  // return simpleDefaults[key] ?? {}\n}\n\nexport function createNewNode(\n  workflow: WorkflowInfo,\n  uiWorkflow: UIWorkflow,\n  component: WorkflowComponent,\n  position: { x: number; y: number },\n) {\n  const newWfNode = emptyWorkflowNode()\n  newWfNode.uuid = createUuid()\n  newWfNode.title = component.title\n  newWfNode.workflowUuid = workflow.uuid\n  newWfNode.wfComponent = component\n  newWfNode.inputConfig = { user_inputs: [], ref_inputs: [] }\n  // 使用映射初始化最小可用的 nodeConfig，特殊项（如 switcher）按需读取 workflow\n  newWfNode.nodeConfig = deepClone(getDefaultNodeConfig(component.name, workflow) || {})\n  newWfNode.outputConfig = {}\n  newWfNode.positionX = position.x\n  newWfNode.positionY = position.y\n\n  workflow.nodes.push(newWfNode)\n  uiWorkflow.nodes.push(wfNodeToUiNode(newWfNode))\n}\n\nexport function createNewEdge(params: {\n  workflow: WorkflowInfo\n  uiWorkflow: UIWorkflow\n  source: string\n  sourceHandle: string\n  target: string\n}) {\n  const wfEdge: WorkflowEdge = {\n    id: '',\n    uuid: uuidv4().replace(/-/g, ''),\n    workflowUuid: params.workflow.uuid,\n    sourceNodeUuid: params.source,\n    sourceHandle: params.sourceHandle,\n    targetNodeUuid: params.target,\n  }\n  params.workflow.edges.push(wfEdge)\n  if (params.target) {\n    const uiEdge = {\n      id: wfEdge.uuid,\n      source: wfEdge.sourceNodeUuid,\n      target: wfEdge.targetNodeUuid,\n      type: 'special',\n      animated: true,\n      sourceHandle: params.sourceHandle ? params.sourceHandle : undefined,\n      data: wfEdge,\n    }\n    params.uiWorkflow.edges.push(uiEdge)\n  }\n}\n\nexport function updateEdgeBySourceHandle(params: {\n  workflow: WorkflowInfo\n  uiWorkflow: UIWorkflow\n  source: string\n  sourceHandle: string\n  target: string\n}) {\n  const wfEdge = params.workflow.edges.find((item) => item.sourceHandle === params.sourceHandle)\n  if (!wfEdge) return\n  wfEdge.targetNodeUuid = params.target\n  const idx = (params.uiWorkflow.edges as any[]).findIndex((item: any) => item.source === params.source && item.sourceHandle === params.sourceHandle)\n  if (idx > -1) (params.uiWorkflow.edges as any[]).splice(idx, 1)\n  const uiEdge = {\n    id: wfEdge.uuid,\n    source: wfEdge.sourceNodeUuid,\n    target: wfEdge.targetNodeUuid,\n    animated: true,\n    sourceHandle: params.sourceHandle,\n  }\n  ;(params.uiWorkflow.edges as any[]).push(uiEdge)\n}\n\nexport function deleteEdgesBySourceHandle(\n  workflow: WorkflowInfo,\n  uiWorkflow: UIWorkflow,\n  source: string,\n  sourceHandle: string,\n) {\n  const edgeIndex = workflow.edges.findIndex((edge) => {\n    const hit = edge.sourceNodeUuid === source && edge.sourceHandle === sourceHandle\n    if (hit && !workflow.deleteEdges) workflow.deleteEdges = []\n    if (hit) workflow.deleteEdges!.push(edge.uuid)\n    return hit\n  })\n  if (edgeIndex !== -1) workflow.edges.splice(edgeIndex, 1)\n  const uiEdgeIndex = (uiWorkflow.edges as any[]).findIndex((edge: any) => edge.sourceNodeUuid === source && edge.sourceHandle === sourceHandle)\n  if (uiEdgeIndex !== -1) (uiWorkflow.edges as any[]).splice(uiEdgeIndex, 1)\n}\n\nfunction wfNodeToUiNode(node: WorkflowNode) {\n  return {\n    id: node.uuid,\n    type: node.wfComponent?.name.toLowerCase() ?? '',\n    data: node,\n    position: { x: node.positionX, y: node.positionY },\n  }\n}\n\nexport function getNameByInputType(type: number) {\n  switch (type) {\n    case 1: return '文本'\n    case 2: return '数字'\n    case 3: return '下拉选项'\n    case 4: return '文件列表'\n    default: return 'Unknown'\n  }\n}\n\nexport function getIconByComponentName(name: string) {\n  switch (name.toLowerCase()) {\n    case 'answer': return 'carbon:question-answering'\n    case 'classifier': return 'carbon:type-pattern'\n    case 'knowledgeretrieval': return 'carbon:connect-target'\n    case 'documentextractor': return 'carbon:ibm-knowledge-catalog-standard'\n    case 'keywordextractor': return 'carbon:api-key'\n    case 'faqextractor': return 'fluent-mdl2:book-answers'\n    case 'switcher': return 'oui:logstash-if'\n    case 'template': return 'carbon:prompt-template'\n    case 'dalle3': return 'solar:pallete-2-linear'\n    case 'tongyiwanx': return 'solar:pallete-2-linear'\n    case 'google': return 'ri:google-line'\n    case 'humanfeedback': return 'covid:transmission-virus-human-transmit-2'\n    case 'mailsend': return 'carbon:mail-all'\n    case 'httprequest': return 'carbon:http'\n    case 'end': return 'carbon:closed-caption'\n    case 'start': return 'carbon:play-outline'\n    default: return ''\n  }\n}\n\nexport function getIconClassByComponentName(name: string) {\n  switch (name.toLowerCase()) {\n    case 'answer': return 'text-green-800'\n    case 'classifier': return 'text-violet-900'\n    case 'knowledgeretrieval': return 'text-stone-900'\n    case 'documentextractor': return 'text-rose-900'\n    case 'keywordextractor': return 'text-cyan-900'\n    case 'faqextractor': return 'text-teal-600'\n    case 'switcher': return 'text-yellow-900'\n    case 'template': return 'text-sky-800'\n    case 'dalle3': return 'text-fuchsia-700'\n    case 'tongyiwanx': return 'text-fuchsia-700'\n    case 'google': return 'text-emerald-900'\n    case 'humanfeedback': return 'text-zinc-800'\n    case 'mailsend': return 'text-amber-800'\n    case 'httprequest': return 'text-slate-800'\n    case 'end': return 'text-orange-800'\n    case 'start': return 'text-blue-900'\n    default: return ''\n  }\n}\n\n\n"
  },
  {
    "path": "apps/web-antd/src/preferences.ts",
    "content": "import { defineOverridesPreferences } from '@vben/preferences';\n\n/**\n * @description 项目配置文件\n * 只需要覆盖项目中的一部分配置，不需要的配置不用覆盖，会自动使用默认配置\n * !!! 更改配置后请清空缓存，否则可能不生效\n */\nexport const overridesPreferences = defineOverridesPreferences({\n  // overrides\n  app: {\n    /**\n     * 不要动这里  后端路由模式\n     */\n    accessMode: 'backend',\n    /**\n     * 不需要refresh token 由后端处理\n     */\n    enableRefreshToken: false,\n    /**\n     * 这里可以设置默认头像 url链接或vite导入的图片链接\n     */\n    // defaultAvatar: '',\n    /**\n     * 在这里设置应用标题\n     */\n    name: import.meta.env.VITE_APP_TITLE,\n    /**\n     * 不支持modal模式 需要改动的地方太多\n     * 1. 正常重新登录后不会再触发接口请求 即触发登录超时的页面为空数据\n     * 2. 切换租户登录后不会重新加载菜单\n     */\n    // loginExpiredMode: 'modal',\n  },\n  footer: {\n    /**\n     * 不显示footer\n     */\n    enable: false,\n  },\n  tabbar: {\n    /**\n     * 标签tab 持久化 关闭\n     */\n    persist: false,\n    // styleType: 'card',\n  },\n  theme: {\n    /**\n     * 浅色sidebar\n     */\n    semiDarkSidebar: false,\n    /**\n     * 圆角大小 换算比例为1.6px = 0.1radius\n     * 这里为6px 与antd保持一致\n     */\n    radius: '0.375',\n  },\n  /**\n   * !!! 更改配置后请清空浏览器缓存\n   * 在这里更换logo\n   * source可选值：\n   * 1. 本地public目录下的图片 需要加上/ 比如：/logo.png\n   * 2. 网络图片链接\n   * 3. vite导入的图片 import xxx from 'xxx.png'\n   *\n   * !!! 更改配置后请清空浏览器缓存\n   */\n  // logo: {\n  //   enable: true,\n  //   source: '',\n  // },\n});\n"
  },
  {
    "path": "apps/web-antd/src/router/access.ts",
    "content": "import type {\n  ComponentRecordType,\n  GenerateMenuAndRoutesOptions,\n  RouteMeta,\n  RouteRecordStringComponent,\n} from '@vben/types';\n\nimport type { Menu } from '#/api';\n\nimport { generateAccessible } from '@vben/access';\nimport { preferences } from '@vben/preferences';\n\nimport { message } from 'ant-design-vue';\nimport { cloneDeep } from 'lodash-es';\n\nimport { getAllMenusApi } from '#/api';\nimport { BasicLayout, IFrameView } from '#/layouts';\nimport { $t } from '#/locales';\n\nimport { localMenuList } from './routes/local';\n\nconst forbiddenComponent = () => import('#/views/_core/fallback/forbidden.vue');\nconst NotFoundComponent = () => import('#/views/_core/fallback/not-found.vue');\n\n/**\n * 后端返回的meta有时候不包括需要的信息 比如activePath等\n * 在这里定义映射\n */\nconst routeMetaMapping: Record<string, Omit<RouteMeta, 'title'>> = {\n  '/system/role-auth/user/:roleId': {\n    activePath: '/system/role',\n    requireHomeRedirect: true,\n  },\n\n  '/system/oss-config/index': {\n    activePath: '/system/oss',\n    requireHomeRedirect: true,\n  },\n\n  '/tool/gen-edit/index/:tableId': {\n    activePath: '/tool/gen',\n    requireHomeRedirect: true,\n  },\n\n  '/workflow/design/index': {\n    activePath: '/workflow/processDefinition',\n    requireHomeRedirect: true,\n  },\n\n  '/workflow/leaveEdit/index': {\n    activePath: '/demo/leave',\n    requireHomeRedirect: true,\n  },\n};\n\n/**\n * 后台路由转vben路由\n * @param menuList 后台菜单\n * @param parentPath 上级目录\n * @returns vben路由\n */\nfunction backMenuToVbenMenu(\n  menuList: Menu[],\n  parentPath = '',\n): RouteRecordStringComponent[] {\n  const resultList: RouteRecordStringComponent[] = [];\n  menuList.forEach((menu) => {\n    // 根目录为菜单形式\n    // 固定有一个children  children为当前菜单\n    if (menu.path === '/' && menu.children && menu.children.length === 1) {\n      if (!menu.children || !menu.children[0]) {\n        return;\n      }\n\n      // 需要处理根目录为内嵌的情况 不会带InnerLink\n      if (/^https?:\\/\\//.test(menu.children[0].path)) {\n        menu.children[0].component = 'InnerLink';\n        menu.children[0].path = menu.children[0].path\n          .replaceAll(/^https?:\\/\\//g, '')\n          .replaceAll('/#/', '')\n          .replaceAll('#', '')\n          .replaceAll(/[?&]/g, '');\n      }\n\n      // 取子路径作为父级路径\n      const path = menu.children[0].path;\n      // 取子菜单的meta作为当前菜单的meta\n      menu.meta = menu.children[0].meta;\n      // 由于在一级路由 父级路径需要加上/\n      menu.path = `/${path}`;\n      menu.component = 'RootMenu';\n      // 将子路径设置为''\n      menu.children[0].path = '';\n    }\n\n    // 外链: http开头 & 组件为Layout || ParentView\n    // 正则判断是否为http://或者https://开头\n    if (\n      /^https?:\\/\\//.test(menu.path) &&\n      (menu.component === 'Layout' || menu.component === 'ParentView')\n    ) {\n      menu.component = 'Link';\n    }\n\n    // 内嵌iframe 组件为InnerLink\n    if (menu.meta?.link && menu.component === 'InnerLink') {\n      menu.component = 'IFrameView';\n    }\n\n    /**\n     * 拼接path\n     * menu.path为''(根目录路由) 则不拼接\n     */\n    if (parentPath && menu.path) {\n      menu.path = `${parentPath}/${menu.path}`;\n    }\n\n    // 创建vben路由对象\n    const vbenRoute: RouteRecordStringComponent = {\n      component: menu.component,\n      meta: {\n        // 当前路由不在菜单显示 但是可以通过链接访问\n        // 不可访问的路由由后端控制隐藏(不返回对应路由)\n        hideInMenu: menu.hidden,\n        icon: menu.meta?.icon,\n        keepAlive: !menu.meta?.noCache,\n        title: menu.meta?.title,\n      },\n      name: menu.name,\n      path: menu.path,\n    };\n\n    // 处理meta映射\n    if (Object.keys(routeMetaMapping).includes(vbenRoute.path)) {\n      const routeMeta = routeMetaMapping[vbenRoute.path];\n      if (routeMeta) {\n        vbenRoute.meta = {\n          ...vbenRoute.meta,\n          ...(routeMeta as RouteMeta),\n        };\n      }\n    }\n\n    // 添加路由参数信息\n    if (menu.query) {\n      try {\n        const query = JSON.parse(menu.query);\n        vbenRoute.meta && (vbenRoute.meta.query = query);\n      } catch {\n        console.error('错误的路由参数类型, 必须为[json]格式');\n      }\n    }\n\n    /**\n     * 处理不同组件\n     */\n    switch (menu.component) {\n      /**\n       * iframe内嵌\n       */\n      case 'IFrameView': {\n        vbenRoute.component = 'IFrameView';\n        if (vbenRoute.meta) {\n          vbenRoute.meta.iframeSrc = menu.meta.link;\n        }\n        /**\n         * 需要判断特殊情况  比如vue的hash是带#的\n         * 比如链接 aaa.com/#/bbb  path会转换为 aaa/com/#/bbb\n         * 比如链接 aaa.com/?bbb=xxx\n         * 需要去除#  否则无法被添加到路由\n         */\n        vbenRoute.path = vbenRoute.path\n          // 替换https:// 或者 http://\n          .replaceAll(/^https?:\\/\\//g, '')\n          .replaceAll('/#/', '')\n          .replaceAll('#', '')\n          .replaceAll(/[?&]/g, '');\n        break;\n      }\n      case 'Layout': {\n        vbenRoute.component = 'BasicLayout';\n        break;\n      }\n      /**\n       * 外链 新窗口打开\n       */\n      case 'Link': {\n        if (vbenRoute.meta) {\n          vbenRoute.meta.link = menu.meta.link;\n        }\n        vbenRoute.component = 'BasicLayout';\n        break;\n      }\n      /**\n       * 三级以上菜单 父级component为ParentView\n       * 不能为layout 会套两层BasicLayout\n       */\n      case 'ParentView': {\n        vbenRoute.component = '';\n        break;\n      }\n      /**\n       * 根目录菜单\n       */\n      case 'RootMenu': {\n        if (vbenRoute.meta) {\n          vbenRoute.meta.hideChildrenInMenu = true;\n        }\n        vbenRoute.component = 'BasicLayout';\n        break;\n      }\n      /**\n       * 其他自定义组件 如system/user/index 拼接/\n       */\n      default: {\n        vbenRoute.component = `/${menu.component}`;\n        break;\n      }\n    }\n\n    // children处理\n    if (menu.children && menu.children.length > 0) {\n      vbenRoute.children = backMenuToVbenMenu(menu.children, menu.path);\n    }\n    // 添加\n    resultList.push(vbenRoute);\n  });\n  return resultList;\n}\n\nasync function generateAccess(options: GenerateMenuAndRoutesOptions) {\n  const pageMap: ComponentRecordType = import.meta.glob('../views/**/*.vue');\n\n  const layoutMap: ComponentRecordType = {\n    BasicLayout,\n    IFrameView,\n    NotFoundComponent,\n  };\n\n  return await generateAccessible(preferences.app.accessMode, {\n    ...options,\n    fetchMenuListAsync: async () => {\n      // 清除以前的message\n      message.destroy();\n      message.loading({\n        content: `${$t('common.loadingMenu')}...`,\n        duration: 1,\n      });\n      // 后台返回路由/菜单\n      const backMenuList = await getAllMenusApi();\n      // 转换为vben能用的路由\n      const vbenMenuList = backMenuToVbenMenu(backMenuList);\n      // 特别注意 这里要深拷贝\n      const menuList = [...cloneDeep(localMenuList), ...vbenMenuList];\n      // console.log('menuList', menuList);\n      return menuList;\n    },\n    // 可以指定没有权限跳转403页面\n    forbiddenComponent,\n    // 如果 route.meta.menuVisibleWithForbidden = true\n    layoutMap,\n    pageMap,\n  });\n}\n\nexport { generateAccess };\n"
  },
  {
    "path": "apps/web-antd/src/router/guard.ts",
    "content": "import type { Router } from 'vue-router';\n\nimport { LOGIN_PATH } from '@vben/constants';\nimport { preferences } from '@vben/preferences';\nimport { useAccessStore, useUserStore } from '@vben/stores';\nimport { startProgress, stopProgress } from '@vben/utils';\n\nimport { accessRoutes, coreRouteNames } from '#/router/routes';\nimport { useAuthStore } from '#/store';\n\nimport { generateAccess } from './access';\n\n/**\n * 通用守卫配置\n * @param router\n */\nfunction setupCommonGuard(router: Router) {\n  // 记录已经加载的页面\n  const loadedPaths = new Set<string>();\n\n  router.beforeEach((to) => {\n    to.meta.loaded = loadedPaths.has(to.path);\n\n    // 页面加载进度条\n    if (!to.meta.loaded && preferences.transition.progress) {\n      startProgress();\n    }\n    return true;\n  });\n\n  router.afterEach((to) => {\n    // 记录页面是否加载,如果已经加载，后续的页面切换动画等效果不在重复执行\n\n    loadedPaths.add(to.path);\n\n    // 关闭页面加载进度条\n    if (preferences.transition.progress) {\n      stopProgress();\n    }\n  });\n}\n\n/**\n * 权限访问守卫配置\n * @param router\n */\nfunction setupAccessGuard(router: Router) {\n  router.beforeEach(async (to, from) => {\n    const accessStore = useAccessStore();\n    const userStore = useUserStore();\n    const authStore = useAuthStore();\n\n    // 基本路由，这些路由不需要进入权限拦截\n    if (coreRouteNames.includes(to.name as string)) {\n      if (to.path === LOGIN_PATH && accessStore.accessToken) {\n        return decodeURIComponent(\n          (to.query?.redirect as string) ||\n            userStore.userInfo?.homePath ||\n            preferences.app.defaultHomePath,\n        );\n      }\n      return true;\n    }\n\n    // accessToken 检查\n    if (!accessStore.accessToken) {\n      // 明确声明忽略权限访问权限，则可以访问\n      if (to.meta.ignoreAccess) {\n        return true;\n      }\n\n      // 没有访问权限，跳转登录页面\n      if (to.fullPath !== LOGIN_PATH) {\n        return {\n          path: LOGIN_PATH,\n          // 如不需要，直接删除 query\n          query:\n            to.fullPath === preferences.app.defaultHomePath\n              ? {}\n              : { redirect: encodeURIComponent(to.fullPath) },\n          // 携带当前跳转的页面，登录后重新跳转该页面\n          replace: true,\n        };\n      }\n      return to;\n    }\n\n    // 是否已经生成过动态路由\n    if (accessStore.isAccessChecked) {\n      return true;\n    }\n\n    // 生成路由表\n    // 当前登录用户拥有的角色标识列表\n    const userInfo = userStore.userInfo || (await authStore.fetchUserInfo());\n    const userRoles = userInfo.roles ?? [];\n\n    // 生成菜单和路由\n    const { accessibleMenus, accessibleRoutes } = await generateAccess({\n      roles: userRoles,\n      router,\n      // 则会在菜单中显示，但是访问会被重定向到403\n      routes: accessRoutes,\n    });\n\n    // 保存菜单信息和路由信息\n    accessStore.setAccessMenus(accessibleMenus);\n    accessStore.setAccessRoutes(accessibleRoutes);\n    accessStore.setIsAccessChecked(true);\n    const redirectPath = (from.query.redirect ??\n      (to.path === preferences.app.defaultHomePath\n        ? userInfo.homePath || preferences.app.defaultHomePath\n        : to.fullPath)) as string;\n\n    return {\n      ...router.resolve(decodeURIComponent(redirectPath)),\n      replace: true,\n    };\n  });\n}\n\n/**\n * 项目守卫配置\n * @param router\n */\nfunction createRouterGuard(router: Router) {\n  /** 通用 */\n  setupCommonGuard(router);\n  /** 权限访问 */\n  setupAccessGuard(router);\n}\n\nexport { createRouterGuard };\n"
  },
  {
    "path": "apps/web-antd/src/router/index.ts",
    "content": "import {\n  createRouter,\n  createWebHashHistory,\n  createWebHistory,\n} from 'vue-router';\n\nimport { resetStaticRoutes } from '@vben/utils';\n\nimport { createRouterGuard } from './guard';\nimport { routes } from './routes';\n\n/**\n *  @zh_CN 创建vue-router实例\n */\nconst router = createRouter({\n  history:\n    import.meta.env.VITE_ROUTER_HISTORY === 'hash'\n      ? createWebHashHistory(import.meta.env.VITE_BASE)\n      : createWebHistory(import.meta.env.VITE_BASE),\n  // 应该添加到路由的初始路由列表。\n  routes,\n  scrollBehavior: (to, _from, savedPosition) => {\n    if (savedPosition) {\n      return savedPosition;\n    }\n    return to.hash ? { behavior: 'smooth', el: to.hash } : { left: 0, top: 0 };\n  },\n  // 是否应该禁止尾部斜杠。\n  // strict: true,\n});\n\nconst resetRoutes = () => resetStaticRoutes(router, routes);\n\n// 创建路由守卫\ncreateRouterGuard(router);\n\nexport { resetRoutes, router };\n"
  },
  {
    "path": "apps/web-antd/src/router/routes/core.ts",
    "content": "import type { RouteRecordRaw } from 'vue-router';\n\nimport { LOGIN_PATH } from '@vben/constants';\nimport { preferences } from '@vben/preferences';\n\nimport { $t } from '#/locales';\n\nconst BasicLayout = () => import('#/layouts/basic.vue');\nconst AuthPageLayout = () => import('#/layouts/auth.vue');\n/** 全局404页面 */\nconst fallbackNotFoundRoute: RouteRecordRaw = {\n  component: () => import('#/views/_core/fallback/not-found.vue'),\n  meta: {\n    hideInBreadcrumb: true,\n    hideInMenu: true,\n    hideInTab: true,\n    title: '404',\n  },\n  name: 'FallbackNotFound',\n  path: '/:path(.*)*',\n};\n\n/** 基本路由，这些路由是必须存在的 */\nconst coreRoutes: RouteRecordRaw[] = [\n  /**\n   * 根路由\n   * 使用基础布局，作为所有页面的父级容器，子级就不必配置BasicLayout。\n   * 此路由必须存在，且不应修改\n   */\n  {\n    component: BasicLayout,\n    meta: {\n      hideInBreadcrumb: true,\n      title: 'Root',\n    },\n    name: 'Root',\n    path: '/',\n    redirect: preferences.app.defaultHomePath,\n    children: [],\n  },\n  {\n    component: () => import('#/views/_core/social-callback/index.vue'),\n    meta: {\n      title: $t('page.auth.oauthLogin'),\n    },\n    name: 'OAuthRedirect',\n    path: '/social-callback',\n  },\n  {\n    component: AuthPageLayout,\n    meta: {\n      hideInTab: true,\n      title: 'Authentication',\n    },\n    name: 'Authentication',\n    path: '/auth',\n    redirect: LOGIN_PATH,\n    children: [\n      {\n        name: 'Login',\n        path: 'login',\n        component: () => import('#/views/_core/authentication/login.vue'),\n        meta: {\n          title: $t('page.auth.login'),\n        },\n      },\n      {\n        name: 'CodeLogin',\n        path: 'code-login',\n        component: () => import('#/views/_core/authentication/code-login.vue'),\n        meta: {\n          title: $t('page.auth.codeLogin'),\n        },\n      },\n      {\n        name: 'QrCodeLogin',\n        path: 'qrcode-login',\n        component: () =>\n          import('#/views/_core/authentication/qrcode-login.vue'),\n        meta: {\n          title: $t('page.auth.qrcodeLogin'),\n        },\n      },\n      {\n        name: 'ForgetPassword',\n        path: 'forget-password',\n        component: () =>\n          import('#/views/_core/authentication/forget-password.vue'),\n        meta: {\n          title: $t('page.auth.forgetPassword'),\n        },\n      },\n      {\n        name: 'Register',\n        path: 'register',\n        component: () => import('#/views/_core/authentication/register.vue'),\n        meta: {\n          title: $t('page.auth.register'),\n        },\n      },\n    ],\n  },\n];\n\nexport { coreRoutes, fallbackNotFoundRoute };\n"
  },
  {
    "path": "apps/web-antd/src/router/routes/index.ts",
    "content": "import type { RouteRecordRaw } from 'vue-router';\n\nimport { mergeRouteModules, traverseTreeValues } from '@vben/utils';\n\nimport { coreRoutes, fallbackNotFoundRoute } from './core';\n\nconst dynamicRouteFiles = import.meta.glob('./modules/**/*.ts', {\n  eager: true,\n});\n\n// 有需要可以自行打开注释，并创建文件夹\n// const externalRouteFiles = import.meta.glob('./external/**/*.ts', { eager: true });\n// const staticRouteFiles = import.meta.glob('./static/**/*.ts', { eager: true });\n\n/** 动态路由 */\nconst dynamicRoutes: RouteRecordRaw[] = mergeRouteModules(dynamicRouteFiles);\n\n/** 外部路由列表，访问这些页面可以不需要Layout，可能用于内嵌在别的系统(不会显示在菜单中) */\n// const externalRoutes: RouteRecordRaw[] = mergeRouteModules(externalRouteFiles);\n// const staticRoutes: RouteRecordRaw[] = mergeRouteModules(staticRouteFiles);\nconst staticRoutes: RouteRecordRaw[] = [];\nconst externalRoutes: RouteRecordRaw[] = [];\n\n/** 路由列表，由基本路由、外部路由和404兜底路由组成\n *  无需走权限验证（会一直显示在菜单中） */\nconst routes: RouteRecordRaw[] = [\n  ...coreRoutes,\n  ...dynamicRoutes,\n  ...externalRoutes,\n  fallbackNotFoundRoute,\n];\n\n/** 基本路由(登录, 第三方登录, 注册等)  */\nconst basicRoutes = [...coreRoutes];\n/** 基本路由列表，这些路由不需要进入权限拦截 */\nconst coreRouteNames = traverseTreeValues(basicRoutes, (route) => route.name);\n\n/** 有权限校验的路由列表，包含动态路由和静态路由 */\nconst accessRoutes = [...dynamicRoutes, ...staticRoutes];\nexport { accessRoutes, coreRouteNames, routes };\n"
  },
  {
    "path": "apps/web-antd/src/router/routes/local.ts",
    "content": "import type { RouteRecordStringComponent } from '@vben/types';\n\nimport { $t } from '@vben/locales';\n\nconst {\n  version,\n  // vite inject-metadata 插件注入的全局变量\n} = __VBEN_ADMIN_METADATA__ || {};\n\n/**\n * 该文件放非后台返回的路由 比如个人中心 等需要跳转显示的页面\n * 也可以直接在菜单管理配置\n */\nconst localRoutes: RouteRecordStringComponent[] = [\n  {\n    component: '/_core/profile/index',\n    meta: {\n      icon: 'mingcute:profile-line',\n      title: $t('ui.widgets.profile'),\n      hideInMenu: true,\n      requireHomeRedirect: true,\n    },\n    name: 'Profile',\n    path: '/profile',\n  },\n];\n\n/**\n * 这里放本地路由\n */\nexport const localMenuList: RouteRecordStringComponent[] = [\n  {\n    component: 'BasicLayout',\n    meta: {\n      order: -1,\n      title: 'page.dashboard.title',\n      // 不使用基础布局（仅在顶级生效）\n      noBasicLayout: true,\n    },\n    name: 'Dashboard',\n    path: '/',\n    redirect: '/analytics',\n    children: [\n      {\n        name: 'Analytics',\n        path: '/analytics',\n        component: '/dashboard/analytics/index',\n        meta: {\n          icon: 'lucide:book-open-text',\n          affixTab: true,\n          title: 'page.dashboard.analytics',\n        },\n      },\n      {\n        name: 'Workspace',\n        path: '/workspace',\n        component: '/dashboard/workspace/index',\n        meta: {\n          icon: 'icon-park-outline:workbench',\n          title: 'page.dashboard.workspace',\n        },\n      },\n    ],\n  },\n  // {\n  //   component: '/_core/about/index',\n  //   meta: {\n  //     icon: 'lucide:copyright',\n  //     order: 9999,\n  //     title: $t('demos.vben.about'),\n  //   },\n  //   name: 'About',\n  //   path: '/vben-admin/about',\n  // },\n  ...localRoutes,\n];\n"
  },
  {
    "path": "apps/web-antd/src/router/routes/modules/aiflow.ts",
    "content": "import type { RouteRecordRaw } from 'vue-router';\n\nconst routes: RouteRecordRaw[] = [\n  {\n    meta: {\n      hideInMenu: true,\n    },\n    name: 'WorkflowEdit',\n    path: '/aiflow/edit/:uuid',\n    component: () => import('#/views/aiflow/edit.vue'),\n  },\n  {\n    meta: {\n      hideInMenu: true,\n    },\n    name: 'WorkflowRun',\n    path: '/aiflow/run/:uuid',\n    component: () => import('#/views/aiflow/run.vue'),\n  },\n];\n\nexport default routes;"
  },
  {
    "path": "apps/web-antd/src/router/routes/modules/dashboard.ts",
    "content": "import type { RouteRecordRaw } from 'vue-router';\n\nimport { $t } from '#/locales';\n\nconst routes: RouteRecordRaw[] = [\n  {\n    meta: {\n      icon: 'lucide:layout-dashboard',\n      order: -1,\n      title: $t('page.dashboard.title'),\n    },\n    name: 'Dashboard',\n    path: '/dashboard',\n    children: [\n      {\n        name: 'Analytics',\n        path: '/analytics',\n        component: () => import('#/views/dashboard/analytics/index.vue'),\n        meta: {\n          affixTab: true,\n          icon: 'lucide:area-chart',\n          title: $t('page.dashboard.analytics'),\n        },\n      },\n      {\n        name: 'Workspace',\n        path: '/workspace',\n        component: () => import('#/views/dashboard/workspace/index.vue'),\n        meta: {\n          icon: 'carbon:workspace',\n          title: $t('page.dashboard.workspace'),\n        },\n      },\n    ],\n  },\n];\n\nexport default routes;\n"
  },
  {
    "path": "apps/web-antd/src/router/routes/modules/knowledge.ts",
    "content": "import type { RouteRecordRaw } from 'vue-router';\n\nconst BasicLayout = () => import('#/layouts/basic.vue');\n\nconst routes: RouteRecordRaw[] = [\n  {\n    component: BasicLayout,\n    meta: {\n      hideInBreadcrumb: true,\n      title: 'Knowledge Base',\n      hideInMenu: true,\n    },\n    name: 'KnowledgeBaseRoot',\n    path: '/knowledge',\n    children: [\n      {\n        name: 'KnowledgeInfoDetail',\n        path: 'info/detail/:id',\n        component: () => import('#/views/knowledge/info/detail/index.vue'),\n        meta: {\n          title: '知识库详情',\n          hideInMenu: true,\n        },\n      },\n    ],\n  },\n];\n\nexport default routes;\n"
  },
  {
    "path": "apps/web-antd/src/router/routes/modules/vben.ts",
    "content": "import type { RouteRecordRaw } from 'vue-router';\n\nimport {\n  VBEN_DOC_URL,\n  VBEN_ELE_PREVIEW_URL,\n  VBEN_GITHUB_URL,\n  VBEN_LOGO_URL,\n  VBEN_NAIVE_PREVIEW_URL,\n} from '@vben/constants';\n\nimport { IFrameView } from '#/layouts';\nimport { $t } from '#/locales';\n\nconst routes: RouteRecordRaw[] = [\n  {\n    meta: {\n      badgeType: 'dot',\n      icon: VBEN_LOGO_URL,\n      order: 9998,\n      title: $t('demos.vben.title'),\n    },\n    name: 'VbenProject',\n    path: '/vben-admin',\n    children: [\n      {\n        name: 'VbenAbout',\n        path: '/vben-admin/about',\n        component: () => import('#/views/_core/about/index.vue'),\n        meta: {\n          icon: 'lucide:copyright|offline',\n          title: $t('demos.vben.about'),\n        },\n      },\n      {\n        name: 'VbenDocument',\n        path: '/vben-admin/document',\n        component: IFrameView,\n        meta: {\n          icon: 'lucide:book-open-text|offline',\n          link: VBEN_DOC_URL,\n          title: $t('demos.vben.document'),\n        },\n      },\n      {\n        name: 'VbenGithub',\n        path: '/vben-admin/github',\n        component: IFrameView,\n        meta: {\n          icon: 'mdi:github',\n          link: VBEN_GITHUB_URL,\n          title: 'Github',\n        },\n      },\n      {\n        name: 'VbenNaive',\n        path: '/vben-admin/naive',\n        component: IFrameView,\n        meta: {\n          badgeType: 'dot',\n          icon: 'logos:naiveui',\n          link: VBEN_NAIVE_PREVIEW_URL,\n          title: $t('demos.vben.naive-ui'),\n        },\n      },\n      {\n        name: 'VbenElementPlus',\n        path: '/vben-admin/ele',\n        component: IFrameView,\n        meta: {\n          badgeType: 'dot',\n          icon: 'logos:element',\n          link: VBEN_ELE_PREVIEW_URL,\n          title: $t('demos.vben.element-plus'),\n        },\n      },\n    ],\n  },\n  {\n    name: 'VbenAbout',\n    path: '/vben-admin/about',\n    component: () => import('#/views/_core/about/index.vue'),\n    meta: {\n      icon: 'lucide:copyright',\n      title: $t('demos.vben.about'),\n      order: 9999,\n    },\n  },\n];\n\nexport default routes;\n"
  },
  {
    "path": "apps/web-antd/src/store/auth.ts",
    "content": "import type { LoginAndRegisterParams } from '@vben/common-ui';\nimport type { UserInfo } from '@vben/types';\n\nimport { ref } from 'vue';\nimport { useRouter } from 'vue-router';\n\nimport { LOGIN_PATH } from '@vben/constants';\nimport { preferences } from '@vben/preferences';\nimport { resetAllStores, useAccessStore, useUserStore } from '@vben/stores';\n\nimport { notification } from 'ant-design-vue';\nimport { defineStore } from 'pinia';\n\nimport { doLogout, getUserInfoApi, loginApi, seeConnectionClose } from '#/api';\nimport {\n  ImpossibleReturn401Exception,\n  UnauthorizedException,\n} from '#/api/helper';\nimport { $t } from '#/locales';\n\nimport { useDictStore } from './dict';\n\nexport const useAuthStore = defineStore('auth', () => {\n  const accessStore = useAccessStore();\n  const userStore = useUserStore();\n  const router = useRouter();\n\n  const loginLoading = ref(false);\n\n  /**\n   * 异步处理登录操作\n   * Asynchronously handle the login process\n   * @param params 登录表单数据\n   */\n  async function authLogin(\n    params: LoginAndRegisterParams,\n    onSuccess?: () => Promise<void> | void,\n  ) {\n    // 异步处理用户登录操作并获取 accessToken\n    let userInfo: null | UserInfo = null;\n    try {\n      loginLoading.value = true;\n      const { access_token } = await loginApi(params);\n\n      // 将 accessToken 存储到 accessStore 中\n      accessStore.setAccessToken(access_token);\n      accessStore.setRefreshToken(access_token);\n\n      // 获取用户信息并存储到 accessStore 中\n      userInfo = await fetchUserInfo();\n      /**\n       * 设置用户信息\n       */\n      userStore.setUserInfo(userInfo);\n      /**\n       * 在这里设置权限\n       */\n      accessStore.setAccessCodes(userInfo.permissions);\n\n      if (accessStore.loginExpired) {\n        accessStore.setLoginExpired(false);\n      } else {\n        onSuccess\n          ? await onSuccess?.()\n          : await router.push(preferences.app.defaultHomePath);\n      }\n\n      if (userInfo?.realName) {\n        notification.success({\n          description: `${$t('authentication.loginSuccessDesc')}:${userInfo?.realName}`,\n          duration: 3,\n          message: $t('authentication.loginSuccess'),\n        });\n      }\n    } finally {\n      loginLoading.value = false;\n    }\n\n    return {\n      userInfo,\n    };\n  }\n\n  async function logout(redirect: boolean = true) {\n    try {\n      // 这两个接口不依赖 不需要await sseClose\n      await Promise.all([seeConnectionClose(), doLogout()]);\n    } catch (error) {\n      console.error(error);\n      /**\n       * 这两个接口按正常逻辑不可能返回401\n       * 在微服务版本配置错误的情况下 这里会抛出401\n       * 在这里抛出自定义异常供上层处理\n       */\n      if (error instanceof UnauthorizedException) {\n        throw new ImpossibleReturn401Exception(error.message);\n      }\n    } finally {\n      resetAllStores();\n      accessStore.setLoginExpired(false);\n\n      // 回登陆页带上当前路由地址\n      await router.replace({\n        path: LOGIN_PATH,\n        query: redirect\n          ? {\n              redirect: encodeURIComponent(router.currentRoute.value.fullPath),\n            }\n          : {},\n      });\n    }\n  }\n\n  async function fetchUserInfo() {\n    const backUserInfo = await getUserInfoApi();\n    /**\n     * 登录超时的情况\n     */\n    if (!backUserInfo) {\n      throw new Error('获取用户信息失败.');\n    }\n    const { permissions = [], roles = [], user } = backUserInfo;\n    /**\n     * 从后台user -> vben user转换\n     */\n    const userInfo: UserInfo = {\n      avatar: user.avatar ?? '',\n      permissions,\n      realName: user.nickName,\n      roles,\n      userId: user.userId,\n      username: user.userName,\n      email: user.email ?? '',\n    };\n    userStore.setUserInfo(userInfo);\n    /**\n     * 需要重新加载字典\n     * 比如退出登录切换到其他租户\n     */\n    const dictStore = useDictStore();\n    dictStore.resetCache();\n    return userInfo;\n  }\n\n  function $reset() {\n    loginLoading.value = false;\n  }\n\n  return {\n    $reset,\n    authLogin,\n    fetchUserInfo,\n    loginLoading,\n    logout,\n  };\n});\n"
  },
  {
    "path": "apps/web-antd/src/store/dict.ts",
    "content": "/* eslint-disable @typescript-eslint/no-non-null-assertion */\nimport type { DictData } from '#/api/system/dict/dict-data-model';\n\nimport { reactive } from 'vue';\n\nimport { defineStore } from 'pinia';\n\n/**\n * antd使用 select和radio通用\n * 本质上是对DictData的拓展\n */\nexport interface DictOption extends DictData {\n  disabled?: boolean;\n  label: string;\n  value: number | string;\n}\n\n/**\n * 将字典数据转为Options\n * @param data 字典数据\n * @param formatNumber 是否需要将value格式化为number类型\n * @returns options\n */\nexport function dictToOptions(\n  data: DictData[],\n  formatNumber = false,\n): DictOption[] {\n  return data.map((item) => ({\n    ...item,\n    label: item.dictLabel,\n    value: formatNumber ? Number(item.dictValue) : item.dictValue,\n  }));\n}\n\nexport const useDictStore = defineStore('app-dict', () => {\n  /**\n   * select radio checkbox等使用 只能为固定格式{label, value}\n   */\n  const dictOptionsMap = reactive(new Map<string, DictOption[]>());\n  /**\n   * 添加一个字典请求状态的缓存\n   *\n   * 主要解决多次请求重复api的问题(不能用abortController 会导致除了第一个其他的获取的全为空)\n   * 比如在一个页面 index表单 modal drawer总共会请求三次 但是获取的都是一样的数据\n   * 相当于加锁 保证只有第一次请求的结果能拿到\n   */\n  const dictRequestCache = reactive(\n    new Map<string, Promise<DictData[] | void>>(),\n  );\n\n  function getDictOptions(dictName: string): DictOption[] {\n    if (!dictName) return [];\n    // 没有key 添加一个空数组\n    if (!dictOptionsMap.has(dictName)) {\n      dictOptionsMap.set(dictName, []);\n    }\n    // 这里拿到的就不可能为空了\n    return dictOptionsMap.get(dictName)!;\n  }\n\n  function resetCache() {\n    dictRequestCache.clear();\n    dictOptionsMap.clear();\n    /**\n     * 不需要清空dictRequestCache 每次请求成功/失败都清空key\n     */\n  }\n\n  /**\n   * 核心逻辑\n   *\n   * 不能直接粗暴使用set 会导致之前return的空数组跟现在的数组指向不是同一个地址  数据也就为空了\n   *\n   * 判断是否已经存在key 并且数组长度为0 说明该次要处理的数据是return的空数组 直接push(不修改指向)\n   * 否则 直接set\n   *\n   */\n  function setDictInfo(\n    dictName: string,\n    dictValue: DictData[],\n    formatNumber = false,\n  ) {\n    if (\n      dictOptionsMap.has(dictName) &&\n      dictOptionsMap.get(dictName)?.length === 0\n    ) {\n      dictOptionsMap\n        .get(dictName)\n        ?.push(...dictToOptions(dictValue, formatNumber));\n    } else {\n      dictOptionsMap.set(dictName, dictToOptions(dictValue, formatNumber));\n    }\n  }\n\n  function $reset() {\n    /**\n     * doNothing\n     */\n  }\n\n  return {\n    $reset,\n    dictOptionsMap,\n    dictRequestCache,\n    getDictOptions,\n    resetCache,\n    setDictInfo,\n  };\n});\n"
  },
  {
    "path": "apps/web-antd/src/store/index.ts",
    "content": "export * from './auth';\nexport * from './notify';\n"
  },
  {
    "path": "apps/web-antd/src/store/notify.ts",
    "content": "import type { NotificationItem } from '@vben/layouts';\n\nimport { computed, ref, watch } from 'vue';\n\nimport { SvgMessageUrl } from '@vben/icons';\nimport { $t } from '@vben/locales';\nimport { useUserStore } from '@vben/stores';\n\nimport { Modal, notification } from 'ant-design-vue';\nimport dayjs from 'dayjs';\nimport { defineStore } from 'pinia';\n\nimport { useSseMessage } from '#/utils/message';\n\nexport const useNotifyStore = defineStore(\n  'app-notify',\n  () => {\n    /**\n     * return才会被持久化 存储全部消息\n     */\n    const notificationList = ref<NotificationItem[]>([]);\n\n    const userStore = useUserStore();\n    const userId = computed(() => {\n      return userStore.userInfo?.userId || '0';\n    });\n\n    const notifications = computed(() => {\n      return notificationList.value.filter(\n        (item) => item.userId === userId.value,\n      );\n    });\n\n    /**\n     * 开始监听sse消息\n     */\n    function startListeningMessage() {\n      // 默认sse 使用 websocket自行开启注释\n      // const websocketReturnData = useWebSocketMessage();\n      // if (!websocketReturnData) {\n      //   return;\n      // }\n      // const { data } = websocketReturnData;\n\n      const sseReturnData = useSseMessage();\n      if (!sseReturnData) {\n        return;\n      }\n      const { data } = sseReturnData;\n\n      watch(data, (message) => {\n        if (!message) return;\n        console.log(`接收到消息: ${message}`);\n\n        notification.success({\n          description: message,\n          duration: 3,\n          message: $t('component.notice.received'),\n        });\n\n        notificationList.value.unshift({\n          // avatar: `https://api.multiavatar.com/${random(0, 10_000)}.png`, 随机头像\n          avatar: SvgMessageUrl,\n          date: dayjs().format('YYYY-MM-DD HH:mm:ss'),\n          isRead: false,\n          message,\n          title: $t('component.notice.title'),\n          userId: userId.value,\n        });\n\n        // 需要手动置空 vue3在值相同时不会触发watch\n        data.value = null;\n      });\n    }\n\n    /**\n     * 设置全部已读\n     */\n    function setAllRead() {\n      notificationList.value\n        .filter((item) => item.userId === userId.value)\n        .forEach((item) => {\n          item.isRead = true;\n        });\n    }\n\n    /**\n     * 设置单条消息已读\n     * @param item 通知\n     */\n    function setRead(item: NotificationItem) {\n      !item.isRead && (item.isRead = true);\n      // 显示信息\n      Modal.info({\n        title: item.title,\n        content: item.message,\n      });\n    }\n\n    /**\n     * 清空全部消息\n     */\n    function clearAllMessage() {\n      notificationList.value = notificationList.value.filter(\n        (item) => item.userId !== userId.value,\n      );\n    }\n\n    /**\n     * 只需要空实现即可\n     * 否则会在退出登录清空所有\n     */\n    function $reset() {\n      // notificationList.value = [];\n    }\n    /**\n     * 显示小圆点\n     */\n    const showDot = computed(() =>\n      notificationList.value\n        .filter((item) => item.userId === userId.value)\n        .some((item) => !item.isRead),\n    );\n\n    return {\n      $reset,\n      clearAllMessage,\n      notificationList,\n      notifications,\n      setAllRead,\n      setRead,\n      showDot,\n      startListeningMessage,\n    };\n  },\n  {\n    persist: {\n      pick: ['notificationList'],\n    },\n  },\n);\n"
  },
  {
    "path": "apps/web-antd/src/store/tenant.ts",
    "content": "import type { TenantOption } from '#/api/core/auth';\n\nimport { ref } from 'vue';\n\nimport { defineStore } from 'pinia';\n\nimport { tenantList as tenantListApi } from '#/api/core/auth';\n\n/**\n * 用于超级管理员切换租户\n */\nexport const useTenantStore = defineStore('app-tenant', () => {\n  // 是否已经选中租户\n  const checked = ref(false);\n  // 是否开启租户功能\n  const tenantEnable = ref(true);\n  const tenantList = ref<TenantOption[]>([]);\n\n  // 初始化 获取租户信息\n  async function initTenant() {\n    const { tenantEnabled, voList } = await tenantListApi();\n    tenantEnable.value = tenantEnabled;\n    tenantList.value = voList;\n  }\n\n  async function setChecked(_checked: boolean) {\n    checked.value = _checked;\n  }\n\n  function $reset() {\n    checked.value = false;\n    tenantEnable.value = true;\n    tenantList.value = [];\n  }\n\n  return {\n    $reset,\n    checked,\n    initTenant,\n    setChecked,\n    tenantEnable,\n    tenantList,\n  };\n});\n"
  },
  {
    "path": "apps/web-antd/src/upload-tip.ts",
    "content": "import { onMounted } from 'vue';\n\nimport { useLocalStorage } from '@vueuse/core';\nimport { Modal } from 'ant-design-vue';\n\nexport function useUploadTip() {\n  const readTip = useLocalStorage<boolean>('__upload_tip_read_5.4.0', false);\n  onMounted(() => {\n    if (readTip.value || !import.meta.env.DEV) {\n      return;\n    }\n    const modalInstance = Modal.info({\n      title: '提示',\n      centered: true,\n      content:\n        '如果你的版本是从低版本升级到后端>5.4.0, 记得执行升级sql, 否则跳转页面(如oss 代码生成配置)等会404',\n      okButtonProps: { disabled: true },\n      onOk() {\n        modalInstance.destroy();\n        readTip.value = true;\n      },\n    });\n\n    let time = 3;\n    const interval = setInterval(() => {\n      modalInstance.update({\n        okText: time === 0 ? '我知道了, 不再弹出' : `${time}秒后关闭`,\n        okButtonProps: { disabled: time > 0 },\n      });\n      if (time <= 0) {\n        clearInterval(interval);\n      }\n      time--;\n    }, 1000);\n  });\n}\n"
  },
  {
    "path": "apps/web-antd/src/utils/dict.ts",
    "content": "import { UnauthorizedException } from '#/api/helper';\nimport { dictDataInfo } from '#/api/system/dict/dict-data';\nimport { useDictStore } from '#/store/dict';\n\n/**\n * 一般是Select, Radio, Checkbox等组件使用\n * @warning 注意内部为异步实现 所以不要写这种`getDictOptions()[0]`的代码 会获取不到\n * @warning 需要保持`formatNumber`统一 在所有调用地方需要一致 不能出现A处为string B处为number\n *\n * @param dictName 字典名称\n * @param formatNumber 是否格式化字典value为number类型\n * @returns Options数组\n */\nexport function getDictOptions(dictName: string, formatNumber = false) {\n  const { dictRequestCache, setDictInfo, getDictOptions } = useDictStore();\n  const dataList = getDictOptions(dictName);\n\n  // 检查请求状态缓存\n  if (dataList.length === 0 && !dictRequestCache.has(dictName)) {\n    dictRequestCache.set(\n      dictName,\n      dictDataInfo(dictName)\n        .then((resp) => {\n          // 缓存到store 这样就不用重复获取了\n          // 内部处理了push的逻辑 这里不用push\n          setDictInfo(dictName, resp, formatNumber);\n        })\n        .catch((error) => {\n          /**\n           * 需要判断是否为401抛出的特定异常 401清除缓存\n           * 其他error清除缓存会导致无限循环调用字典接口 则不做处理\n           */\n          if (error instanceof UnauthorizedException) {\n            // 401时 移除字典缓存 下次登录重新获取\n            dictRequestCache.delete(dictName);\n          }\n          // 其他不做处理\n        })\n        .finally(() => {\n          // 移除请求状态缓存\n          /**\n           * 这里主要判断字典item为空的情况(无奈兼容 不给字典item本来就是错误用法)\n           * 会导致if一直进入逻辑导致接口无限刷新\n           * 在这里dictList为空时 不删除缓存\n           */\n          if (dataList.length > 0) {\n            dictRequestCache.delete(dictName);\n          }\n        }),\n    );\n  }\n  return dataList;\n}\n"
  },
  {
    "path": "apps/web-antd/src/utils/file/base64Conver.ts",
    "content": "/**\n * @description: base64 to blob\n */\nexport function dataURLtoBlob(base64Buf: string): Blob {\n  const arr = base64Buf.split(',');\n  const typeItem = arr[0];\n  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion\n  const mime = typeItem!.match(/:(.*?);/)![1];\n  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion\n  const bstr = window.atob(arr[1]!);\n  let n = bstr.length;\n  const u8arr = new Uint8Array(n);\n  while (n--) {\n    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion\n    u8arr[n] = bstr.codePointAt(n)!;\n  }\n  return new Blob([u8arr], { type: mime });\n}\n\n/**\n * img url to base64\n * @param url\n */\nexport function urlToBase64(url: string, mineType?: string): Promise<string> {\n  return new Promise((resolve, reject) => {\n    let canvas = document.createElement('CANVAS') as HTMLCanvasElement | null;\n    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion\n    const ctx = canvas!.getContext('2d');\n\n    const img = new Image();\n    img.crossOrigin = '';\n    img.addEventListener('load', () => {\n      if (!canvas || !ctx) {\n        // eslint-disable-next-line prefer-promise-reject-errors\n        return reject();\n      }\n      canvas.height = img.height;\n      canvas.width = img.width;\n      ctx.drawImage(img, 0, 0);\n      const dataURL = canvas.toDataURL(mineType || 'image/png');\n      canvas = null;\n      resolve(dataURL);\n    });\n    img.src = url;\n  });\n}\n"
  },
  {
    "path": "apps/web-antd/src/utils/file/download.ts",
    "content": "import type { VbenFormProps } from '#/adapter/form';\n\nimport { $t } from '@vben/locales';\nimport { cloneDeep, formatDate } from '@vben/utils';\n\nimport { message } from 'ant-design-vue';\nimport { isFunction } from 'lodash-es';\n\nimport { dataURLtoBlob, urlToBase64 } from './base64Conver';\n\n/**\n *\n * @deprecated 无法处理区间选择器数据 请使用commonDownloadExcel\n *\n * 下载excel文件\n * @param [func] axios函数\n * @param [fileName] 文件名称 不需要带xlsx后缀\n * @param [requestData] 请求参数\n * @param [withRandomName] 是否带随机文件名\n *\n * @return void\n */\nexport async function downloadExcel(\n  func: (data?: any) => Promise<Blob>,\n  fileName: string,\n  requestData: any = {},\n  withRandomName = true,\n) {\n  const hideLoading = message.loading($t('pages.common.downloadLoading'), 0);\n  try {\n    const data = await func(requestData);\n    downloadExcelFile(data, fileName, withRandomName);\n  } catch (error) {\n    console.error(error);\n  } finally {\n    hideLoading();\n  }\n}\n\n/**\n * 源码同packages\\@core\\ui-kit\\form-ui\\src\\components\\form-actions.vue\n * @param values 表单值\n * @param fieldMappingTime 区间选择器 字段映射\n * @returns 格式化后的值\n */\nfunction handleRangeTimeValue(\n  values: Record<string, any>,\n  fieldMappingTime: VbenFormProps['fieldMappingTime'],\n) {\n  // 需要深拷贝 可能是readonly的\n  values = cloneDeep(values);\n  if (!fieldMappingTime || !Array.isArray(fieldMappingTime)) {\n    return values;\n  }\n\n  fieldMappingTime.forEach(\n    ([field, [startTimeKey, endTimeKey], format = 'YYYY-MM-DD']) => {\n      if (startTimeKey && endTimeKey && values[field] === null) {\n        Reflect.deleteProperty(values, startTimeKey);\n        Reflect.deleteProperty(values, endTimeKey);\n        // delete values[startTimeKey];\n        // delete values[endTimeKey];\n      }\n\n      if (!values[field]) {\n        Reflect.deleteProperty(values, field);\n        // delete values[field];\n        return;\n      }\n\n      const [startTime, endTime] = values[field];\n      if (format === null) {\n        values[startTimeKey] = startTime;\n        values[endTimeKey] = endTime;\n      } else if (isFunction(format)) {\n        values[startTimeKey] = format(startTime, startTimeKey);\n        values[endTimeKey] = format(endTime, endTimeKey);\n      } else {\n        const [startTimeFormat, endTimeFormat] = Array.isArray(format)\n          ? format\n          : [format, format];\n\n        values[startTimeKey] = startTime\n          ? formatDate(startTime, startTimeFormat)\n          : undefined;\n        values[endTimeKey] = endTime\n          ? formatDate(endTime, endTimeFormat)\n          : undefined;\n      }\n      // delete values[field];\n      Reflect.deleteProperty(values, field);\n    },\n  );\n  return values;\n}\n\nexport interface DownloadExcelOptions {\n  // 是否随机文件名(带时间戳)\n  withRandomName?: boolean;\n  // 区间选择器 字段映射\n  fieldMappingTime?: VbenFormProps['fieldMappingTime'];\n}\n\n/**\n * 通用下载excel方法\n * @param api 后端下载接口\n * @param fileName 文件名 不带拓展名\n * @param requestData 请求参数\n * @param options 下载选项\n */\nexport async function commonDownloadExcel(\n  api: (data?: any) => Promise<Blob>,\n  fileName: string,\n  requestData: any = {},\n  options: DownloadExcelOptions = {},\n) {\n  const hideLoading = message.loading($t('pages.common.downloadLoading'), 0);\n  try {\n    const { withRandomName = true, fieldMappingTime } = options;\n    // 需要处理时间字段映射\n    const data = await api(handleRangeTimeValue(requestData, fieldMappingTime));\n    downloadExcelFile(data, fileName, withRandomName);\n  } catch (error) {\n    console.error(error);\n  } finally {\n    hideLoading();\n  }\n}\n\nexport function downloadExcelFile(\n  data: BlobPart,\n  filename: string,\n  withRandomName = true,\n) {\n  let realFileName = filename;\n  if (withRandomName) {\n    realFileName = `${filename}-${Date.now()}.xlsx`;\n  }\n  downloadByData(data, realFileName);\n}\n\n/**\n * Download online pictures\n * @param url\n * @param filename\n * @param mime\n * @param bom\n */\nexport function downloadByOnlineUrl(\n  url: string,\n  filename: string,\n  mime?: string,\n  bom?: BlobPart,\n) {\n  urlToBase64(url).then((base64) => {\n    downloadByBase64(base64, filename, mime, bom);\n  });\n}\n\n/**\n * Download pictures based on base64\n * @param buf\n * @param filename\n * @param mime\n * @param bom\n */\nexport function downloadByBase64(\n  buf: string,\n  filename: string,\n  mime?: string,\n  bom?: BlobPart,\n) {\n  const base64Buf = dataURLtoBlob(buf);\n  downloadByData(base64Buf, filename, mime, bom);\n}\n\n/**\n * Download according to the background interface file stream\n * @param {*} data\n * @param {*} filename\n * @param {*} mime\n * @param {*} bom\n */\nexport function downloadByData(\n  data: BlobPart,\n  filename: string,\n  mime?: string,\n  bom?: BlobPart,\n) {\n  const blobData = bom === undefined ? [data] : [bom, data];\n  const blob = new Blob(blobData, { type: mime || 'application/octet-stream' });\n\n  const blobURL = window.URL.createObjectURL(blob);\n  const tempLink = document.createElement('a');\n  tempLink.style.display = 'none';\n  tempLink.href = blobURL;\n  tempLink.setAttribute('download', filename);\n  if (tempLink.download === undefined) {\n    tempLink.setAttribute('target', '_blank');\n  }\n  document.body.append(tempLink);\n  tempLink.click();\n  tempLink.remove();\n  window.URL.revokeObjectURL(blobURL);\n}\n\nexport function openWindow(\n  url: string,\n  opt?: {\n    noopener?: boolean;\n    noreferrer?: boolean;\n    target?: '_blank' | '_self' | string;\n  },\n) {\n  const { noopener = true, noreferrer = true, target = '__blank' } = opt || {};\n  const feature: string[] = [];\n\n  noopener && feature.push('noopener=yes');\n  noreferrer && feature.push('noreferrer=yes');\n\n  window.open(url, target, feature.join(','));\n}\n\n/**\n * Download file according to file address\n * @param {*} sUrl\n */\nexport function downloadByUrl({\n  fileName,\n  target = '_blank',\n  url,\n}: {\n  fileName?: string;\n  target?: '_blank' | '_self';\n  url: string;\n}): boolean {\n  const isChrome = window.navigator.userAgent.toLowerCase().includes('chrome');\n  const isSafari = window.navigator.userAgent.toLowerCase().includes('safari');\n\n  if (/iP/.test(window.navigator.userAgent)) {\n    console.error('Your browser does not support download!');\n    return false;\n  }\n  if (isChrome || isSafari) {\n    const link = document.createElement('a');\n    link.href = url;\n    link.target = target;\n\n    if (link.download !== undefined) {\n      link.download =\n        // eslint-disable-next-line unicorn/prefer-string-slice\n        fileName || url.substring(url.lastIndexOf('/') + 1, url.length);\n    }\n\n    if (document.createEvent) {\n      const e = document.createEvent('MouseEvents');\n      e.initEvent('click', true, true);\n      link.dispatchEvent(e);\n      return true;\n    }\n  }\n  if (!url.includes('?')) {\n    url += '?download';\n  }\n\n  openWindow(url, { target });\n  return true;\n}\n"
  },
  {
    "path": "apps/web-antd/src/utils/file/index.ts",
    "content": "/**\n * 计算文件大小并以适当单位表示\n *\n * 此函数接收一个表示文件大小的数字（以字节为单位），并返回一个格式化后的字符串，\n * 该字符串表示文件的大小，以最适合的单位（B, KB, MB, GB, TB）表示\n *\n * @param size 文件大小，以字节为单位\n * @param isInteger 是否返回整数大小，默认为false如果设置为true，\n *                    则返回的大小将不包含小数部分；如果为false，则根据单位的不同，\n *                    返回最多3位小数的大小\n * @returns 格式化后的文件大小字符串，如\"4.5KB\"或\"3MB\"\n */\nexport function calculateFileSize(size: number, isInteger = false) {\n  // 定义文件大小的单位数组\n  const units = ['B', 'KB', 'MB', 'GB', 'TB'];\n  // 定义换算基数，1KB = 1024B，1MB = 1024KB，以此类推\n  const base = 1024;\n\n  // 初始化单位索引，初始值为0，即默认单位为B\n  let unitIndex = 0;\n  // 当文件大小大于等于基数且单位索引未超出单位数组范围时，循环进行单位转换\n  while (size >= base && unitIndex < units.length - 1) {\n    size /= base;\n    unitIndex++;\n  }\n\n  // 根据是否需要整数大小，确定输出的精度\n  const precision = isInteger ? 0 : Math.min(unitIndex, 3);\n  // 返回格式化后的文件大小字符串\n  return `${size.toFixed(precision)}${units[unitIndex]}`;\n}\n"
  },
  {
    "path": "apps/web-antd/src/utils/message.ts",
    "content": "import { useAppConfig } from '@vben/hooks';\nimport { useAccessStore } from '@vben/stores';\n\nimport { useEventSource, useWebSocket } from '@vueuse/core';\n\nconst { apiURL, clientId, sseEnable, websocketEnable } = useAppConfig(\n  import.meta.env,\n  import.meta.env.PROD,\n);\n\nexport function useSseMessage() {\n  /**\n   * 未开启 不监听\n   */\n  if (!sseEnable) {\n    console.warn('当前未开启sse.');\n    return;\n  }\n  const accessStore = useAccessStore();\n  const token = accessStore.accessToken;\n\n  const sseAddr = `${apiURL}/resource/sse?clientid=${clientId}&Authorization=Bearer ${token}`;\n\n  const sseReturnData = useEventSource(sseAddr, [], {\n    autoReconnect: {\n      delay: 1000,\n      onFailed() {\n        console.error('sse重连失败.');\n      },\n      retries: 3,\n    },\n  });\n\n  return sseReturnData;\n}\n\nfunction isUrl(path?: string) {\n  return /^https?:\\/\\//.test(path || '');\n}\n\nexport function useWebSocketMessage() {\n  if (!websocketEnable) {\n    console.warn('当前未开启websocket.');\n    return;\n  }\n  let apiUrlStr = String(apiURL);\n  /**\n   * 这里可能有两种情况 兼容dev模式的proxy或者prod模式但是没有用全路径比如http://xxx/xxx\n   * 1. apiUrl为https://xxx.com/xxx\n   * 2. apiUrl为/xxx\n   * 转换后为http链接形式\n   */\n  if (!isUrl(apiURL)) {\n    // 协议+域名\n    apiUrlStr = `${window.location.protocol}//${window.location.host}${apiURL}`;\n  }\n  const accessStore = useAccessStore();\n  const token = accessStore.accessToken;\n  // 这里是http链接形式\n  let websocketAddr = `${apiUrlStr}/resource/websocket?clientid=${clientId}&Authorization=Bearer ${token}`;\n  // http/https处理\n  websocketAddr = window.location.protocol.includes('https')\n    ? websocketAddr.replace('https://', 'wss://')\n    : websocketAddr.replace('http://', 'ws://');\n  // console.log('websocketUrl: ' + websocketAddr);\n\n  const websocketResponse = useWebSocket(websocketAddr, {\n    autoReconnect: {\n      // 重连最大次数\n      retries: 3,\n      // 重连间隔\n      delay: 1000,\n      onFailed() {\n        console.error('websocket重连失败.');\n      },\n    },\n    heartbeat: {\n      message: JSON.stringify({ type: 'ping' }),\n      // 发送心跳的间隔\n      interval: 10_000,\n      // 接收到心跳response的超时时间\n      pongTimeout: 2000,\n    },\n    onConnected() {\n      console.info('websocket已经连接');\n    },\n    onDisconnected() {\n      console.warn('websocket已经断开');\n    },\n  });\n\n  return websocketResponse;\n}\n"
  },
  {
    "path": "apps/web-antd/src/utils/modal.tsx",
    "content": "import type { ModalFuncProps } from 'ant-design-vue';\nimport type { Rule } from 'ant-design-vue/es/form';\n\nimport { reactive } from 'vue';\n\nimport { Alert, Form, Input, Modal } from 'ant-design-vue';\nimport { isFunction } from 'lodash-es';\n\nexport interface ConfirmModalProps extends Omit<ModalFuncProps, 'visible'> {\n  confirmText?: string;\n  placeholder?: string;\n  onValidated?: () => Promise<void>;\n}\n\nexport function confirmDeleteModal(props: ConfirmModalProps) {\n  const placeholder = props.placeholder || `输入'确认删除'`;\n  const confirmText = props.confirmText || '确认删除';\n\n  const formValue = reactive({\n    content: '',\n  });\n  const rulesRef = reactive<{ [key: string]: Rule[] }>({\n    content: [\n      {\n        message: '校验不通过',\n        required: true,\n        trigger: 'change',\n        validator(_, value) {\n          if (value !== confirmText) {\n            return Promise.reject(new Error('校验不通过'));\n          }\n          return Promise.resolve();\n        },\n      },\n    ],\n  });\n  const useForm = Form.useForm;\n  const { validate, validateInfos } = useForm(formValue, rulesRef);\n\n  Modal.confirm({\n    ...props,\n    centered: true,\n    content: (\n      <div class=\"flex flex-col gap-[8px]\">\n        <Alert message={'确认删除后将无法恢复，请谨慎操作！'} type=\"error\" />\n        <Form layout=\"vertical\" model={formValue}>\n          <Form.Item {...validateInfos.content}>\n            <Input\n              placeholder={placeholder}\n              v-model:value={formValue.content}\n            />\n          </Form.Item>\n        </Form>\n      </div>\n    ),\n    okButtonProps: { danger: true, type: 'primary' },\n    onOk: async () => {\n      await validate();\n      isFunction(props.onValidated) && props.onValidated();\n    },\n    title: '提示',\n    type: 'warning',\n  });\n}\n"
  },
  {
    "path": "apps/web-antd/src/utils/popup.ts",
    "content": "import type { ExtendedFormApi } from '@vben/common-ui';\nimport type { MaybePromise } from '@vben/types';\n\nimport { ref } from 'vue';\n\nimport { $t } from '@vben/locales';\n\nimport { Modal } from 'ant-design-vue';\nimport { isFunction } from 'lodash-es';\n\ninterface BeforeCloseDiffProps {\n  /**\n   * 初始化值如何获取\n   * @returns Promise<string>\n   */\n  initializedGetter: () => MaybePromise<string>;\n  /**\n   * 当前值如何获取\n   * @returns Promise<string>\n   */\n  currentGetter: () => MaybePromise<string>;\n  /**\n   * 自定义比较函数\n   * @param init 初始值\n   * @param current 当前值\n   * @returns boolean\n   */\n  compare?: (init: string, current: string) => boolean;\n}\n\n/**\n * 用于Drawer/Modal使用 判断表单是否有变动来决定是否弹窗提示\n * @param props props\n * @returns hook\n */\nexport function useBeforeCloseDiff(props: BeforeCloseDiffProps) {\n  const { initializedGetter, currentGetter, compare } = props;\n  /**\n   * 记录初始值 json\n   */\n  const initialized = ref<string>('');\n  /**\n   * 是否已经初始化了 通过这个值判断是否需要进行对比 为false直接关闭 不弹窗\n   */\n  const isInitialized = ref(false);\n\n  /**\n   * 标记是否已经完成初始化 后续需要进行对比\n   * @param data 自定义初始化数据 可选\n   */\n  async function markInitialized(data?: string) {\n    initialized.value = data || (await initializedGetter());\n    isInitialized.value = true;\n  }\n\n  /**\n   * 重置初始化状态 需要在closed前调用 或者打开窗口时\n   */\n  function resetInitialized() {\n    initialized.value = '';\n    isInitialized.value = false;\n  }\n\n  /**\n   * 提供给useVbenForm/useVbenDrawer使用\n   * @returns 是否允许关闭\n   */\n  async function onBeforeClose(): Promise<boolean> {\n    // 如果还未初始化，直接允许关闭\n    if (!isInitialized.value) {\n      return true;\n    }\n\n    try {\n      // 获取当前表单数据\n      const current = await currentGetter();\n      // 自定义比较的情况\n      if (isFunction(compare) && compare(initialized.value, current)) {\n        return true;\n      } else {\n        // 如果数据没有变化，直接允许关闭\n        if (current === initialized.value) {\n          return true;\n        }\n      }\n\n      // 数据有变化，显示确认对话框\n      return new Promise<boolean>((resolve) => {\n        Modal.confirm({\n          title: $t('pages.common.tip'),\n          content: $t('pages.common.beforeCloseTip'),\n          centered: true,\n          okButtonProps: { danger: true },\n          cancelText: $t('common.cancel'),\n          okText: $t('common.confirm'),\n          onOk: () => {\n            resolve(true);\n            isInitialized.value = false;\n          },\n          onCancel: () => resolve(false),\n        });\n      });\n    } catch (error) {\n      console.error('Failed to compare data:', error);\n      return true;\n    }\n  }\n\n  return {\n    onBeforeClose,\n    markInitialized,\n    resetInitialized,\n  };\n}\n\n/**\n * 给useVbenForm使用的 封装函数\n * @param formApi 表单实例\n * @returns getter\n */\nexport function defaultFormValueGetter(formApi: ExtendedFormApi) {\n  return async () => {\n    const v = await formApi.getValues();\n    return JSON.stringify(v);\n  };\n}\n"
  },
  {
    "path": "apps/web-antd/src/utils/render.tsx",
    "content": "import type { Component as ComponentType } from 'vue';\n\nimport type { DictData } from '#/api/system/dict/dict-data-model';\nimport type { DictFallback } from '#/components/dict/src/type';\n\nimport { h } from 'vue';\n\nimport { JsonPreview } from '@vben/common-ui';\nimport {\n  AndroidIcon,\n  BaiduIcon,\n  ChromeIcon,\n  DefaultBrowserIcon,\n  DefaultOsIcon,\n  DingtalkIcon,\n  EdgeIcon,\n  FirefoxIcon,\n  IconifyIcon,\n  IPhoneIcon,\n  LinuxIcon,\n  MicromessengerIcon,\n  OperaIcon,\n  OSXIcon,\n  QuarkIcon,\n  SafariIcon,\n  SvgQQIcon,\n  UcIcon,\n  WindowsIcon,\n} from '@vben/icons';\n\nimport { Tag } from 'ant-design-vue';\n\nimport { DictTag } from '#/components/dict';\n\nimport { getDictOptions } from './dict';\n\n/**\n * 渲染标签\n * @param text 文字\n * @param color 颜色\n * @returns render\n */\nfunction renderTag(text: string, color?: string) {\n  return <Tag color={color}>{text}</Tag>;\n}\n\n/**\n *\n * @param tags 标签list\n * @param wrap 是否换行显示\n * @param [gap] 间隔\n * @returns render\n */\nexport function renderTags(tags: string[], wrap = false, gap = 1) {\n  return (\n    <div\n      class={['flex', wrap ? 'flex-col' : 'flex-row']}\n      style={{ gap: `${gap}px` }}\n    >\n      {tags.map((tag, index) => {\n        return <div key={index}>{renderTag(tag)}</div>;\n      })}\n    </div>\n  );\n}\n\n/**\n *\n * @param json json对象 接受object/string类型\n * @returns json预览\n */\nexport function renderJsonPreview(json: any) {\n  if (typeof json !== 'object' && typeof json !== 'string') {\n    return <span>{json}</span>;\n  }\n  if (typeof json === 'object') {\n    return <JsonPreview class=\"break-normal\" data={json} />;\n  }\n  try {\n    const obj = JSON.parse(json);\n    // 基本数据类型可以被转为json\n    if (typeof obj !== 'object') {\n      return <span>{obj}</span>;\n    }\n    return <JsonPreview class=\"break-normal\" data={obj} />;\n  } catch {\n    return <span>{json}</span>;\n  }\n}\n\n/**\n * iconify图标\n * @param icon icon名称\n * @returns render\n */\nexport function renderIcon(icon: string) {\n  return <IconifyIcon icon={icon}></IconifyIcon>;\n}\n\n/**\n * httpMethod标签\n * @param type method类型\n * @returns render\n */\nexport function renderHttpMethodTag(type: string) {\n  const method = type.toUpperCase();\n  const colors: { [key: string]: string } = {\n    DELETE: 'red',\n    GET: 'green',\n    POST: 'blue',\n    PUT: 'orange',\n  };\n\n  const color = colors[method] ?? 'default';\n  const title = `${method}请求`;\n\n  return <Tag color={color}>{title}</Tag>;\n}\n\nexport function renderDictTag(value: number | string, dicts: DictData[]) {\n  return <DictTag dicts={dicts} value={value}></DictTag>;\n}\n\n/**\n * render多个dictTag\n * @param value key数组 string[]类型\n * @param dicts 字典数组\n * @param wrap 是否需要换行显示\n * @param [gap] 间隔\n * @returns render\n */\nexport function renderDictTags(\n  value: string[],\n  dicts: DictData[],\n  wrap = true,\n  gap = 1,\n) {\n  if (!Array.isArray(value)) {\n    return <div>{value}</div>;\n  }\n  return (\n    <div\n      class={['flex', wrap ? 'flex-col' : 'flex-row']}\n      style={{ gap: `${gap}px` }}\n    >\n      {value.map((item, index) => {\n        return <div key={index}>{renderDictTag(item, dicts)}</div>;\n      })}\n    </div>\n  );\n}\n\nexport interface RenderDictOptions {\n  fallback?: DictFallback;\n}\n\n/**\n * 显示字典标签 一般是table使用\n * @param value 值\n * @param dictName dictName\n * @returns tag\n */\nexport function renderDict(\n  value: number | string,\n  dictName: string,\n  options?: RenderDictOptions,\n) {\n  const { fallback } = options ?? {};\n  const dictInfo = getDictOptions(dictName);\n  return <DictTag dicts={dictInfo} fallback={fallback} value={value}></DictTag>;\n}\n\nexport function renderIconSpan(\n  icon: ComponentType,\n  value: string,\n  center = false,\n  marginLeft = '2px',\n) {\n  const justifyCenter = center ? 'justify-center' : '';\n\n  return (\n    <span class={['flex', 'items-center', justifyCenter]}>\n      {h(icon)}\n      <span style={{ marginLeft }}>{value}</span>\n    </span>\n  );\n}\n\nconst osOptions = [\n  { icon: WindowsIcon, value: 'windows' },\n  { icon: LinuxIcon, value: 'linux' },\n  { icon: OSXIcon, value: 'osx' },\n  { icon: AndroidIcon, value: 'android' },\n  { icon: IPhoneIcon, value: 'iphone' },\n];\n\n/**\n * 浏览器图标\n * cn.hutool.http.useragent -> browers\n */\nconst browserOptions = [\n  { icon: ChromeIcon, value: 'chrome' },\n  { icon: EdgeIcon, value: 'edge' },\n  { icon: FirefoxIcon, value: 'firefox' },\n  { icon: OperaIcon, value: 'opera' },\n  { icon: SafariIcon, value: 'safari' },\n  { icon: MicromessengerIcon, value: 'micromessenger' },\n  { icon: MicromessengerIcon, value: 'windowswechat' },\n  { icon: QuarkIcon, value: 'quark' },\n  { icon: MicromessengerIcon, value: 'wxwork' },\n  { icon: SvgQQIcon, value: 'qq' },\n  { icon: DingtalkIcon, value: 'dingtalk' },\n  { icon: UcIcon, value: 'uc' },\n  { icon: BaiduIcon, value: 'baidu' },\n];\n\nexport function renderOsIcon(os: string, center = false) {\n  if (!os) {\n    return;\n  }\n  let current = osOptions.find((item) =>\n    os.toLocaleLowerCase().includes(item.value),\n  );\n  // windows要特殊处理\n  if (os.toLocaleLowerCase().includes('windows')) {\n    current = osOptions[0];\n  }\n  const icon = current ? current.icon : DefaultOsIcon;\n  return renderIconSpan(icon, os, center, '5px');\n}\n\nexport function renderBrowserIcon(browser: string, center = false) {\n  if (!browser) {\n    return;\n  }\n  const current = browserOptions.find((item) =>\n    browser.toLocaleLowerCase().includes(item.value),\n  );\n  const icon = current ? current.icon : DefaultBrowserIcon;\n  return renderIconSpan(icon, browser, center, '5px');\n}\n"
  },
  {
    "path": "apps/web-antd/src/views/_core/README.md",
    "content": "# \\_core\n\n此目录包含应用程序正常运行所需的基本视图。这些视图是应用程序布局中使用的视图。\n"
  },
  {
    "path": "apps/web-antd/src/views/_core/about/index.vue",
    "content": "<script lang=\"ts\" setup>\nimport { About } from '@vben/common-ui';\n\ndefineOptions({ name: 'About' });\n</script>\n\n<template>\n  <About />\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/_core/authentication/code-login.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { LoginCodeParams, VbenFormSchema } from '@vben/common-ui';\n\nimport type { TenantResp } from '#/api';\n\nimport { computed, onMounted, ref, useTemplateRef } from 'vue';\n\nimport { AuthenticationCodeLogin, z } from '@vben/common-ui';\nimport { DEFAULT_TENANT_ID } from '@vben/constants';\nimport { $t } from '@vben/locales';\n\nimport { Alert, message } from 'ant-design-vue';\n\nimport { tenantList } from '#/api';\nimport { sendSmsCode } from '#/api/core/captcha';\nimport { useAuthStore } from '#/store';\n\ndefineOptions({ name: 'CodeLogin' });\n\nconst loading = ref(false);\nconst CODE_LENGTH = 4;\n\nconst tenantInfo = ref<TenantResp>({\n  tenantEnabled: false,\n  voList: [],\n});\n\nconst codeLoginRef = useTemplateRef('codeLoginRef');\nasync function loadTenant() {\n  const resp = await tenantList();\n  tenantInfo.value = resp;\n  // 选中第一个租户\n  if (resp.tenantEnabled && resp.voList.length > 0) {\n    const firstTenantId = resp.voList[0]!.tenantId;\n    codeLoginRef.value?.getFormApi().setFieldValue('tenantId', firstTenantId);\n  }\n}\n\nonMounted(loadTenant);\n\nconst formSchema = computed((): VbenFormSchema[] => {\n  return [\n    {\n      component: 'VbenSelect',\n      componentProps: {\n        class: 'bg-background h-[40px] focus:border-primary',\n        contentClass: 'max-h-[256px] overflow-y-auto',\n        options: tenantInfo.value.voList?.map((item) => ({\n          label: item.companyName,\n          value: item.tenantId,\n        })),\n        placeholder: $t('authentication.selectAccount'),\n      },\n      defaultValue: DEFAULT_TENANT_ID,\n      dependencies: {\n        if: () => tenantInfo.value.tenantEnabled,\n        triggerFields: [''],\n      },\n      fieldName: 'tenantId',\n      label: $t('authentication.selectAccount'),\n      rules: z.string().min(1, { message: $t('authentication.selectAccount') }),\n    },\n    {\n      component: 'VbenInput',\n      componentProps: {\n        placeholder: $t('authentication.mobile'),\n      },\n      fieldName: 'phoneNumber',\n      label: $t('authentication.mobile'),\n      rules: z\n        .string()\n        .min(1, { message: $t('authentication.mobileTip') })\n        .refine((v) => /^\\d{11}$/.test(v), {\n          message: $t('authentication.mobileErrortip'),\n        }),\n    },\n    {\n      component: 'VbenPinInput',\n      componentProps(_, form) {\n        return {\n          createText: (countdown: number) => {\n            const text =\n              countdown > 0\n                ? $t('authentication.sendText', [countdown])\n                : $t('authentication.sendCode');\n            return text;\n          },\n          // 验证码长度\n          codeLength: CODE_LENGTH,\n          placeholder: $t('authentication.code'),\n          handleSendCode: async () => {\n            const { valid, value } = await form.validateField('phoneNumber');\n            if (!valid) {\n              // 必须抛异常 不能直接return\n              throw new Error('未填写手机号');\n            }\n            // 调用接口发送\n            await sendSmsCode(value);\n            message.success('验证码发送成功');\n          },\n        };\n      },\n      fieldName: 'code',\n      label: $t('authentication.code'),\n      rules: z.string().length(CODE_LENGTH, {\n        message: $t('authentication.codeTip', [CODE_LENGTH]),\n      }),\n    },\n  ];\n});\n\nconst authStore = useAuthStore();\nasync function handleLogin(values: LoginCodeParams) {\n  try {\n    const requestParams: any = {\n      tenantId: values.tenantId,\n      phonenumber: values.phoneNumber,\n      smsCode: values.code,\n      grantType: 'sms',\n    };\n    console.log('login params', requestParams);\n    await authStore.authLogin(requestParams);\n  } catch (error) {\n    console.error(error);\n  }\n}\n</script>\n\n<template>\n  <div>\n    <Alert\n      class=\"mb-4\"\n      how-icon\n      message=\"测试手机号: 15888888888 正确验证码: 1234 演示使用 不会真的发送\"\n      type=\"info\"\n    />\n    <AuthenticationCodeLogin\n      ref=\"codeLoginRef\"\n      :form-schema=\"formSchema\"\n      :loading=\"loading\"\n      @submit=\"handleLogin\"\n    />\n  </div>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/_core/authentication/forget-password.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { VbenFormSchema } from '@vben/common-ui';\nimport type { Recordable } from '@vben/types';\n\nimport { computed, ref } from 'vue';\n\nimport { AuthenticationForgetPassword, z } from '@vben/common-ui';\nimport { $t } from '@vben/locales';\n\ndefineOptions({ name: 'ForgetPassword' });\n\nconst loading = ref(false);\n\nconst formSchema = computed((): VbenFormSchema[] => {\n  return [\n    {\n      component: 'VbenInput',\n      componentProps: {\n        placeholder: 'example@example.com',\n      },\n      fieldName: 'email',\n      label: $t('authentication.email'),\n      rules: z\n        .string()\n        .min(1, { message: $t('authentication.emailTip') })\n        .email($t('authentication.emailValidErrorTip')),\n    },\n  ];\n});\n\nfunction handleSubmit(value: Recordable<any>) {\n  console.log('reset email:', value);\n}\n</script>\n\n<template>\n  <AuthenticationForgetPassword\n    :form-schema=\"formSchema\"\n    :loading=\"loading\"\n    @submit=\"handleSubmit\"\n  />\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/_core/authentication/login.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { LoginAndRegisterParams, VbenFormSchema } from '@vben/common-ui';\n\nimport type { TenantResp } from '#/api';\nimport type { CaptchaResponse } from '#/api/core/captcha';\n\nimport { computed, onMounted, ref, useTemplateRef } from 'vue';\n\nimport { AuthenticationLogin, z } from '@vben/common-ui';\nimport { DEFAULT_TENANT_ID } from '@vben/constants';\nimport { $t } from '@vben/locales';\n\nimport { omit } from 'lodash-es';\n\nimport { tenantList } from '#/api';\nimport { captchaImage } from '#/api/core/captcha';\nimport { useAuthStore } from '#/store';\n\nimport { useLoginTenantId } from '../oauth-common';\nimport OAuthLogin from './oauth-login.vue';\n\ndefineOptions({ name: 'Login' });\n\nconst authStore = useAuthStore();\n\nconst loginFormRef = useTemplateRef('loginFormRef');\n\nconst captchaInfo = ref<CaptchaResponse>({\n  captchaEnabled: false,\n  img: '',\n  uuid: '',\n});\n// 验证码loading\nconst captchaLoading = ref(false);\n\nasync function loadCaptcha() {\n  try {\n    captchaLoading.value = true;\n\n    const resp = await captchaImage();\n    if (resp.captchaEnabled) {\n      resp.img = `data:image/png;base64,${resp.img}`;\n    }\n    captchaInfo.value = resp;\n  } catch (error) {\n    console.error(error);\n  } finally {\n    captchaLoading.value = false;\n  }\n}\n\nconst tenantInfo = ref<TenantResp>({\n  tenantEnabled: false,\n  voList: [],\n});\n\nasync function loadTenant() {\n  const resp = await tenantList();\n  tenantInfo.value = resp;\n  // 选中第一个租户\n  if (resp.tenantEnabled && resp.voList.length > 0) {\n    const firstTenantId = resp.voList[0]!.tenantId;\n    loginFormRef.value?.getFormApi().setFieldValue('tenantId', firstTenantId);\n  }\n}\n\nonMounted(async () => {\n  await Promise.all([loadCaptcha(), loadTenant()]);\n});\n\nconst { loginTenantId } = useLoginTenantId();\n\nconst formSchema = computed((): VbenFormSchema[] => {\n  return [\n    {\n      component: 'VbenSelect',\n      componentProps: {\n        class: 'bg-background h-[40px] focus:border-primary',\n        contentClass: 'max-h-[256px] overflow-y-auto',\n        options: tenantInfo.value.voList?.map((item) => ({\n          label: item.companyName,\n          value: item.tenantId,\n        })),\n        placeholder: $t('authentication.selectAccount'),\n      },\n      defaultValue: DEFAULT_TENANT_ID,\n      dependencies: {\n        if: () => tenantInfo.value.tenantEnabled,\n        // 可以把这里当做watch\n        trigger: (model) => {\n          // 给oauth登录使用\n          loginTenantId.value = model?.tenantId ?? DEFAULT_TENANT_ID;\n        },\n        triggerFields: ['', 'tenantId'],\n      },\n      fieldName: 'tenantId',\n      label: $t('authentication.selectAccount'),\n      rules: z.string().min(1, { message: $t('authentication.selectAccount') }),\n    },\n    {\n      component: 'VbenInput',\n      componentProps: {\n        class: 'focus:border-primary',\n        placeholder: $t('authentication.usernameTip'),\n      },\n      defaultValue: 'admin',\n      fieldName: 'username',\n      label: $t('authentication.username'),\n      rules: z.string().min(1, { message: $t('authentication.usernameTip') }),\n    },\n    {\n      component: 'VbenInputPassword',\n      componentProps: {\n        class: 'focus:border-primary',\n        placeholder: $t('authentication.password'),\n      },\n      defaultValue: 'admin123',\n      fieldName: 'password',\n      label: $t('authentication.password'),\n      rules: z.string().min(5, { message: $t('authentication.passwordTip') }),\n    },\n    {\n      component: 'VbenInputCaptcha',\n      componentProps: {\n        captcha: captchaInfo.value.img,\n        class: 'focus:border-primary',\n        onCaptchaClick: loadCaptcha,\n        placeholder: $t('authentication.code'),\n        loading: captchaLoading.value,\n      },\n      dependencies: {\n        if: () => captchaInfo.value.captchaEnabled,\n        triggerFields: [''],\n      },\n      fieldName: 'code',\n      label: $t('authentication.code'),\n      rules: z\n        .string()\n        .min(1, { message: $t('authentication.verifyRequiredTip') }),\n    },\n  ];\n});\n\nasync function handleAccountLogin(values: LoginAndRegisterParams) {\n  try {\n    const requestParam: any = omit(values, ['code']);\n    // 验证码\n    if (captchaInfo.value.captchaEnabled) {\n      requestParam.code = values.code;\n      requestParam.uuid = captchaInfo.value.uuid;\n    }\n    // 登录\n    await authStore.authLogin(requestParam);\n  } catch (error) {\n    console.error(error);\n    // 处理验证码错误\n    if (error instanceof Error) {\n      // 刷新验证码\n      loginFormRef.value?.getFormApi().setFieldValue('code', '');\n      await loadCaptcha();\n    }\n  }\n}\n</script>\n\n<template>\n  <AuthenticationLogin\n    ref=\"loginFormRef\"\n    :form-schema=\"formSchema\"\n    :loading=\"authStore.loginLoading\"\n    :show-register=\"false\"\n    :show-third-party-login=\"true\"\n    @submit=\"handleAccountLogin\"\n  >\n    <!-- 可通过show-third-party-login控制是否显示第三方登录 -->\n    <template #third-party-login>\n      <OAuthLogin />\n    </template>\n  </AuthenticationLogin>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/_core/authentication/oauth-login.vue",
    "content": "<script setup lang=\"ts\">\nimport { $t } from '@vben/locales';\n\nimport { Col, Row, Tooltip } from 'ant-design-vue';\n\nimport { accountBindList, handleAuthBinding } from '../oauth-common';\n\ndefineOptions({\n  name: 'OAuthLogin',\n});\n</script>\n\n<template>\n  <div class=\"w-full sm:mx-auto md:max-w-md\">\n    <div class=\"my-4 flex items-center justify-between\">\n      <span class=\"border-input w-[35%] border-b dark:border-gray-600\"></span>\n      <span class=\"text-muted-foreground text-center text-xs uppercase\">\n        {{ $t('authentication.thirdPartyLogin') }}\n      </span>\n      <span class=\"border-input w-[35%] border-b dark:border-gray-600\"></span>\n    </div>\n    <Row class=\"enter-x flex items-center justify-evenly\">\n      <!-- todo 这里在点击登录时要disabled -->\n      <Col\n        v-for=\"item in accountBindList\"\n        :key=\"item.source\"\n        :span=\"4\"\n        class=\"my-2\"\n      >\n        <Tooltip :title=\"`${item.title}登录`\">\n          <span class=\"flex cursor-pointer items-center justify-center\">\n            <component\n              v-if=\"item.avatar\"\n              :is=\"item.avatar\"\n              :style=\"item?.style ?? {}\"\n              class=\"size-[24px]\"\n              @click=\"handleAuthBinding(item.source)\"\n            />\n          </span>\n        </Tooltip>\n      </Col>\n    </Row>\n  </div>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/_core/authentication/qrcode-login.vue",
    "content": "<script lang=\"ts\" setup>\nimport { AuthenticationQrCodeLogin } from '@vben/common-ui';\nimport { LOGIN_PATH } from '@vben/constants';\n\ndefineOptions({ name: 'QrCodeLogin' });\n</script>\n\n<template>\n  <AuthenticationQrCodeLogin :login-path=\"LOGIN_PATH\" />\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/_core/authentication/register.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { VbenFormSchema } from '@vben/common-ui';\nimport type { Recordable } from '@vben/types';\n\nimport { computed, h, ref } from 'vue';\n\nimport { AuthenticationRegister, z } from '@vben/common-ui';\nimport { $t } from '@vben/locales';\n\ndefineOptions({ name: 'Register' });\n\nconst loading = ref(false);\n\nconst formSchema = computed((): VbenFormSchema[] => {\n  return [\n    {\n      component: 'VbenInput',\n      componentProps: {\n        placeholder: $t('authentication.usernameTip'),\n      },\n      fieldName: 'username',\n      label: $t('authentication.username'),\n      rules: z.string().min(1, { message: $t('authentication.usernameTip') }),\n    },\n    {\n      component: 'VbenInputPassword',\n      componentProps: {\n        passwordStrength: true,\n        placeholder: $t('authentication.password'),\n      },\n      fieldName: 'password',\n      label: $t('authentication.password'),\n      renderComponentContent() {\n        return {\n          strengthText: () => $t('authentication.passwordStrength'),\n        };\n      },\n      rules: z.string().min(1, { message: $t('authentication.passwordTip') }),\n    },\n    {\n      component: 'VbenInputPassword',\n      componentProps: {\n        placeholder: $t('authentication.confirmPassword'),\n      },\n      dependencies: {\n        rules(values) {\n          const { password } = values;\n          return z\n            .string({ required_error: $t('authentication.passwordTip') })\n            .min(1, { message: $t('authentication.passwordTip') })\n            .refine((value) => value === password, {\n              message: $t('authentication.confirmPasswordTip'),\n            });\n        },\n        triggerFields: ['password'],\n      },\n      fieldName: 'confirmPassword',\n      label: $t('authentication.confirmPassword'),\n    },\n    {\n      component: 'VbenCheckbox',\n      fieldName: 'agreePolicy',\n      renderComponentContent: () => ({\n        default: () =>\n          h('span', [\n            $t('authentication.agree'),\n            h(\n              'a',\n              {\n                class: 'vben-link ml-1 ',\n                href: '',\n              },\n              `${$t('authentication.privacyPolicy')} & ${$t('authentication.terms')}`,\n            ),\n          ]),\n      }),\n      rules: z.boolean().refine((value) => !!value, {\n        message: $t('authentication.agreeTip'),\n      }),\n    },\n  ];\n});\n\nfunction handleSubmit(value: Recordable<any>) {\n  console.log('register submit:', value);\n}\n</script>\n\n<template>\n  <AuthenticationRegister\n    :form-schema=\"formSchema\"\n    :loading=\"loading\"\n    @submit=\"handleSubmit\"\n  />\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/_core/fallback/coming-soon.vue",
    "content": "<script lang=\"ts\" setup>\nimport { Fallback } from '@vben/common-ui';\n</script>\n\n<template>\n  <Fallback status=\"coming-soon\" />\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/_core/fallback/forbidden.vue",
    "content": "<script lang=\"ts\" setup>\nimport { Fallback } from '@vben/common-ui';\n\ndefineOptions({ name: 'Fallback403Demo' });\n</script>\n\n<template>\n  <Fallback status=\"403\" />\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/_core/fallback/internal-error.vue",
    "content": "<script lang=\"ts\" setup>\nimport { Fallback } from '@vben/common-ui';\n\ndefineOptions({ name: 'Fallback500Demo' });\n</script>\n\n<template>\n  <Fallback status=\"500\" />\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/_core/fallback/not-found.vue",
    "content": "<script lang=\"ts\" setup>\nimport { Fallback } from '@vben/common-ui';\n\ndefineOptions({ name: 'Fallback404Demo' });\n</script>\n\n<template>\n  <Fallback status=\"404\" />\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/_core/fallback/offline.vue",
    "content": "<script lang=\"ts\" setup>\nimport { Fallback } from '@vben/common-ui';\n\ndefineOptions({ name: 'FallbackOfflineDemo' });\n</script>\n\n<template>\n  <Fallback status=\"offline\" />\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/_core/oauth-common.ts",
    "content": "import type { Component, CSSProperties } from 'vue';\n\nimport { markRaw, ref } from 'vue';\n\nimport { DEFAULT_TENANT_ID } from '@vben/constants';\nimport {\n  GiteeIcon,\n  GithubOAuthIcon,\n  SvgMaxKeyIcon,\n  SvgTopiamIcon,\n  SvgWechatIcon,\n} from '@vben/icons';\n\nimport { createGlobalState } from '@vueuse/core';\n\nimport { authBinding } from '#/api/core/auth';\n\n/**\n * @description: oauth登录\n * @param title 标题\n * @param description 描述\n * @param avatar 图标\n * @param color 图标颜色可直接写英文颜色/hex\n */\nexport interface ListItem {\n  title: string;\n  description: string;\n  avatar?: Component;\n  style?: CSSProperties;\n}\n\n/**\n * @description: 绑定账号\n * @param source 来源 如gitee github 与后端的social-callback?source=xxx对应\n * @param bound 是否已经绑定\n */\nexport interface BindItem extends ListItem {\n  source: string;\n  bound?: boolean;\n}\n\n/**\n * 这里存储登录页的tenantId 由于个人中心也会用到 需要共享\n * 所以使用`createGlobalState`\n * @see https://vueuse.org/shared/createGlobalState/\n */\nexport const useLoginTenantId = createGlobalState(() => {\n  const loginTenantId = ref(DEFAULT_TENANT_ID);\n\n  return {\n    loginTenantId,\n  };\n});\n\n/**\n * 绑定授权\n * @param source\n */\nexport async function handleAuthBinding(source: string) {\n  const { loginTenantId } = useLoginTenantId();\n  // 这里返回打开授权页面的链接\n  const href = await authBinding(source, loginTenantId.value);\n  window.location.href = href;\n}\n\n/**\n * 账号绑定 list\n * 添加账号绑定只需要在这里增加即可\n */\nexport const accountBindList: BindItem[] = [\n  {\n    avatar: markRaw(GiteeIcon),\n    description: '绑定Gitee账号',\n    source: 'gitee',\n    title: 'Gitee',\n    style: { color: '#c71d23' },\n  },\n  {\n    avatar: markRaw(GithubOAuthIcon),\n    description: '绑定Github账号',\n    source: 'github',\n    title: 'Github',\n  },\n  {\n    avatar: markRaw(SvgMaxKeyIcon),\n    description: '绑定MaxKey账号',\n    source: 'maxkey',\n    title: 'MaxKey',\n  },\n  {\n    avatar: markRaw(SvgTopiamIcon),\n    description: '绑定topiam账号',\n    source: 'topiam',\n    title: 'Topiam',\n  },\n  {\n    avatar: markRaw(SvgWechatIcon),\n    description: '绑定wechat账号',\n    source: 'wechat',\n    title: 'Wechat',\n  },\n];\n"
  },
  {
    "path": "apps/web-antd/src/views/_core/profile/components/account-bind.vue",
    "content": "<script setup lang=\"tsx\">\nimport type { BindItem } from '../../oauth-common';\n\nimport type { SocialInfo } from '#/api/system/social/model';\n\nimport { onMounted, ref } from 'vue';\n\nimport { Alert, Avatar, Card, Empty, Modal, Tooltip } from 'ant-design-vue';\n\nimport { authUnbinding } from '#/api';\nimport { socialList } from '#/api/system/social';\n\nimport { accountBindList, handleAuthBinding } from '../../oauth-common';\n\ninterface BindItemWithInfo extends BindItem {\n  info?: SocialInfo;\n  bind?: boolean;\n}\n\nconst bindList = ref<BindItemWithInfo[]>([]);\n\nasync function loadData() {\n  const resp = await socialList();\n\n  const list: BindItemWithInfo[] = [...accountBindList];\n  list.forEach((item) => {\n    /**\n     * 平台转小写\n     */\n    item.bound = resp\n      .map((social) => social.source.toLowerCase())\n      .includes(item.source.toLowerCase());\n    /**\n     * 添加info信息\n     */\n    if (item.bound) {\n      item.info = resp.find(\n        (social) => social.source.toLowerCase() === item.source,\n      );\n    }\n  });\n  bindList.value = list;\n}\nonMounted(loadData);\n\n/**\n * 解绑账号\n */\nfunction handleUnbind(record: BindItemWithInfo) {\n  if (!record.info) {\n    return;\n  }\n  Modal.confirm({\n    content: `确定解绑[${record.source}]平台的[${record.info.userName}]账号吗？`,\n    async onOk() {\n      await authUnbinding(record.info!.id);\n      await loadData();\n    },\n    title: '提示',\n    type: 'warning',\n  });\n}\n\nconst simpleImage = Empty.PRESENTED_IMAGE_SIMPLE;\n</script>\n\n<template>\n  <div class=\"flex flex-col gap-4 pb-4\">\n    <div\n      v-if=\"bindList.length > 0\"\n      class=\"grid grid-cols-1 gap-4 lg:grid-cols-2 2xl:grid-cols-3\"\n    >\n      <Card\n        class=\"transition-shadow duration-300 hover:shadow-md\"\n        v-for=\"item in bindList\"\n        :key=\"item.source\"\n      >\n        <div class=\"flex w-full items-center gap-4\">\n          <component\n            :is=\"item.avatar\"\n            v-if=\"item.avatar\"\n            :style=\"item?.style ?? {}\"\n            class=\"size-[40px]\"\n          />\n          <div class=\"flex flex-1 items-center justify-between\">\n            <div class=\"flex flex-col\">\n              <h4 class=\"mb-[4px] text-[14px] text-black/85 dark:text-white/85\">\n                {{ item.title }}\n              </h4>\n              <span class=\"text-black/45 dark:text-white/45\">\n                <template v-if=\"!item.bound\">\n                  {{ item.description }}\n                </template>\n                <template v-if=\"item.bound && item.info\">\n                  <Tooltip>\n                    <template #title>\n                      <div class=\"flex flex-col items-center gap-2 p-2\">\n                        <Avatar :size=\"36\" :src=\"item.info.avatar\" />\n                        <div>绑定时间: {{ item.info.createTime }}</div>\n                      </div>\n                    </template>\n                    <div class=\"cursor-pointer\">\n                      已绑定: {{ item.info.nickName }}\n                    </div>\n                  </Tooltip>\n                </template>\n              </span>\n            </div>\n            <!-- TODO: 这里有优化空间? -->\n            <a-button\n              size=\"small\"\n              :type=\"item.bound ? 'default' : 'link'\"\n              @click=\"\n                item.bound ? handleUnbind(item) : handleAuthBinding(item.source)\n              \"\n            >\n              {{ item.bound ? '取消绑定' : '绑定' }}\n            </a-button>\n          </div>\n        </div>\n      </Card>\n    </div>\n    <div\n      v-if=\"bindList.length === 0\"\n      class=\"flex items-center justify-center rounded-lg border py-4\"\n    >\n      <Empty :image=\"simpleImage\" description=\"暂无可绑定的第三方账户\" />\n    </div>\n    <Alert message=\"说明\" type=\"info\">\n      <template #description>\n        <p>\n          需要添加第三方账号在\n          <span class=\"font-bold\">\n            apps\\web-antd\\src\\views\\_core\\oauth-common.ts\n          </span>\n          中accountBindList按模板添加\n        </p>\n      </template>\n    </Alert>\n  </div>\n</template>\n\n<style lang=\"scss\" scoped>\n/**\nlist item 间距\n*/\n:deep(.ant-list-item) {\n  padding: 6px;\n}\n</style>\n"
  },
  {
    "path": "apps/web-antd/src/views/_core/profile/components/base-setting.vue",
    "content": "<script setup lang=\"ts\">\nimport type { Recordable } from '@vben/types';\n\nimport type { UserProfile } from '#/api/system/profile/model';\n\nimport { onMounted } from 'vue';\n\nimport { DictEnum } from '@vben/constants';\nimport { useUserStore } from '@vben/stores';\n\nimport { pick } from 'lodash-es';\n\nimport { useVbenForm, z } from '#/adapter/form';\nimport { userProfileUpdate } from '#/api/system/profile';\nimport { useAuthStore } from '#/store';\nimport { getDictOptions } from '#/utils/dict';\n\nimport { emitter } from '../mitt';\n\nconst props = defineProps<{ profile: UserProfile }>();\n\nconst userStore = useUserStore();\nconst authStore = useAuthStore();\n\nconst [BasicForm, formApi] = useVbenForm({\n  commonConfig: {\n    labelWidth: 60,\n  },\n  handleSubmit,\n  resetButtonOptions: {\n    show: false,\n  },\n  schema: [\n    {\n      component: 'Input',\n      dependencies: {\n        show: () => false,\n        triggerFields: [''],\n      },\n      fieldName: 'userId',\n      label: '用户ID',\n      rules: 'required',\n    },\n    {\n      component: 'Input',\n      fieldName: 'nickName',\n      label: '昵称',\n      rules: 'required',\n    },\n    {\n      component: 'Input',\n      fieldName: 'email',\n      label: '邮箱',\n      rules: z.string().email('请输入正确的邮箱'),\n    },\n    {\n      component: 'RadioGroup',\n      componentProps: {\n        buttonStyle: 'solid',\n        options: getDictOptions(DictEnum.SYS_USER_SEX),\n        optionType: 'button',\n      },\n      defaultValue: '0',\n      fieldName: 'sex',\n      label: '性别',\n      rules: 'required',\n    },\n    {\n      component: 'Input',\n      fieldName: 'phonenumber',\n      label: '电话',\n      rules: z.string().regex(/^1[3-9]\\d{9}$/, '请输入正确的电话'),\n    },\n  ],\n  submitButtonOptions: {\n    content: '更新信息',\n  },\n});\n\nfunction buttonLoading(loading: boolean) {\n  formApi.setState((prev) => ({\n    ...prev,\n    submitButtonOptions: { ...prev.submitButtonOptions, loading },\n  }));\n}\n\nasync function handleSubmit(values: Recordable<any>) {\n  try {\n    buttonLoading(true);\n    await userProfileUpdate(values);\n    // 更新store\n    const userInfo = await authStore.fetchUserInfo();\n    userStore.setUserInfo(userInfo);\n    // 左边reload\n    emitter.emit('updateProfile');\n  } catch (error) {\n    console.error(error);\n  } finally {\n    buttonLoading(false);\n  }\n}\n\nonMounted(() => {\n  const data = pick(props.profile.user, [\n    'userId',\n    'nickName',\n    'email',\n    'phonenumber',\n    'sex',\n  ]);\n  formApi.setValues(data);\n});\n</script>\n\n<template>\n  <div class=\"mt-[16px] md:w-full lg:w-1/2 2xl:w-2/5\">\n    <BasicForm />\n  </div>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/_core/profile/components/online-device.vue",
    "content": "<script setup lang=\"ts\">\nimport type { Recordable } from '@vben/types';\n\nimport type { VxeGridProps } from '#/adapter/vxe-table';\n\nimport { Popconfirm } from 'ant-design-vue';\n\nimport { useVbenVxeGrid } from '#/adapter/vxe-table';\nimport { forceLogout2, onlineDeviceList } from '#/api/monitor/online';\nimport { columns } from '#/views/monitor/online/data';\n\nconst onlineDeviceColumns: VxeGridProps['columns'] = [\n  {\n    type: 'seq',\n    title: '序号',\n    width: 60,\n  },\n  // 个人中心不需要显示重复字段\n  ...(columns?.filter(\n    (item) => !['deptName', 'userName'].includes(item.field ?? ''),\n  ) ?? []),\n];\n\nconst gridOptions: VxeGridProps = {\n  columns: onlineDeviceColumns,\n  keepSource: true,\n  pagerConfig: {\n    enabled: false,\n  },\n  maxHeight: 600,\n  proxyConfig: {\n    ajax: {\n      query: async () => {\n        return await onlineDeviceList();\n      },\n    },\n  },\n  rowConfig: {\n    keyField: 'tokenId',\n    isCurrent: true,\n  },\n};\n\nconst [BasicTable, tableApi] = useVbenVxeGrid({ gridOptions });\n\nasync function handleForceOffline(row: Recordable<any>) {\n  await forceLogout2(row.tokenId);\n  await tableApi.query();\n}\n</script>\n\n<template>\n  <div>\n    <BasicTable table-title=\"我的在线设备\">\n      <template #action=\"{ row }\">\n        <Popconfirm\n          :title=\"`确认强制下线[${row.userName}]?`\"\n          placement=\"left\"\n          @confirm=\"handleForceOffline(row)\"\n        >\n          <a-button danger size=\"small\" type=\"link\">强制下线</a-button>\n        </Popconfirm>\n      </template>\n    </BasicTable>\n  </div>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/_core/profile/components/secure-setting.vue",
    "content": "<script setup lang=\"ts\">\nimport type { UpdatePasswordParam } from '#/api/system/profile/model';\n\nimport { Modal } from 'ant-design-vue';\nimport { omit } from 'lodash-es';\n\nimport { useVbenForm, z } from '#/adapter/form';\nimport { userUpdatePassword } from '#/api/system/profile';\nimport { useAuthStore } from '#/store';\n\nconst [BasicForm, formApi] = useVbenForm({\n  commonConfig: {\n    labelWidth: 90,\n  },\n  handleSubmit,\n  resetButtonOptions: {\n    show: false,\n  },\n  schema: [\n    {\n      component: 'InputPassword',\n      fieldName: 'oldPassword',\n      label: '旧密码',\n      rules: z\n        .string({ message: '请输入密码' })\n        .min(5, '密码长度不能少于5个字符')\n        .max(20, '密码长度不能超过20个字符'),\n    },\n    {\n      component: 'InputPassword',\n      dependencies: {\n        rules(values) {\n          return z\n            .string({ message: '请输入新密码' })\n            .min(5, '密码长度不能少于5个字符')\n            .max(20, '密码长度不能超过20个字符')\n            .refine(\n              (value) => value !== values.oldPassword,\n              '新旧密码不能相同',\n            );\n        },\n        triggerFields: ['newPassword', 'oldPassword'],\n      },\n      fieldName: 'newPassword',\n      label: '新密码',\n      rules: 'required',\n    },\n    {\n      component: 'InputPassword',\n      dependencies: {\n        rules(values) {\n          return z\n            .string({ message: '请输入确认密码' })\n            .min(5, '密码长度不能少于5个字符')\n            .max(20, '密码长度不能超过20个字符')\n            .refine(\n              (value) => value === values.newPassword,\n              '新密码和确认密码不一致',\n            );\n        },\n        triggerFields: ['newPassword', 'confirmPassword'],\n      },\n      fieldName: 'confirmPassword',\n      label: '确认密码',\n      rules: 'required',\n    },\n  ],\n  submitButtonOptions: {\n    content: '修改密码',\n  },\n});\n\nfunction buttonLoading(loading: boolean) {\n  formApi.setState((prev) => ({\n    ...prev,\n    submitButtonOptions: { ...prev.submitButtonOptions, loading },\n  }));\n}\n\nconst authStore = useAuthStore();\nfunction handleSubmit(values: any) {\n  Modal.confirm({\n    content: '确认修改密码吗？',\n    onOk: async () => {\n      try {\n        buttonLoading(true);\n        const data = omit(values, ['confirmPassword']) as UpdatePasswordParam;\n        await userUpdatePassword(data);\n        await authStore.logout(true);\n      } catch (error) {\n        console.error(error);\n      } finally {\n        buttonLoading(false);\n      }\n    },\n    title: '提示',\n  });\n}\n</script>\n\n<template>\n  <div class=\"mt-[16px] md:w-full lg:w-1/2 2xl:w-2/5\">\n    <BasicForm />\n  </div>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/_core/profile/index.vue",
    "content": "<script setup lang=\"ts\">\nimport type { UserProfile } from '#/api/system/profile/model';\n\nimport { onMounted, onUnmounted, ref } from 'vue';\n\nimport { Page } from '@vben/common-ui';\nimport { useUserStore } from '@vben/stores';\n\nimport { userProfile } from '#/api/system/profile';\nimport { useAuthStore } from '#/store';\n\nimport { emitter } from './mitt';\nimport ProfilePanel from './profile-panel.vue';\nimport SettingPanel from './setting-panel.vue';\n\nconst profile = ref<UserProfile>();\nasync function loadProfile() {\n  const resp = await userProfile();\n  profile.value = resp;\n}\n\nonMounted(loadProfile);\n\nconst authStore = useAuthStore();\nconst userStore = useUserStore();\n/**\n * ToDo 接口重复\n */\nasync function handleUploadFinish() {\n  // 重新加载用户信息\n  await loadProfile();\n  // 更新store\n  const userInfo = await authStore.fetchUserInfo();\n  userStore.setUserInfo(userInfo);\n}\n\nonMounted(() => emitter.on('updateProfile', loadProfile));\nonUnmounted(() => emitter.off('updateProfile'));\n</script>\n\n<template>\n  <Page>\n    <div class=\"flex flex-col gap-[16px] lg:flex-row\">\n      <!-- 左侧 -->\n      <ProfilePanel :profile=\"profile\" @upload-finish=\"handleUploadFinish\" />\n      <!-- 右侧 -->\n      <SettingPanel\n        v-if=\"profile\"\n        :profile=\"profile\"\n        class=\"flex-1 overflow-hidden\"\n      />\n    </div>\n  </Page>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/_core/profile/mitt.ts",
    "content": "import { mitt } from '@vben/utils';\n\ntype Events = {\n  updateProfile: void;\n};\n\nexport const emitter = mitt<Events>();\n"
  },
  {
    "path": "apps/web-antd/src/views/_core/profile/profile-panel.vue",
    "content": "<script setup lang=\"ts\">\nimport type { UserProfile } from '#/api/system/profile/model';\n\nimport { computed } from 'vue';\n\nimport { preferences, usePreferences } from '@vben/preferences';\n\nimport {\n  Card,\n  Descriptions,\n  DescriptionsItem,\n  Tag,\n  Tooltip,\n} from 'ant-design-vue';\n\nimport { userUpdateAvatar } from '#/api/system/profile';\nimport { CropperAvatar } from '#/components/cropper';\n\nconst props = defineProps<{ profile?: UserProfile }>();\n\ndefineEmits<{\n  // 头像上传完毕\n  uploadFinish: [];\n}>();\n\nconst avatar = computed(\n  () => props.profile?.user.avatar || preferences.app.defaultAvatar,\n);\n\nconst { isDark } = usePreferences();\nconst poetrySrc = computed(() => {\n  const color = isDark.value ? 'white' : 'gray';\n  return `https://v2.jinrishici.com/one.svg?font-size=12&color=${color}`;\n});\n</script>\n\n<template>\n  <Card :loading=\"!profile\" class=\"h-full lg:w-1/3\">\n    <div v-if=\"profile\" class=\"flex flex-col items-center gap-[24px]\">\n      <div class=\"flex flex-col items-center gap-[20px]\">\n        <Tooltip title=\"点击上传头像\">\n          <CropperAvatar\n            :show-btn=\"false\"\n            :upload-api=\"userUpdateAvatar\"\n            :value=\"avatar\"\n            width=\"120\"\n            @change=\"$emit('uploadFinish')\"\n          />\n        </Tooltip>\n        <div class=\"flex flex-col items-center gap-[8px]\">\n          <span class=\"text-foreground text-xl font-bold\">\n            {{ profile.user.nickName ?? '未知' }}\n          </span>\n          <!-- https://www.jinrishici.com/doc/#image -->\n          <img :src=\"poetrySrc\" />\n        </div>\n      </div>\n      <div class=\"px-[24px]\">\n        <Descriptions :column=\"1\">\n          <DescriptionsItem label=\"账号\">\n            {{ profile.user.userName }}\n          </DescriptionsItem>\n          <DescriptionsItem label=\"手机号码\">\n            {{ profile.user.phonenumber || '未绑定手机号' }}\n          </DescriptionsItem>\n          <DescriptionsItem label=\"邮箱\">\n            {{ profile.user.email || '未绑定邮箱' }}\n          </DescriptionsItem>\n          <DescriptionsItem label=\"部门\">\n            <Tag color=\"processing\">\n              {{ profile.user.deptName ?? '未分配部门' }}\n            </Tag>\n            <Tag v-if=\"profile.postGroup\" color=\"processing\">\n              {{ profile.postGroup }}\n            </Tag>\n          </DescriptionsItem>\n          <DescriptionsItem label=\"上次登录\">\n            {{ profile.user.loginDate }}\n          </DescriptionsItem>\n        </Descriptions>\n      </div>\n    </div>\n  </Card>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/_core/profile/setting-panel.vue",
    "content": "<script setup lang=\"ts\">\nimport { TabPane, Tabs } from 'ant-design-vue';\n\nimport AccountBind from './components/account-bind.vue';\nimport BaseSetting from './components/base-setting.vue';\nimport OnlineDevice from './components/online-device.vue';\nimport SecureSetting from './components/secure-setting.vue';\n\nconst settingList = [\n  {\n    component: BaseSetting,\n    key: '1',\n    name: '基本设置',\n  },\n  {\n    component: SecureSetting,\n    key: '2',\n    name: '安全设置',\n  },\n  {\n    component: AccountBind,\n    key: '3',\n    name: '账号绑定',\n  },\n  {\n    component: OnlineDevice,\n    key: '4',\n    name: '在线设备',\n  },\n];\n</script>\n\n<template>\n  <Tabs class=\"bg-background rounded-[var(--radius)] px-[16px] lg:flex-1\">\n    <TabPane v-for=\"item in settingList\" :key=\"item.key\" :tab=\"item.name\">\n      <component :is=\"item.component\" v-bind=\"$attrs\" />\n    </TabPane>\n  </Tabs>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/_core/social-callback/index.vue",
    "content": "<script setup lang=\"ts\">\nimport type { AuthApi } from '#/api';\n\nimport { onMounted } from 'vue';\nimport { useRoute, useRouter } from 'vue-router';\n\nimport { DEFAULT_TENANT_ID, LOGIN_PATH } from '@vben/constants';\nimport { useAccessStore } from '@vben/stores';\nimport { cn } from '@vben/utils';\n\nimport { message, Spin } from 'ant-design-vue';\n\nimport { authCallback } from '#/api';\nimport { useAuthStore } from '#/store';\n\nimport { accountBindList } from '../oauth-common';\n\nconst route = useRoute();\n\nconst code = route.query.code as string;\nconst state = route.query.state as string;\nconst stateJson = JSON.parse(atob(state));\n// 来源\nconst source = route.query.source as string;\n// 租户ID\nconst defaultTenantId = DEFAULT_TENANT_ID;\nconst tenantId = (stateJson.tenantId as string) ?? defaultTenantId;\nconst domain = stateJson.domain as string;\n\nconst accessStore = useAccessStore();\nconst authStore = useAuthStore();\n\nconst router = useRouter();\n\nonMounted(async () => {\n  // 如果域名不相等 则重定向处理\n  const host = window.location.host;\n  if (domain !== host) {\n    const urlFull = new URL(window.location.href);\n    urlFull.host = domain;\n    window.location.href = urlFull.toString();\n    return;\n  }\n\n  try {\n    // 已经实现的平台\n    const currentClient = accountBindList.find(\n      (item) => item.source === source,\n    );\n    if (!currentClient) {\n      message.error({ content: `未找到${source}平台` });\n      return;\n    }\n    const data: AuthApi.OAuthLoginParams = {\n      grantType: 'social',\n      socialCode: code,\n      socialState: state,\n      source,\n      tenantId,\n    };\n    // 没有token为登录 有token是授权\n    if (accessStore.accessToken) {\n      await authCallback(data);\n      message.success(`${source}授权成功`);\n    } else {\n      // 这里内部已经做了跳转到首页的操作\n      await authStore.authLogin(data as any);\n      message.success(`${source}登录成功`);\n    }\n  } catch (error) {\n    console.error(error);\n    // 500 你还没有绑定第三方账号，绑定后才可以登录！\n    setTimeout(() => {\n      router.push(LOGIN_PATH);\n    }, 1500);\n  }\n});\n</script>\n\n<template>\n  <div :class=\"cn('flex items-center justify-center', 'h-screen w-screen')\">\n    <Spin size=\"large\" />\n  </div>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/aiflow/README.md",
    "content": "# 工作流管理模块\n\n## 📁 目录结构\n\n```\nworkflow/\n├── index.vue           # 工作流列表页面（主页面）\n├── edit.vue            # 工作流编辑页面\n├── run.vue             # 工作流运行页面\n├── data.ts             # 表格列定义和查询表单配置\n└── README.md           # 本文档\n```\n\n## 🎯 页面说明\n\n### 1. 工作流列表页面 (index.vue)\n\n**路由：** `/workflow`\n\n**功能：**\n- ✅ 展示所有工作流（我的工作流和公开工作流）\n- ✅ 搜索和过滤工作流\n- ✅ 新建工作流\n- ✅ 编辑、运行、删除工作流\n- ✅ 批量删除工作流\n- ✅ 分页展示\n\n**设计风格：** 参考系统模型页面，使用VxeGrid表格\n\n### 2. 工作流编辑页面 (edit.vue)\n\n**路由：** `/workflow/edit/:uuid`\n\n**功能：**\n- ✅ 加载指定工作流数据\n- ✅ 可视化编辑工作流（拖拽节点、连接线等）\n- ✅ 保存工作流（保存后自动返回列表页）\n- ✅ 取消编辑（提示确认，返回列表页）\n- ✅ 显示页面标题和返回按钮\n\n**组件：** 使用 `WorkflowDesigner` 组件进行编辑\n\n### 3. 工作流运行页面 (run.vue)\n\n**路由：** `/workflow/run/:uuid`\n\n**功能：**\n- ✅ 加载工作流数据\n- ✅ 运行工作流\n- ✅ 展示运行结果和日志\n- ✅ 支持人工反馈\n\n**组件：** 使用 `RunDetail` 组件展示运行详情\n\n## 🔄 业务流程\n\n### 新建工作流\n\n```\n列表页 -> 点击\"新建工作流\" -> 后台创建 -> 跳转到编辑页 -> 编辑 -> 保存 -> 返回列表页\n```\n\n### 编辑工作流\n\n```\n列表页 -> 点击\"编辑\" -> 跳转到编辑页 -> 编辑 -> 保存 -> 返回列表页\n```\n\n### 运行工作流\n\n```\n列表页 -> 点击\"运行\" -> 跳转到运行页 -> 运行并展示结果\n```\n\n## 📊 数据结构\n\n### WorkflowInfo 工作流信息\n\n```typescript\ninterface WorkflowInfo {\n  uuid: string;                    // 工作流唯一标识\n  title: string;                   // 工作流名称\n  remark?: string;                 // 备注\n  isPublic: boolean;               // 是否公开\n  nodes: WorkflowNode[];           // 节点列表\n  edges: WorkflowEdge[];           // 连线列表\n  createTime?: string;             // 创建时间\n  updateTime?: string;             // 更新时间\n}\n```\n\n## 🔌 API 接口\n\n### 列表页使用的接口\n\n| 接口 | 方法 | 说明 |\n|------|------|------|\n| `workflowPage` | POST | 分页查询工作流列表 |\n| `workflowAdd` | POST | 新建工作流 |\n| `workflowUpdate` | POST | 更新工作流 |\n| `workflowDel` | POST | 删除工作流 |\n\n### 编辑页使用的接口\n\n| 接口 | 方法 | 说明 |\n|------|------|------|\n| `workflowGet` | GET | 获取工作流详情 |\n| `workflowUpdate` | POST | 保存工作流 |\n| `workflowComponents` | GET | 获取组件列表 |\n\n### 运行页使用的接口\n\n| 接口 | 方法 | 说明 |\n|------|------|------|\n| `workflowGet` | GET | 获取工作流详情 |\n| `workflowRuntimeResume` | POST | 恢复运行（人工反馈） |\n\n## 🎨 样式风格\n\n### 列表页\n- 使用 `Page` 组件包裹\n- 使用 `VxeGrid` 表格展示数据\n- 使用 `ghost-button` 按钮进行操作\n- 搜索表单采用响应式布局（grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4）\n\n### 编辑页\n- 使用 `Page` 组件包裹，显示标题和返回按钮\n- 全屏展示工作流设计器\n- 设计器高度：`calc(100vh - 120px)`\n\n### 运行页\n- 使用 `Page` 组件包裹，显示标题和返回按钮\n- 居中展示运行详情\n- 最大宽度：800px\n\n## 🚀 使用示例\n\n### 跳转到编辑页\n\n```typescript\nimport { useRouter } from 'vue-router';\n\nconst router = useRouter();\n\n// 编辑现有工作流\nrouter.push({\n  name: 'WorkflowEdit',\n  params: { uuid: workflowUuid },\n});\n```\n\n### 跳转到运行页\n\n```typescript\n// 运行工作流\nrouter.push({\n  name: 'WorkflowRun',\n  params: { uuid: workflowUuid },\n});\n```\n\n### 返回列表页\n\n```typescript\n// 从编辑页或运行页返回\nrouter.push({ name: 'Workflow' });\n```\n\n## ⚠️ 注意事项\n\n1. **保存自动返回：** 编辑页保存成功后会自动返回列表页\n2. **取消提示：** 点击返回按钮会提示确认，防止误操作导致数据丢失\n3. **加载状态：** 编辑页和运行页在加载数据时会显示loading状态\n4. **错误处理：** 所有API调用都包含错误处理，失败时会显示错误消息\n\n## 📝 TODO\n\n- [ ] 添加工作流模板功能\n- [ ] 支持工作流导入导出\n- [ ] 添加工作流版本管理\n- [ ] 优化运行日志展示\n\n"
  },
  {
    "path": "apps/web-antd/src/views/aiflow/data.ts",
    "content": "import type { FormSchemaGetter } from '#/adapter/form';\nimport type { VxeGridProps } from '#/adapter/vxe-table';\n\nimport { h } from 'vue';\nimport { Tag } from 'ant-design-vue';\n// import { getPopupContainer } from '@vben/utils';\n\nexport const querySchema: FormSchemaGetter = () => [\n  {\n    component: 'Input',\n    fieldName: 'title',\n    label: '工作流名称',\n  },\n  // {\n  //   component: 'Select',\n  //   componentProps: {\n  //     getPopupContainer,\n  //     options: [\n  //       { label: '全部', value: '' },\n  //       { label: '我的', value: 'my' },\n  //       { label: '公开', value: 'public' },\n  //     ],\n  //   },\n  //   fieldName: 'type',\n  //   label: '类型',\n  // },\n];\n\nexport const columns: VxeGridProps['columns'] = [\n  { type: 'checkbox', width: 60 },\n  {\n    title: '工作流名称',\n    field: 'title',\n    minWidth: 180,\n  },\n  {\n    title: '备注',\n    field: 'remark',\n    minWidth: 200,\n  },\n  {\n    title: '节点数量',\n    field: 'nodeCount',\n    width: 100,\n    slots: {\n      default: ({ row }) => {\n        const count = Array.isArray(row.nodes) ? row.nodes.length : 0;\n        return h('span', count.toString());\n      },\n    },\n  },\n  {\n    title: '是否公开',\n    field: 'isPublic',\n    width: 100,\n    slots: {\n      default: ({ row }) => {\n        if (row.isPublic) {\n          return h(Tag, { color: 'green' }, () => '公开');\n        }\n        return h(Tag, { color: 'default' }, () => '私有');\n      },\n    },\n  },\n  {\n    title: '创建时间',\n    field: 'createTime',\n    width: 180,\n  },\n  {\n    title: '更新时间',\n    field: 'updateTime',\n    width: 180,\n  },\n  {\n    field: 'action',\n    fixed: 'right',\n    slots: { default: 'action' },\n    title: '操作',\n    width: 320,\n  },\n];\n\n"
  },
  {
    "path": "apps/web-antd/src/views/aiflow/edit.vue",
    "content": "<script setup lang=\"ts\">\nimport { ref, onMounted, computed } from 'vue';\nimport { useRouter, useRoute } from 'vue-router';\nimport { message, Modal, Spin, Empty } from 'ant-design-vue';\nimport { Page } from '@vben/common-ui';\n\nimport WorkflowDesigner from '#/packages/workflow-designer/StandaloneWorkflowDesigner.vue';\nimport type {\n  WorkflowInfo,\n  WorkflowComponent,\n} from '#/packages/workflow-designer/types/index.d';\nimport { workflowApi } from '#/api/aiflow';\n\nconst router = useRouter();\nconst route = useRoute();\n\nconst workflow = ref<WorkflowInfo>({\n  uuid: '',\n  title: '新建工作流',\n  nodes: [],\n  edges: [],\n});\n\nconst wfComponents = ref<WorkflowComponent[]>([]);\nconst componentIdMap = ref<Record<number, string>>({});\nconst nameToIdMap = ref<Record<string, number>>({});\nconst saving = ref(false);\nconst loading = ref(true);\n\n// 页面标题\nconst pageTitle = computed(() => {\n  return workflow.value.title || '工作流编辑';\n});\n\n// 获取工作流组件列表\nasync function fetchWorkflowComponents() {\n  try {\n    const res = await workflowApi.workflowComponents();\n    wfComponents.value = res || [];\n    generateComponentIdMap();\n  } catch (error) {\n    message.error('获取工作流组件失败');\n    wfComponents.value = [\n      { name: 'Start', title: '开始' },\n      { name: 'End', title: '结束' },\n      { name: 'Answer', title: '回答' },\n    ];\n    generateComponentIdMap();\n  }\n}\n\n// 根据组件列表生成ID映射\nfunction generateComponentIdMap() {\n  const map: Record<number, string> = {};\n\n  wfComponents.value.forEach((component, index) => {\n    const id = (component as any).id ?? index;\n    map[id] = component.name;\n  });\n\n  if (Object.keys(map).length === 0) {\n    map[0] = 'Start';\n    map[1] = 'End';\n    map[2] = 'Answer';\n  }\n\n  componentIdMap.value = map;\n  nameToIdMap.value = Object.fromEntries(\n    Object.entries(map).map(([id, name]) => [name, Number(id)]),\n  );\n}\n\n// 加载工作流数据\nasync function loadWorkflow() {\n  const uuid = route.params.uuid as string;\n  if (!uuid) {\n    message.error('工作流ID不存在');\n    router.back();\n    return;\n  }\n\n  try {\n    loading.value = true;\n    const data = await workflowApi.workflowGet(uuid);\n    workflow.value = data;\n  } catch (error: any) {\n    message.error(error.message || '加载工作流失败');\n    router.back();\n  } finally {\n    loading.value = false;\n  }\n}\n\n// 保存工作流\nasync function handleSave(updated: WorkflowInfo) {\n  console.log('updated', updated.nodes);\n  if (saving.value) return;\n\n  if (updated?.nodes.length) {\n    // Step 1: Check if there is a node with title \"生成回答\"\n    const answerNode = updated.nodes.find((node) => node.title === '生成回答');\n\n    if (answerNode) {\n      // Step 2: Check if nodeConfig exists and prompt is not empty\n      const nodeConfig = answerNode.nodeConfig || {};\n      if (!nodeConfig.prompt || nodeConfig.prompt.trim() === '') {\n        message.error('节点“生成回答”的提示词不能为空');\n        return; // Stop saving if validation fails\n      }\n    }\n  }\n\n  saving.value = true;\n  try {\n    // 设置 workflowComponentId\n    updated.nodes.forEach((node) => {\n      if (node.wfComponent) {\n        node.workflowComponentId =\n          nameToIdMap.value[node.wfComponent?.name || ''] ?? 0;\n      }\n      if (node.nodeConfig === undefined) {\n        node.nodeConfig = {};\n      }\n    });\n\n    await workflowApi.workflowUpdate({\n      uuid: updated.uuid,\n      title: updated.title,\n      remark: (updated as any).remark ?? '',\n      isPublic: (updated as any).isPublic ?? false,\n      nodes: updated.nodes || [],\n      edges: updated.edges || [],\n    });\n\n    message.success('保存成功');\n\n    // 保存成功后返回列表页\n    router.push({ name: 'Workflow' });\n  } catch (error: any) {\n    message.error(error.message || '保存失败');\n  } finally {\n    saving.value = false;\n  }\n}\n\n// 取消编辑\nfunction handleCancel() {\n  Modal.confirm({\n    title: '提示',\n    content: '确定要取消编辑吗？未保存的修改将丢失。',\n    okText: '确定',\n    cancelText: '取消',\n    onOk: () => {\n      router.push({ name: 'Workflow' });\n    },\n  });\n}\n\n// 运行工作流\nasync function handleRun() {\n  Modal.confirm({\n    title: '提示',\n    content: '运行前需要先保存工作流，是否继续？',\n    okText: '保存并运行',\n    cancelText: '取消',\n    onOk: async () => {\n      try {\n        // 先保存工作流\n        await handleSave(workflow.value);\n        // 保存成功后跳转到运行页面\n        router.push({\n          name: 'WorkflowRun',\n          params: { uuid: workflow.value.uuid },\n        });\n      } catch (error) {\n        message.error('保存失败，无法运行工作流');\n      }\n    },\n  });\n}\n\nonMounted(async () => {\n  await fetchWorkflowComponents();\n  await loadWorkflow();\n});\n</script>\n\n<template>\n  <Page\n    :auto-content-height=\"true\"\n    :title=\"pageTitle\"\n    :show-back=\"true\"\n    @back=\"handleCancel\"\n  >\n    <div v-if=\"loading\" class=\"flex h-full items-center justify-center\">\n      <Spin size=\"large\" tip=\"加载中...\" />\n    </div>\n    <div v-else-if=\"workflow.uuid\" class=\"workflow-edit-page\">\n      <WorkflowDesigner\n        :workflow=\"workflow\"\n        :wf-components=\"wfComponents\"\n        :component-id-map=\"componentIdMap\"\n        :saving=\"saving\"\n        @save=\"handleSave\"\n        @run=\"handleRun\"\n      />\n    </div>\n    <div v-else class=\"flex h-full items-center justify-center\">\n      <Empty description=\"工作流加载失败\" />\n    </div>\n  </Page>\n</template>\n\n<style scoped>\n.workflow-edit-page {\n  width: 100%;\n  height: calc(100vh - 120px);\n  overflow: hidden;\n}\n</style>\n"
  },
  {
    "path": "apps/web-antd/src/views/aiflow/index.vue",
    "content": "<script setup lang=\"ts\">\nimport type { VbenFormProps } from '@vben/common-ui';\n\nimport type { VxeGridProps } from '#/adapter/vxe-table';\nimport type { WorkflowInfo } from '#/packages/workflow-designer/types/index.d';\n\nimport { useRouter } from 'vue-router';\n\nimport { Page, useVbenModal } from '@vben/common-ui';\nimport { getVxePopupContainer } from '@vben/utils';\n\nimport { Button, Modal, Popconfirm, Space } from 'ant-design-vue';\n\nimport { useVbenVxeGrid, vxeCheckboxChecked } from '#/adapter/vxe-table';\nimport { workflowApi } from '#/api/aiflow';\n\nimport { columns, querySchema } from './data';\nimport WorkflowModal from './workflow-modal.vue';\n\nconst router = useRouter();\n\nconst formOptions: VbenFormProps = {\n  commonConfig: {\n    labelWidth: 80,\n    componentProps: {\n      allowClear: true,\n    },\n  },\n  schema: querySchema(),\n  wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4',\n};\n\nconst gridOptions: VxeGridProps = {\n  checkboxConfig: {\n    highlight: true,\n    reserve: true,\n  },\n  columns,\n  height: 'auto',\n  keepSource: true,\n  pagerConfig: {},\n  proxyConfig: {\n    ajax: {\n      query: async ({ page }, formValues = {}) => {\n        const result = await workflowApi.workflowPage({\n          currentPage: page.currentPage,\n          pageSize: page.pageSize,\n          wfSearchReq: {\n            ...formValues,\n          },\n        });\n        // 转换数据结构以匹配 VXE 表格期望的格式\n        return {\n          rows: result.records,\n          total: result.total,\n        };\n      },\n    },\n  },\n  rowConfig: {\n    keyField: 'uuid',\n  },\n  id: 'workflow-index',\n};\n\nconst [BasicTable, tableApi] = useVbenVxeGrid({\n  formOptions,\n  gridOptions,\n});\n\nconst [WorkflowModalComponent, workflowModalApi] = useVbenModal({\n  connectedComponent: WorkflowModal,\n});\n\n// 新建工作流\nfunction handleAdd() {\n  workflowModalApi.setData({});\n  workflowModalApi.open();\n}\n\n// 编辑工作流基本信息\nfunction handleEditInfo(record: WorkflowInfo) {\n  workflowModalApi.setData({\n    uuid: record.uuid,\n    title: record.title,\n    remark: record.remark,\n    isPublic: record.isPublic,\n  });\n  workflowModalApi.open();\n}\n\n// Modal操作成功回调\nasync function handleReload(result?: any) {\n  // 判断是新建还是编辑\n  const data = workflowModalApi.getData() as { uuid?: string };\n  const isEdit = data?.uuid;\n\n  if (isEdit) {\n    // 编辑模式：刷新列表\n    await tableApi.query();\n  } else {\n    // 新建模式：跳转到编辑页面\n    // 注意：requestClient 会自动解析响应，直接返回 data 字段\n    const uuid = result?.uuid;\n    if (uuid) {\n      router.push({\n        name: 'WorkflowEdit',\n        params: { uuid },\n      });\n    } else {\n      console.error('创建工作流成功，但未返回 uuid', result);\n      await tableApi.query();\n    }\n  }\n}\n\n// 进入编辑器\nfunction handleEdit(record: WorkflowInfo) {\n  router.push({\n    name: 'WorkflowEdit',\n    params: { uuid: record.uuid },\n  });\n}\n\n// 运行工作流\nfunction handleRun(record: WorkflowInfo) {\n  router.push({\n    name: 'WorkflowRun',\n    params: { uuid: record.uuid },\n  });\n}\n\n// 删除工作流\nasync function handleDelete(row: WorkflowInfo) {\n  await workflowApi.workflowDel(row.uuid);\n  await tableApi.query();\n}\n\n// 批量删除\nfunction handleMultiDelete() {\n  const rows = tableApi.grid.getCheckboxRecords();\n  const uuids = rows.map((row: WorkflowInfo) => row.uuid);\n  Modal.confirm({\n    title: '提示',\n    okType: 'danger',\n    content: `确认删除选中的${uuids.length}条记录吗？`,\n    onOk: async () => {\n      await Promise.all(uuids.map((uuid) => workflowApi.workflowDel(uuid)));\n      await tableApi.query();\n    },\n  });\n}\n</script>\n\n<template>\n  <Page :auto-content-height=\"true\">\n    <BasicTable table-title=\"工作流列表\">\n      <template #toolbar-tools>\n        <Space>\n          <Button\n            :disabled=\"!vxeCheckboxChecked(tableApi)\"\n            danger\n            type=\"primary\"\n            @click=\"handleMultiDelete\"\n          >\n            批量删除\n          </Button>\n          <Button type=\"primary\" @click=\"handleAdd\"> 新建工作流 </Button>\n        </Space>\n      </template>\n      <template #action=\"{ row }\">\n        <Space>\n          <ghost-button @click.stop=\"handleEditInfo(row)\"> 编辑 </ghost-button>\n          <ghost-button @click.stop=\"handleEdit(row)\"> 设计 </ghost-button>\n          <ghost-button @click.stop=\"handleRun(row)\"> 运行 </ghost-button>\n          <Popconfirm\n            :get-popup-container=\"getVxePopupContainer\"\n            placement=\"left\"\n            title=\"确认删除该工作流吗？\"\n            @confirm=\"handleDelete(row)\"\n          >\n            <ghost-button danger @click.stop=\"\"> 删除 </ghost-button>\n          </Popconfirm>\n        </Space>\n      </template>\n    </BasicTable>\n    <WorkflowModalComponent @reload=\"handleReload\" />\n  </Page>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/aiflow/run.vue",
    "content": "<script setup lang=\"ts\">\nimport type { WorkflowInfo } from '#/packages/workflow-designer/types/index.d';\n\nimport { onMounted, ref } from 'vue';\nimport { useRoute, useRouter } from 'vue-router';\n\nimport { Page } from '@vben/common-ui';\n\nimport { Empty, message, Spin } from 'ant-design-vue';\n\nimport { workflowApi } from '#/api/aiflow';\nimport RunDetail from '#/packages/workflow-designer/components/RunDetail.vue';\n\nconst router = useRouter();\nconst route = useRoute();\n\nconst workflow = ref<WorkflowInfo>({\n  uuid: '',\n  title: '工作流运行',\n  nodes: [],\n  edges: [],\n});\n\nconst loading = ref(true);\n\n// 加载工作流数据\nasync function loadWorkflow() {\n  const uuid = route.params.uuid as string;\n  if (!uuid) {\n    message.error('工作流ID不存在');\n    router.back();\n    return;\n  }\n\n  try {\n    loading.value = true;\n    const data = await workflowApi.workflowGet(uuid);\n    workflow.value = data;\n  } catch (error: any) {\n    message.error(error.message || '加载工作流失败');\n    router.back();\n  } finally {\n    loading.value = false;\n  }\n}\n\nonMounted(async () => {\n  await loadWorkflow();\n});\n</script>\n\n<template>\n  <Page\n    :auto-content-height=\"true\"\n    :title=\"`运行工作流 - ${workflow.title}`\"\n    :show-back=\"true\"\n  >\n    <div v-if=\"loading\" class=\"flex h-full items-center justify-center\">\n      <Spin size=\"large\" tip=\"加载中...\" />\n    </div>\n    <div v-else-if=\"workflow.uuid\" class=\"rounded-lg bg-white p-6\">\n      <RunDetail :workflow=\"workflow\" />\n    </div>\n    <div v-else class=\"flex h-full items-center justify-center\">\n      <Empty description=\"工作流加载失败\" />\n    </div>\n  </Page>\n</template>\n\n<style scoped>\n.workflow-run-page {\n  width: 100%;\n  max-width: 800px;\n  margin: 0 auto;\n}\n</style>\n"
  },
  {
    "path": "apps/web-antd/src/views/aiflow/workflow-modal.vue",
    "content": "<script setup lang=\"ts\">\nimport { computed, ref } from 'vue';\nimport { Form, Input, Switch, message } from 'ant-design-vue';\nimport { useVbenModal } from '@vben/common-ui';\n\nimport { workflowApi } from '#/api/aiflow';\n\nconst emit = defineEmits<{\n  (e: 'reload', result: any): void;\n}>();\n\nconst defaultValues = {\n  uuid: '',\n  title: '',\n  remark: '',\n  isPublic: false,\n};\n\nconst formData = ref({ ...defaultValues });\nconst isEdit = ref(false);\n\nconst title = computed(() => isEdit.value ? '编辑工作流' : '新建工作流');\n\nconst [BasicModal, modalApi] = useVbenModal({\n  class: 'w-[550px]',\n  fullscreenButton: false,\n  closeOnClickModal: false,\n  onClosed: handleCancel,\n  onConfirm: handleConfirm,\n  onOpenChange: async (isOpen) => {\n    if (!isOpen) {\n      return null;\n    }\n\n    const data = modalApi.getData() as { uuid?: string; title?: string; remark?: string; isPublic?: boolean };\n    isEdit.value = !!data?.uuid;\n\n    if (data) {\n      formData.value = {\n        uuid: data.uuid || '',\n        title: data.title || '',\n        remark: data.remark || '',\n        isPublic: data.isPublic || false,\n      };\n    } else {\n      formData.value = { ...defaultValues };\n    }\n  },\n});\n\nasync function handleConfirm() {\n  // 表单验证\n  if (!formData.value.title || !formData.value.title.trim()) {\n    message.error('请输入工作流名称');\n    return;\n  }\n\n  try {\n    modalApi.modalLoading(true);\n\n    let result;\n    if (isEdit.value) {\n      // 编辑模式：更新基本信息\n      result = await workflowApi.workflowBaseInfoUpdate({\n        uuid: formData.value.uuid,\n        title: formData.value.title.trim(),\n        remark: formData.value.remark.trim(),\n        isPublic: formData.value.isPublic,\n      });\n    } else {\n      // 新建模式：创建新工作流\n      result = await workflowApi.workflowAdd({\n        title: formData.value.title.trim(),\n        remark: formData.value.remark.trim(),\n        isPublic: formData.value.isPublic,\n      });\n    }\n\n    message.success(isEdit.value ? '更新成功' : '创建成功');\n    emit('reload', result);\n    await handleCancel();\n  } catch (error: any) {\n    message.error(error.message || (isEdit.value ? '更新失败' : '创建失败'));\n  } finally {\n    modalApi.modalLoading(false);\n  }\n}\n\nasync function handleCancel() {\n  modalApi.close();\n  formData.value = { ...defaultValues };\n}\n</script>\n\n<template>\n  <BasicModal :title=\"title\">\n    <Form\n      :model=\"formData\"\n      :label-col=\"{ span: 5 }\"\n      :wrapper-col=\"{ span: 19 }\"\n    >\n      <Form.Item label=\"工作流名称\" required>\n        <Input\n          v-model:value=\"formData.title\"\n          placeholder=\"请输入工作流名称\"\n          :maxlength=\"100\"\n          show-count\n        />\n      </Form.Item>\n\n      <Form.Item label=\"备注说明\">\n        <Input.TextArea\n          v-model:value=\"formData.remark\"\n          placeholder=\"请输入备注说明\"\n          :rows=\"3\"\n          :maxlength=\"500\"\n          show-count\n        />\n      </Form.Item>\n\n      <Form.Item label=\"是否公开\">\n        <div class=\"flex items-center\">\n          <Switch v-model:checked=\"formData.isPublic\" />\n          <span class=\"ml-2 text-gray-500 text-sm\">\n            公开后其他用户可以查看和使用\n          </span>\n        </div>\n      </Form.Item>\n    </Form>\n  </BasicModal>\n</template>\n\n<style scoped>\n:deep(.ant-form-item) {\n  margin-bottom: 20px;\n}\n</style>\n\n"
  },
  {
    "path": "apps/web-antd/src/views/chat/message/data.ts",
    "content": "import type { FormSchemaGetter } from '#/adapter/form';\nimport type { VxeGridProps } from '#/adapter/vxe-table';\n\nexport const querySchema: FormSchemaGetter = () => [\n  {\n    component: 'Input',\n    fieldName: 'modelName',\n    label: '模型名称',\n  },\n];\n\n// 需要使用i18n注意这里要改成getter形式 否则切换语言不会刷新\n// export const columns: () => VxeGridProps['columns'] = () => [\nexport const columns: VxeGridProps['columns'] = [\n  { type: 'checkbox', width: 60 },\n  {\n    title: '主键',\n    field: 'id',\n  },\n  {\n    title: '用户id',\n    field: 'userId',\n  },\n  {\n    title: '消息内容',\n    field: 'content',\n  },\n  {\n    title: '对话角色',\n    field: 'role',\n  },\n  {\n    title: '累计 Tokens',\n    field: 'totalTokens',\n  },\n  {\n    title: '模型名称',\n    field: 'modelName',\n  },\n  {\n    title: '备注',\n    field: 'remark',\n  },\n  {\n    field: 'action',\n    fixed: 'right',\n    slots: { default: 'action' },\n    title: '操作',\n    width: 180,\n  },\n];\n"
  },
  {
    "path": "apps/web-antd/src/views/chat/message/index.vue",
    "content": "<script setup lang=\"ts\">\nimport type { Recordable } from '@vben/types';\n\nimport { ref } from 'vue';\n\nimport { Page, useVbenModal, type VbenFormProps } from '@vben/common-ui';\nimport { getVxePopupContainer } from '@vben/utils';\n\nimport { Modal, Popconfirm, Space } from 'ant-design-vue';\n\nimport {\n  useVbenVxeGrid,\n  vxeCheckboxChecked,\n  type VxeGridProps\n} from '#/adapter/vxe-table';\n\nimport {\n  messageExport,\n  messageList,\n  messageRemove,\n} from '#/api/chat/message';\nimport type { MessageForm } from '#/api/chat/message/model';\nimport { commonDownloadExcel } from '#/utils/file/download';\n\nimport { columns, querySchema } from './data';\n\nconst formOptions: VbenFormProps = {\n  commonConfig: {\n    labelWidth: 80,\n    componentProps: {\n      allowClear: true,\n    },\n  },\n  schema: querySchema(),\n  wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4',\n};\n\nconst gridOptions: VxeGridProps = {\n  checkboxConfig: {\n    highlight: true,\n    reserve: true,\n  },\n  columns,\n  height: 'auto',\n  keepSource: true,\n  pagerConfig: {},\n  proxyConfig: {\n    ajax: {\n      query: async ({ page }, formValues = {}) => {\n        return await messageList({\n          pageNum: page.currentPage,\n          pageSize: page.pageSize,\n          ...formValues,\n        });\n      },\n    },\n  },\n  rowConfig: {\n    keyField: 'id',\n  },\n  id: 'system-message-index'\n};\n\nconst [BasicTable, tableApi] = useVbenVxeGrid({\n  formOptions,\n  gridOptions,\n});\n\nasync function handleDelete(row: Required<MessageForm>) {\n  await messageRemove(row.id);\n  await tableApi.query();\n}\n\nfunction handleMultiDelete() {\n  const rows = tableApi.grid.getCheckboxRecords();\n  const ids = rows.map((row: Required<MessageForm>) => row.id);\n  Modal.confirm({\n    title: '提示',\n    okType: 'danger',\n    content: `确认删除选中的${ids.length}条记录吗？`,\n    onOk: async () => {\n      await messageRemove(ids);\n      await tableApi.query();\n    },\n  });\n}\n\nfunction handleDownloadExcel() {\n  commonDownloadExcel(messageExport, '聊天消息数据', tableApi.formApi.form.values, {\n    fieldMappingTime: formOptions.fieldMappingTime,\n  });\n}\n</script>\n\n<template>\n  <Page :auto-content-height=\"true\">\n    <BasicTable table-title=\"聊天消息列表\">\n      <template #toolbar-tools>\n        <Space>\n          <a-button\n            v-access:code=\"['system:message:export']\"\n            @click=\"handleDownloadExcel\"\n          >\n            {{ $t('pages.common.export') }}\n          </a-button>\n          <a-button\n            :disabled=\"!vxeCheckboxChecked(tableApi)\"\n            danger\n            type=\"primary\"\n            v-access:code=\"['system:message:remove']\"\n            @click=\"handleMultiDelete\">\n            {{ $t('pages.common.delete') }}\n          </a-button>\n        </Space>\n      </template>\n      <template #action=\"{ row }\">\n        <Space>\n          <Popconfirm\n            :get-popup-container=\"getVxePopupContainer\"\n            placement=\"left\"\n            title=\"确认删除？\"\n            @confirm=\"handleDelete(row)\"\n          >\n            <ghost-button\n              danger\n              v-access:code=\"['system:message:remove']\"\n              @click.stop=\"\"\n            >\n              {{ $t('pages.common.delete') }}\n            </ghost-button>\n          </Popconfirm>\n        </Space>\n      </template>\n    </BasicTable>\n  </Page>\n</template>\n\n"
  },
  {
    "path": "apps/web-antd/src/views/chat/message/message-modal.vue",
    "content": "<!--\n使用 Ant Design Vue 原生 Form 组件生成表单\n详细用法参考: https://antdv.com/components/form-cn\n注意: 如果 VSCode 配置了自动移除未使用的导入，可能会误删某些组件导入\n-->\n<script setup lang=\"ts\">\nimport type { RuleObject } from 'ant-design-vue/es/form';\nimport { computed, ref } from 'vue';\n\nimport { Input, Textarea, Select, RadioGroup, CheckboxGroup, DatePicker, Form, FormItem } from 'ant-design-vue';\nimport { ImageUpload, FileUpload } from '#/components/upload';\nimport { Tinymce } from '#/components/tinymce';\nimport { getPopupContainer } from '@vben/utils';\nimport { pick } from 'lodash-es';\n\nimport { useVbenModal } from '@vben/common-ui';\nimport { $t } from '@vben/locales';\nimport { cloneDeep } from '@vben/utils';\nimport { DictEnum } from '@vben/constants';\n\nimport { useVbenForm } from '#/adapter/form';\nimport { messageAdd, messageInfo, messageUpdate } from '#/api/chat/message';\nimport type { MessageForm } from '#/api/chat/message/model';\nimport { getDictItems } from '#/api/system/dict';\n\nconst emit = defineEmits<{ reload: [] }>();\n\nconst isUpdate = ref(false);\nconst title = computed(() => {\n  return isUpdate.value ? $t('pages.common.edit') : $t('pages.common.add');\n});\n\n// 计费类型字典选项\nconst billingTypeOptions = ref<Array<{ label: string; value: string }>>([]);\n\n// 获取计费类型字典数据\nasync function fetchBillingTypeDict() {\n  try {\n    const dictData = await getDictItems(DictEnum.SYS_MODEL_BILLING);\n    billingTypeOptions.value = dictData.map((item: any) => ({\n      label: item.dictLabel,\n      value: item.dictValue,\n    }));\n  } catch (error) {\n    console.error('获取计费类型字典失败:', error);\n  }\n}\n\n/**\n * 定义默认值 用于reset\n */\nconst defaultValues: Partial<MessageForm> = {\n  id: undefined,\n  sessionId: undefined,\n  userId: undefined,\n  content: undefined,\n  role: undefined,\n  deductCost: undefined,\n  totalTokens: undefined,\n  modelName: undefined,\n  billingType: undefined,\n  remark: undefined,\n}\n\n/**\n * 表单数据ref\n */\nconst formData = ref(defaultValues);\n\ntype AntdFormRules<T> = Partial<Record<keyof T, RuleObject[]>> & {\n  [key: string]: RuleObject[];\n};\n/**\n * 表单校验规则\n */\nconst formRules = ref<AntdFormRules<MessageForm>>({\n    userId: [\n      { required: true, message: \"用户id不能为空\" }\n    ],\n});\n\n/**\n * useForm解构出表单方法\n */\nconst { validate, validateInfos, resetFields } = Form.useForm(\n  formData,\n  formRules,\n);\n\nconst [BasicModal, modalApi] = useVbenModal({\n  class: 'w-[550px]',\n  fullscreenButton: false,\n  closeOnClickModal: false,\n  onClosed: handleCancel,\n  onConfirm: handleConfirm,\n  onOpenChange: async (isOpen) => {\n    if (!isOpen) {\n      return null;\n    }\n    modalApi.modalLoading(true);\n\n    // 初始化字典数据\n    if (billingTypeOptions.value.length === 0) {\n      await fetchBillingTypeDict();\n    }\n\n    const { id } = modalApi.getData() as { id?: number | string };\n    isUpdate.value = !!id;\n\n    if (isUpdate.value && id) {\n      const record = await messageInfo(id);\n      // 只赋值存在的字段\n      const filterRecord = pick(record, Object.keys(defaultValues));\n      formData.value = filterRecord;\n    }\n\n    modalApi.modalLoading(false);\n  },\n});\n\nasync function handleConfirm() {\n  try {\n    modalApi.modalLoading(true);\n    await validate();\n    // 可能会做数据处理 使用cloneDeep深拷贝\n    const data = cloneDeep(formData.value);\n    await (isUpdate.value ? messageUpdate(data) : messageAdd(data));\n    emit('reload');\n    await handleCancel();\n  } catch (error) {\n    console.error(error);\n  } finally {\n    modalApi.modalLoading(false);\n  }\n}\n\nasync function handleCancel() {\n  modalApi.close();\n  formData.value = cloneDeep(defaultValues);\n  resetFields();\n}\n</script>\n\n<template>\n  <BasicModal :title=\"title\">\n    <Form :label-col=\"{ span: 4 }\">\n      <FormItem label=\"会话id\" v-bind=\"validateInfos.sessionId\">\n        <Input v-model:value=\"formData.sessionId\" :placeholder=\"$t('ui.formRules.required')\" />\n      </FormItem>\n      <FormItem label=\"用户id\" v-bind=\"validateInfos.userId\">\n        <Input v-model:value=\"formData.userId\" :placeholder=\"$t('ui.formRules.required')\" />\n      </FormItem>\n      <FormItem label=\"消息内容\" v-bind=\"validateInfos.content\">\n        <Tinymce\n          :options=\"{ readonly: false }\"\n          v-model=\"formData.content\"\n        />\n      </FormItem>\n      <FormItem label=\"对话角色\" v-bind=\"validateInfos.role\">\n        <Input v-model:value=\"formData.role\" :placeholder=\"$t('ui.formRules.required')\" />\n      </FormItem>\n      <FormItem label=\"扣除金额\" v-bind=\"validateInfos.deductCost\">\n        <Input v-model:value=\"formData.deductCost\" :placeholder=\"$t('ui.formRules.required')\" />\n      </FormItem>\n      <FormItem label=\"累计 Tokens\" v-bind=\"validateInfos.totalTokens\">\n        <Input v-model:value=\"formData.totalTokens\" :placeholder=\"$t('ui.formRules.required')\" />\n      </FormItem>\n      <FormItem label=\"模型名称\" v-bind=\"validateInfos.modelName\">\n        <Input v-model:value=\"formData.modelName\" :placeholder=\"$t('ui.formRules.required')\" />\n      </FormItem>\n      <FormItem label=\"计费类型\" v-bind=\"validateInfos.billingType\">\n        <Select\n          v-model:value=\"formData.billingType\"\n          :options=\"billingTypeOptions\"\n          :get-popup-container=\"getPopupContainer\"\n          :placeholder=\"$t('ui.formRules.selectRequired')\"\n        />\n      </FormItem>\n      <FormItem label=\"备注\" v-bind=\"validateInfos.remark\">\n        <Textarea\n          v-model:value=\"formData.remark\"\n          :placeholder=\"$t('ui.formRules.required')\"\n          :rows=\"4\"\n        />\n      </FormItem>\n    </Form>\n  </BasicModal>\n</template>\n\n"
  },
  {
    "path": "apps/web-antd/src/views/chat/model/data.ts",
    "content": "import type { FormSchemaGetter } from '#/adapter/form';\nimport type { VxeGridProps } from '#/adapter/vxe-table';\n\nexport const querySchema: FormSchemaGetter = () => [\n  {\n    component: 'Input',\n    fieldName: 'modelName',\n    label: '模型名称',\n  },\n];\n\n/**\n * 表格列配置\n * 如果需要使用 i18n 国际化，请使用 getter 函数形式，否则切换语言时列配置不会刷新\n * 使用方式: export const columns: () => VxeGridProps['columns'] = () => [...]\n */\nexport const columns: VxeGridProps['columns'] = [\n  { type: 'checkbox', width: 60 },\n  {\n    title: '模型名称',\n    field: 'modelName',\n  },\n  {\n    title: '模型描述',\n    field: 'modelDescribe',\n  },\n  {\n    title: '模型供应商',\n    field: 'providerCode',\n  },\n  {\n    title: '模型分类',\n    field: 'category',\n    slots: { default: 'category' },\n  },\n\n  {\n    field: 'action',\n    fixed: 'right',\n    slots: { default: 'action' },\n    title: '操作',\n    width: 180,\n  },\n];\n"
  },
  {
    "path": "apps/web-antd/src/views/chat/model/index.vue",
    "content": "<script setup lang=\"ts\">\nimport type { Recordable } from '@vben/types';\n\nimport { ref } from 'vue';\n\nimport { Page, useVbenModal, type VbenFormProps } from '@vben/common-ui';\nimport { DictEnum } from '@vben/constants';\nimport { getVxePopupContainer } from '@vben/utils';\n\nimport { Modal, Popconfirm, Space } from 'ant-design-vue';\n\nimport {\n  useVbenVxeGrid,\n  vxeCheckboxChecked,\n  type VxeGridProps\n} from '#/adapter/vxe-table';\n\nimport {\n  modelExport,\n  modelList,\n  modelRemove,\n} from '#/api/chat/model';\nimport type { ModelForm } from '#/api/chat/model/model';\nimport { commonDownloadExcel } from '#/utils/file/download';\nimport { getDictOptions } from '#/utils/dict';\n\nimport modelModal from './model-modal.vue';\nimport { columns, querySchema } from './data';\n\nconst formOptions: VbenFormProps = {\n  commonConfig: {\n    labelWidth: 80,\n    componentProps: {\n      allowClear: true,\n    },\n  },\n  schema: querySchema(),\n  wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4',\n  // 处理区间选择器 RangePicker 时间格式映射\n  // 将一个时间区间字段映射为两个独立的开始/结束时间字段，用于搜索和导出\n  // 示例: 将 createTime 字段映射为 params[beginTime] 和 params[endTime]\n  // fieldMappingTime: [\n  //   [\n  //     'createTime', // 表单中的字段名\n  //     ['params[beginTime]', 'params[endTime]'], // 映射后的字段名\n  //     ['YYYY-MM-DD 00:00:00', 'YYYY-MM-DD 23:59:59'], // 时间格式\n  //   ],\n  // ],\n};\n\nconst gridOptions: VxeGridProps = {\n  checkboxConfig: {\n    // 高亮\n    highlight: true,\n    // 翻页时保留选中状态\n    reserve: true,\n    // 点击行选中\n    // trigger: 'row',\n  },\n  // 需要使用i18n注意这里要改成getter形式 否则切换语言不会刷新\n  // columns: columns(),\n  columns,\n  height: 'auto',\n  keepSource: true,\n  pagerConfig: {},\n  proxyConfig: {\n    ajax: {\n      query: async ({ page }, formValues = {}) => {\n        return await modelList({\n          pageNum: page.currentPage,\n          pageSize: page.pageSize,\n          ...formValues,\n        });\n      },\n    },\n  },\n  rowConfig: {\n    keyField: 'id',\n  },\n  // 表格全局唯一标识，用于保存列配置\n  id: 'system-model-index'\n};\n\nconst [BasicTable, tableApi] = useVbenVxeGrid({\n  formOptions,\n  gridOptions,\n});\n\nconst [ModelModal, modalApi] = useVbenModal({\n  connectedComponent: modelModal,\n});\n\nfunction handleAdd() {\n  modalApi.setData({});\n  modalApi.open();\n}\n\nasync function handleEdit(row: Required<ModelForm>) {\n  modalApi.setData({ id: row.id });\n  modalApi.open();\n}\n\nasync function handleDelete(row: Required<ModelForm>) {\n  await modelRemove(row.id);\n  await tableApi.query();\n}\n\nfunction handleMultiDelete() {\n  const rows = tableApi.grid.getCheckboxRecords();\n  const ids = rows.map((row: Required<ModelForm>) => row.id);\n  Modal.confirm({\n    title: '提示',\n    okType: 'danger',\n    content: `确认删除选中的${ids.length}条记录吗？`,\n    onOk: async () => {\n      await modelRemove(ids);\n      await tableApi.query();\n    },\n  });\n}\n\nfunction handleDownloadExcel() {\n  commonDownloadExcel(modelExport, '模型管理数据', tableApi.formApi.form.values, {\n    fieldMappingTime: formOptions.fieldMappingTime,\n  });\n}\n</script>\n\n<template>\n  <Page :auto-content-height=\"true\">\n    <BasicTable table-title=\"模型管理列表\">\n      <template #toolbar-tools>\n        <Space>\n          <a-button\n            v-access:code=\"['system:model:export']\"\n            @click=\"handleDownloadExcel\"\n          >\n            {{ $t('pages.common.export') }}\n          </a-button>\n          <a-button\n            :disabled=\"!vxeCheckboxChecked(tableApi)\"\n            danger\n            type=\"primary\"\n            v-access:code=\"['system:model:remove']\"\n            @click=\"handleMultiDelete\">\n            {{ $t('pages.common.delete') }}\n          </a-button>\n          <a-button\n            type=\"primary\"\n            v-access:code=\"['system:model:add']\"\n            @click=\"handleAdd\"\n          >\n            {{ $t('pages.common.add') }}\n          </a-button>\n        </Space>\n      </template>\n      <template #category=\"{ row }\">\n        {{\n          getDictOptions(DictEnum.CHAT_MODEL_CATEGORY).find(\n            (item) => item.value === row.category,\n          )?.label || row.category\n        }}\n      </template>\n      <template #action=\"{ row }\">\n        <Space>\n          <ghost-button\n            v-access:code=\"['system:model:edit']\"\n            @click.stop=\"handleEdit(row)\"\n          >\n            {{ $t('pages.common.edit') }}\n          </ghost-button>\n          <Popconfirm\n            :get-popup-container=\"getVxePopupContainer\"\n            placement=\"left\"\n            title=\"确认删除？\"\n            @confirm=\"handleDelete(row)\"\n          >\n            <ghost-button\n              danger\n              v-access:code=\"['system:model:remove']\"\n              @click.stop=\"\"\n            >\n              {{ $t('pages.common.delete') }}\n            </ghost-button>\n          </Popconfirm>\n        </Space>\n      </template>\n    </BasicTable>\n    <ModelModal @reload=\"tableApi.query()\" />\n  </Page>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/chat/model/model-modal.vue",
    "content": "<!--\n使用 Ant Design Vue 原生 Form 组件生成表单\n详细用法参考: https://antdv.com/components/form-cn\n注意: 如果 VSCode 配置了自动移除未使用的导入，可能会误删某些组件导入\n-->\n<script setup lang=\"ts\">\nimport type { RuleObject } from 'ant-design-vue/es/form';\n\nimport type { ModelForm } from '#/api/chat/model/model';\n\nimport { computed, onMounted, ref, watch } from 'vue';\n\nimport { useVbenModal } from '@vben/common-ui';\nimport { DictEnum } from '@vben/constants';\nimport { $t } from '@vben/locales';\nimport { cloneDeep } from '@vben/utils';\n\nimport {\n  Col,\n  Form,\n  FormItem,\n  Input,\n  Row,\n  Select,\n  Tag,\n  Textarea,\n} from 'ant-design-vue';\nimport { pick } from 'lodash-es';\n\nimport { modelAdd, modelInfo, modelUpdate } from '#/api/chat/model';\nimport { providerList } from '#/api/chat/provider';\nimport { getDictOptions } from '#/utils/dict';\n\nconst emit = defineEmits<{ reload: [] }>();\n\nconst isUpdate = ref(false);\nconst providerOptions = ref<Array<{ label: string; value: number | string }>>(\n  [],\n);\nconst providersMap = ref<Map<number | string, any>>(new Map());\nconst categoryOptions = computed(() => {\n  const options = getDictOptions(DictEnum.CHAT_MODEL_CATEGORY);\n  // 注入 rerank（如果字典没有）\n  if (!options.some((opt) => opt.value === 'rerank')) {\n    options.push({ label: '重排模型', value: 'rerank', cssClass: 'magenta' });\n  }\n\n  // 联动逻辑：仅支持重排的厂商显示重排选项\n  const supportedRerankProviders = ['alibailian', 'qianwen', 'siliconflow'];\n  const currentProvider = formData.value.providerCode?.toLowerCase();\n  if (currentProvider && !supportedRerankProviders.includes(currentProvider)) {\n    return options.filter((opt) => opt.value !== 'rerank');\n  }\n\n  return options;\n});\n\nconst title = computed(() => {\n  return isUpdate.value ? $t('pages.common.edit') : $t('pages.common.add');\n});\n\nonMounted(async () => {\n  loadProviders();\n});\n\nasync function loadProviders() {\n  try {\n    const res = await providerList({ pageNum: 1, pageSize: 999 });\n    providerOptions.value = res.rows.map((item) => ({\n      label: item.providerName,\n      value: item.providerCode,\n    }));\n    // 存储供应商完整信息，以便后续查询apiHost\n    providersMap.value.clear();\n    res.rows.forEach((item) => {\n      providersMap.value.set(item.providerCode, item);\n    });\n  } catch (error) {\n    console.error('Failed to load providers:', error);\n  }\n}\n\n/**\n * 定义默认值 用于reset\n */\nconst defaultValues: Partial<ModelForm> = {\n  id: undefined,\n  category: undefined,\n  modelName: undefined,\n  providerCode: undefined,\n  modelDescribe: undefined,\n  modelShow: undefined,\n  modelDimension: undefined,\n  apiHost: undefined,\n  apiKey: undefined,\n  remark: undefined,\n};\n\n/**\n * 表单数据ref\n */\nconst formData = ref(defaultValues);\n\n/**\n * 监听供应商变化\n */\nwatch(\n  () => formData.value.providerCode,\n  (newProviderCode) => {\n    if (newProviderCode === 'custom_api') {\n      formData.value.apiHost = undefined;\n      formRules.value.apiHost = [\n        { required: true, message: $t('ui.formRules.required') },\n      ];\n    } else {\n      delete formRules.value.apiHost;\n      if (newProviderCode && providersMap.value.has(newProviderCode)) {\n        const provider = providersMap.value.get(newProviderCode);\n        formData.value.apiHost = provider.apiHost;\n      }\n    }\n\n    // 自动校验重排分类：如果切换到的厂商不支持重排，且当前选中了重排，则强行清空分类\n    if (newProviderCode) {\n      const supportedRerankProviders = ['alibailian', 'qianwen', 'siliconflow'];\n      if (\n        !supportedRerankProviders.includes(newProviderCode.toLowerCase()) &&\n        formData.value.category === 'rerank'\n      ) {\n        formData.value.category = undefined;\n      }\n    }\n  },\n);\n\ntype AntdFormRules<T> = Partial<Record<keyof T, RuleObject[]>> & {\n  [key: string]: RuleObject[];\n};\n/**\n * 表单校验规则\n */\nconst formRules = ref<AntdFormRules<ModelForm>>({});\n\n/**\n * useForm解构出表单方法\n */\nconst { validate, validateInfos, resetFields } = Form.useForm(\n  formData,\n  formRules,\n);\n\nconst [BasicModal, modalApi] = useVbenModal({\n  class: 'w-[800px]',\n  fullscreenButton: false,\n  closeOnClickModal: false,\n  onClosed: handleCancel,\n  onConfirm: handleConfirm,\n  onOpenChange: async (isOpen) => {\n    if (!isOpen) {\n      return null;\n    }\n    modalApi.modalLoading(true);\n\n    const { id } = modalApi.getData() as { id?: number | string };\n    isUpdate.value = !!id;\n\n    if (isUpdate.value && id) {\n      const record = await modelInfo(id);\n      // 只赋值存在的字段\n      const filterRecord = pick(record, Object.keys(defaultValues));\n      formData.value = filterRecord;\n    }\n\n    modalApi.modalLoading(false);\n  },\n});\n\nasync function handleConfirm() {\n  try {\n    modalApi.modalLoading(true);\n    await validate();\n    // 可能会做数据处理 使用cloneDeep深拷贝\n    const data = cloneDeep(formData.value);\n    await (isUpdate.value ? modelUpdate(data) : modelAdd(data));\n    emit('reload');\n    await handleCancel();\n  } catch (error) {\n    console.error(error);\n  } finally {\n    modalApi.modalLoading(false);\n  }\n}\n\nasync function handleCancel() {\n  modalApi.close();\n  formData.value = cloneDeep(defaultValues);\n  resetFields();\n}\n\n/**\n * 获取分类颜色\n * @param category 分类值或颜色值\n * @param option 完整的选项对象，包含cssClass等信息\n */\nfunction getCategoryColor(category: string, option?: any): string {\n  // 优先使用option中的cssClass字段（可能包含自定义颜色）\n  if (option?.cssClass) {\n    const cssClass = option.cssClass.trim();\n    // 如果是十六进制颜色或其他有效的CSS颜色值\n    if (cssClass.startsWith('#') || isValidCSSColor(cssClass)) {\n      return cssClass;\n    }\n  }\n\n  // 其次查看category是否是有效的颜色值\n  if (category.startsWith('#') || isValidCSSColor(category)) {\n    return category;\n  }\n\n  // 最后使用预定义的颜色映射\n  const colorMap: Record<string, string> = {\n    chat: 'blue',\n    embedding: 'green',\n    image: 'orange',\n    audio: 'purple',\n    video: 'red',\n    code: 'cyan',\n    rerank: 'magenta',\n  };\n  return colorMap[category] || 'default';\n}\n\n/**\n * 判断是否为有效的CSS颜色值\n */\nfunction isValidCSSColor(color: string): boolean {\n  // 预定义的Ant Design颜色\n  const antColors = [\n    'red',\n    'orange',\n    'gold',\n    'yellow',\n    'lime',\n    'green',\n    'cyan',\n    'blue',\n    'geekblue',\n    'purple',\n    'magenta',\n    'volcano',\n    'default',\n  ];\n  if (antColors.includes(color)) {\n    return true;\n  }\n\n  // 简单检查是否为有效的十六进制颜色或rgb颜色\n  const hexRegex = /^#([A-F0-9]{6}|[A-F0-9]{3}|[A-F0-9]{8})$/i;\n  const rgbRegex = /^rgba?\\(/;\n\n  return hexRegex.test(color) || rgbRegex.test(color);\n}\n</script>\n\n<template>\n  <BasicModal :title=\"title\" class=\"w-[700px]\">\n    <Form :label-col=\"{ span: 24 }\" :wrapper-col=\"{ span: 24 }\">\n      <Row :gutter=\"16\">\n        <Col :span=\"12\">\n          <FormItem label=\"供应商\" v-bind=\"validateInfos.providerCode\">\n            <Select\n              v-model:value=\"formData.providerCode\"\n              :placeholder=\"$t('ui.formRules.required')\"\n              :options=\"providerOptions\"\n            />\n          </FormItem>\n        </Col>\n        <Col :span=\"12\">\n          <FormItem label=\"模型分类\" v-bind=\"validateInfos.category\">\n            <Select\n              v-model:value=\"formData.category\"\n              :placeholder=\"$t('ui.formRules.required')\"\n              :options=\"categoryOptions\"\n              show-search\n              option-filter-prop=\"label\"\n            >\n              <template #option=\"{ label, value, cssClass }\">\n                <div class=\"flex items-center justify-between\">\n                  <span>{{ label }}</span>\n                  <Tag\n                    :color=\"getCategoryColor(value, { cssClass })\"\n                    class=\"ml-2\"\n                  >\n                    {{ value }}\n                  </Tag>\n                </div>\n              </template>\n              <template #tagRender=\"{ label, option }\">\n                <Tag :color=\"getCategoryColor(option.value, option)\">\n                  {{ label }}\n                </Tag>\n              </template>\n            </Select>\n          </FormItem>\n        </Col>\n      </Row>\n\n      <Row :gutter=\"16\">\n        <Col :span=\"12\">\n          <FormItem label=\"模型名称\" v-bind=\"validateInfos.modelName\">\n            <Input\n              v-model:value=\"formData.modelName\"\n              :placeholder=\"$t('ui.formRules.required')\"\n            />\n          </FormItem>\n        </Col>\n        <Col :span=\"12\">\n          <FormItem label=\"模型描述\" v-bind=\"validateInfos.modelDescribe\">\n            <Input\n              v-model:value=\"formData.modelDescribe\"\n              :placeholder=\"$t('ui.formRules.required')\"\n            />\n          </FormItem>\n        </Col>\n      </Row>\n\n      <Row :gutter=\"16\">\n        <Col v-if=\"formData.category === 'vector'\" :span=\"12\">\n          <FormItem label=\"模型维度\" v-bind=\"validateInfos.modelDimension\">\n            <Input\n              v-model:value=\"formData.modelDimension\"\n              :placeholder=\"$t('ui.formRules.required')\"\n            />\n          </FormItem>\n        </Col>\n        <Col v-if=\"formData.providerCode === 'custom_api'\" :span=\"12\">\n          <FormItem label=\"请求地址\" v-bind=\"validateInfos.apiHost\">\n            <Input\n              v-model:value=\"formData.apiHost\"\n              :placeholder=\"$t('ui.formRules.required')\"\n            />\n          </FormItem>\n        </Col>\n      </Row>\n\n      <Row :gutter=\"16\">\n        <Col :span=\"24\">\n          <FormItem label=\"密钥\" v-bind=\"validateInfos.apiKey\">\n            <Input\n              v-model:value=\"formData.apiKey\"\n              :placeholder=\"$t('ui.formRules.required')\"\n            />\n          </FormItem>\n        </Col>\n      </Row>\n\n      <FormItem label=\"备注\" v-bind=\"validateInfos.remark\">\n        <Textarea\n          v-model:value=\"formData.remark\"\n          :placeholder=\"$t('ui.formRules.required')\"\n          :rows=\"4\"\n        />\n      </FormItem>\n    </Form>\n  </BasicModal>\n</template>\n\n<style scoped>\n:deep(.ant-form-item) {\n  padding: 0 8px;\n  margin-bottom: 20px;\n}\n\n:deep(.ant-form-item-label) {\n  padding-bottom: 8px;\n  text-align: left !important;\n}\n\n:deep(.ant-form-item-label > label) {\n  justify-content: flex-start !important;\n  font-weight: 500;\n  color: rgb(0 0 0 / 85%);\n  text-align: left !important;\n}\n\n:deep(.ant-form-item-control) {\n  text-align: left !important;\n}\n\n:deep(.ant-form-item-control-input) {\n  text-align: left !important;\n}\n\n:deep(.ant-input),\n:deep(.ant-select),\n:deep(.ant-picker),\n:deep(.ant-textarea) {\n  text-align: left !important;\n}\n\n:deep(.ant-input::placeholder),\n:deep(.ant-textarea::placeholder) {\n  color: rgb(0 0 0 / 45%);\n}\n</style>\n"
  },
  {
    "path": "apps/web-antd/src/views/chat/provider/data.ts",
    "content": "import type { FormSchemaGetter } from '#/adapter/form';\nimport type { VxeGridProps } from '#/adapter/vxe-table';\n\nexport const querySchema: FormSchemaGetter = () => [\n  {\n    component: 'Input',\n    fieldName: 'providerName',\n    label: '厂商名称',\n  },\n  {\n    component: 'Input',\n    fieldName: 'providerCode',\n    label: '厂商编码',\n  },\n];\n\n/**\n * 表格列配置\n * 如果需要使用 i18n 国际化，请使用 getter 函数形式，否则切换语言时列配置不会刷新\n * 使用方式: export const columns: () => VxeGridProps['columns'] = () => [...]\n */\nexport const columns: VxeGridProps['columns'] = [\n  { type: 'checkbox', width: 60 },\n  {\n    title: '厂商图标',\n    field: 'providerIcon',\n    slots: { default: 'providerIcon' },\n  },\n  {\n    title: '厂商名称',\n    field: 'providerName',\n  },\n  {\n    title: '厂商编码',\n    field: 'providerCode',\n  },\n\n  {\n    title: '厂商描述',\n    field: 'providerDesc',\n  },\n  {\n    title: 'API地址',\n    field: 'apiHost',\n  },\n  {\n    title: '备注',\n    field: 'remark',\n  },\n\n  {\n    field: 'action',\n    fixed: 'right',\n    slots: { default: 'action' },\n    title: '操作',\n    width: 180,\n  },\n];\n"
  },
  {
    "path": "apps/web-antd/src/views/chat/provider/index.vue",
    "content": "<script setup lang=\"ts\">\nimport type { Recordable } from '@vben/types';\n\nimport { onMounted, ref } from 'vue';\n\nimport { Page, useVbenModal, type VbenFormProps } from '@vben/common-ui';\nimport { getVxePopupContainer } from '@vben/utils';\n\nimport { Image, Modal, Popconfirm, Space, Spin, Switch, Tooltip } from 'ant-design-vue';\n\nimport {\n  useVbenVxeGrid,\n  vxeCheckboxChecked,\n  type VxeGridProps\n} from '#/adapter/vxe-table';\n\nimport {\n  providerExport,\n  providerList,\n  providerRemove,\n} from '#/api/chat/provider';\nimport type { ProviderForm } from '#/api/chat/provider/model';\nimport { commonDownloadExcel } from '#/utils/file/download';\n\nimport providerModal from './provider-modal.vue';\nimport { columns, querySchema } from './data';\n\n// 图片预览相关配置\nconst preview = ref(true);  // 默认开启预览\nconst supportImageList = ['jpg', 'jpeg', 'png', 'gif', 'webp'];\n\n/**\n * 根据扩展名判断是否是图片\n * @param url 文件url或路径\n */\nfunction isImageFile(url: string) {\n  if (!url) return false;\n  return supportImageList.some((item) =>\n    url.toLocaleLowerCase().includes(item),\n  );\n}\n\nconst formOptions: VbenFormProps = {\n  commonConfig: {\n    labelWidth: 80,\n    componentProps: {\n      allowClear: true,\n    },\n  },\n  schema: querySchema(),\n  wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4',\n  // 处理区间选择器 RangePicker 时间格式映射\n  // 将一个时间区间字段映射为两个独立的开始/结束时间字段，用于搜索和导出\n  // 示例: 将 createTime 字段映射为 params[beginTime] 和 params[endTime]\n  // fieldMappingTime: [\n  //   [\n  //     'createTime', // 表单中的字段名\n  //     ['params[beginTime]', 'params[endTime]'], // 映射后的字段名\n  //     ['YYYY-MM-DD 00:00:00', 'YYYY-MM-DD 23:59:59'], // 时间格式\n  //   ],\n  // ],\n};\n\nconst gridOptions: VxeGridProps = {\n  checkboxConfig: {\n    // 高亮\n    highlight: true,\n    // 翻页时保留选中状态\n    reserve: true,\n    // 点击行选中\n    // trigger: 'row',\n  },\n  // 需要使用i18n注意这里要改成getter形式 否则切换语言不会刷新\n  // columns: columns(),\n  columns,\n  height: 'auto',\n  keepSource: true,\n  pagerConfig: {},\n  proxyConfig: {\n    ajax: {\n      query: async ({ page }, formValues = {}) => {\n        return await providerList({\n          pageNum: page.currentPage,\n          pageSize: page.pageSize,\n          ...formValues,\n        });\n      },\n    },\n  },\n  rowConfig: {\n    keyField: 'id',\n  },\n  // 表格全局唯一标识，用于保存列配置\n  id: 'system-provider-index'\n};\n\nconst [BasicTable, tableApi] = useVbenVxeGrid({\n  formOptions,\n  gridOptions,\n});\n\nconst [ProviderModal, modalApi] = useVbenModal({\n  connectedComponent: providerModal,\n});\n\nfunction handleAdd() {\n  modalApi.setData({});\n  modalApi.open();\n}\n\nasync function handleEdit(row: Required<ProviderForm>) {\n  modalApi.setData({ id: row.id });\n  modalApi.open();\n}\n\nasync function handleDelete(row: Required<ProviderForm>) {\n  await providerRemove(row.id);\n  await tableApi.query();\n}\n\nfunction handleMultiDelete() {\n  const rows = tableApi.grid.getCheckboxRecords();\n  const ids = rows.map((row: Required<ProviderForm>) => row.id);\n  Modal.confirm({\n    title: '提示',\n    okType: 'danger',\n    content: `确认删除选中的${ids.length}条记录吗？`,\n    onOk: async () => {\n      await providerRemove(ids);\n      await tableApi.query();\n    },\n  });\n}\n\nfunction handleDownloadExcel() {\n  commonDownloadExcel(providerExport, '厂商管理数据', tableApi.formApi.form.values, {\n    fieldMappingTime: formOptions.fieldMappingTime,\n  });\n}\n</script>\n\n<template>\n  <Page :auto-content-height=\"true\">\n    <BasicTable table-title=\"厂商管理列表\">\n      <template #toolbar-tools>\n        <Space>\n          <Tooltip title=\"预览图片\">\n            <Switch v-model:checked=\"preview\" />\n          </Tooltip>\n          <a-button\n            v-access:code=\"['system:provider:export']\"\n            @click=\"handleDownloadExcel\"\n          >\n            {{ $t('pages.common.export') }}\n          </a-button>\n          <a-button\n            :disabled=\"!vxeCheckboxChecked(tableApi)\"\n            danger\n            type=\"primary\"\n            v-access:code=\"['system:provider:remove']\"\n            @click=\"handleMultiDelete\">\n            {{ $t('pages.common.delete') }}\n          </a-button>\n          <a-button\n            type=\"primary\"\n            v-access:code=\"['system:provider:add']\"\n            @click=\"handleAdd\"\n          >\n            {{ $t('pages.common.add') }}\n          </a-button>\n        </Space>\n      </template>\n      <template #providerIcon=\"{ row }\">\n        <Image\n          v-if=\"preview && isImageFile(row.providerIcon)\"\n          :key=\"row.id\"\n          :src=\"row.providerIcon\"\n          height=\"35px\"\n          width=\"35px\"\n          style=\"object-fit: cover\"\n          preview\n        >\n          <template #placeholder>\n            <div class=\"flex size-full items-center justify-center\">\n              <Spin />\n            </div>\n          </template>\n        </Image>\n        <span v-else-if=\"isImageFile(row.providerIcon)\" class=\"text-blue-500 cursor-pointer\">\n          {{ row.providerIcon.split('/').pop() }}\n        </span>\n        <span v-else>{{ row.providerIcon }}</span>\n      </template>\n      <template #action=\"{ row }\">\n        <Space>\n          <ghost-button\n            v-access:code=\"['system:provider:edit']\"\n            @click.stop=\"handleEdit(row)\"\n          >\n            {{ $t('pages.common.edit') }}\n          </ghost-button>\n          <Popconfirm\n            :get-popup-container=\"getVxePopupContainer\"\n            placement=\"left\"\n            title=\"确认删除？\"\n            @confirm=\"handleDelete(row)\"\n          >\n            <ghost-button\n              danger\n              v-access:code=\"['system:provider:remove']\"\n              @click.stop=\"\"\n            >\n              {{ $t('pages.common.delete') }}\n            </ghost-button>\n          </Popconfirm>\n        </Space>\n      </template>\n    </BasicTable>\n    <ProviderModal @reload=\"tableApi.query()\" />\n  </Page>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/chat/provider/options.ts",
    "content": "export const providerOptions = [\n  { label: 'OpenAI', value: 'openai' },\n  { label: '深度求索', value: 'deepseek' },\n  { label: '智谱AI', value: 'zhipu' },\n  { label: '小米MiMo', value: 'xiaomi' },\n  { label: '阿里云百炼', value: 'qianwen' },\n  { label: 'PPIO', value: 'ppio' },\n  { label: 'MiniMax', value: 'minimax' },\n  { label: 'Ollama', value: 'ollama' },\n  { label: '自定义厂商', value: 'custom_api' },\n] as const;\n"
  },
  {
    "path": "apps/web-antd/src/views/chat/provider/provider-modal.vue",
    "content": "<!--\n使用 Ant Design Vue 原生 Form 组件生成表单\n详细用法参考: https://antdv.com/components/form-cn\n注意: 如果 VSCode 配置了自动移除未使用的导入，可能会误删某些组件导入\n-->\n<script setup lang=\"ts\">\nimport type { RuleObject } from 'ant-design-vue/es/form';\nimport { computed, ref } from 'vue';\n\nimport { Input, Textarea, Select, Form, FormItem } from 'ant-design-vue';\nimport { ImageUpload } from '#/components/upload';\nimport { pick } from 'lodash-es';\n\nimport { useVbenModal } from '@vben/common-ui';\nimport { $t } from '@vben/locales';\nimport { cloneDeep } from '@vben/utils';\n\nimport { providerAdd, providerInfo, providerUpdate } from '#/api/chat/provider';\nimport { ossInfo } from '#/api/system/oss';\nimport type { ProviderForm } from '#/api/chat/provider/model';\n\nimport { providerOptions } from './options';\n\nconst emit = defineEmits<{ reload: [] }>();\n\nconst isUpdate = ref(false);\nconst title = computed(() => {\n  return isUpdate.value ? $t('pages.common.edit') : $t('pages.common.add');\n});\n\n/**\n * 定义默认值 用于reset\n */\nconst defaultValues: Partial<ProviderForm> = {\n  id: undefined,\n  providerName: undefined,\n  providerCode: undefined,\n  providerIcon: undefined,\n  providerDesc: undefined,\n  apiHost: undefined,\n  status: undefined,\n  sortOrder: undefined,\n  remark: undefined,\n  updateIp: undefined,\n};\n\n/**\n * 表单数据ref\n */\nconst formData = ref(defaultValues);\n\ntype AntdFormRules<T> = Partial<Record<keyof T, RuleObject[]>> & {\n  [key: string]: RuleObject[];\n};\n/**\n * 表单校验规则\n */\nconst formRules = ref<AntdFormRules<ProviderForm>>({\n  providerName: [{ required: true, message: '厂商名称不能为空' }],\n  providerCode: [{ required: true, message: '厂商编码不能为空' }],\n});\n\n/**\n * useForm解构出表单方法\n */\nconst { validate, validateInfos, resetFields } = Form.useForm(\n  formData,\n  formRules,\n);\n\nconst [BasicModal, modalApi] = useVbenModal({\n  class: 'w-[550px]',\n  fullscreenButton: false,\n  closeOnClickModal: false,\n  onClosed: handleCancel,\n  onConfirm: handleConfirm,\n  onOpenChange: async (isOpen) => {\n    if (!isOpen) {\n      return null;\n    }\n    modalApi.modalLoading(true);\n\n    const { id } = modalApi.getData() as { id?: number | string };\n    isUpdate.value = !!id;\n\n    if (isUpdate.value && id) {\n      const record = await providerInfo(id);\n      // 只赋值存在的字段\n      const filterRecord = pick(record, Object.keys(defaultValues));\n\n      // 如果providerIcon是URL格式，直接保留\n      if (\n        filterRecord.providerIcon\n        && typeof filterRecord.providerIcon === 'string'\n        && filterRecord.providerIcon.startsWith('http')\n      ) {\n        filterRecord.providerIcon = filterRecord.providerIcon;\n      }\n\n      formData.value = filterRecord;\n    }\n\n    modalApi.modalLoading(false);\n  },\n});\n\nasync function handleConfirm() {\n  try {\n    modalApi.modalLoading(true);\n    await validate();\n    // 可能会做数据处理 使用cloneDeep深拷贝\n    const data = cloneDeep(formData.value);\n\n    // 如果providerIcon是ossId，需要转换为URL\n    if (data.providerIcon && typeof data.providerIcon === 'string') {\n      // 检查是否是ossId（不包含http/https的字符串被认为是ossId）\n      if (!data.providerIcon.startsWith('http')) {\n        try {\n          const ossFileList = await ossInfo(data.providerIcon);\n          if (ossFileList && ossFileList.length > 0) {\n            data.providerIcon = ossFileList[0].url;\n          }\n        } catch {\n          // 失败时保持原值\n        }\n      }\n    }\n\n    await (isUpdate.value ? providerUpdate(data) : providerAdd(data));\n    emit('reload');\n    await handleCancel();\n  } catch (error) {\n    console.error(error);\n  } finally {\n    modalApi.modalLoading(false);\n  }\n}\n\nasync function handleCancel() {\n  modalApi.close();\n  formData.value = cloneDeep(defaultValues);\n  resetFields();\n}\n</script>\n\n<template>\n  <BasicModal :title=\"title\">\n    <Form :label-col=\"{ span: 4 }\">\n      <FormItem label=\"厂商名称\" v-bind=\"validateInfos.providerName\">\n        <Input v-model:value=\"formData.providerName\" :placeholder=\"$t('ui.formRules.required')\" />\n      </FormItem>\n      <FormItem label=\"厂商编码\" v-bind=\"validateInfos.providerCode\">\n        <Select\n          v-model:value=\"formData.providerCode\"\n          :options=\"providerOptions\"\n          :placeholder=\"$t('ui.formRules.required')\"\n          allow-clear\n          show-search\n        />\n      </FormItem>\n      <FormItem label=\"厂商图标\" v-bind=\"validateInfos.providerIcon\">\n        <ImageUpload\n          v-model:value=\"formData.providerIcon\"\n          :max-count=\"1\"\n          help-message\n          keep-missing-id\n        />\n      </FormItem>\n      <FormItem label=\"厂商描述\" v-bind=\"validateInfos.providerDesc\">\n        <Textarea\n          v-model:value=\"formData.providerDesc\"\n          :placeholder=\"$t('ui.formRules.required')\"\n          :rows=\"4\"\n        />\n      </FormItem>\n      <FormItem label=\"API地址\" v-bind=\"validateInfos.apiHost\">\n        <Input v-model:value=\"formData.apiHost\" :placeholder=\"$t('ui.formRules.required')\" />\n      </FormItem>\n      <FormItem label=\"排序\" v-bind=\"validateInfos.sortOrder\">\n        <Input v-model:value=\"formData.sortOrder\" :placeholder=\"$t('ui.formRules.required')\" />\n      </FormItem>\n      <FormItem label=\"备注\" v-bind=\"validateInfos.remark\">\n        <Textarea\n          v-model:value=\"formData.remark\"\n          :placeholder=\"$t('ui.formRules.required')\"\n          :rows=\"4\"\n        />\n      </FormItem>\n    </Form>\n  </BasicModal>\n</template>\n\n"
  },
  {
    "path": "apps/web-antd/src/views/dashboard/analytics/analytics-trends.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { EchartsUIType } from '@vben/plugins/echarts';\n\nimport { EchartsUI, useEcharts } from '@vben/plugins/echarts';\nimport { onMounted, ref } from 'vue';\n\nconst chartRef = ref<EchartsUIType>();\nconst { renderEcharts } = useEcharts(chartRef);\n\nonMounted(() => {\n  renderEcharts({\n    grid: {\n      bottom: 0,\n      containLabel: true,\n      left: '1%',\n      right: '1%',\n      top: '2 %',\n    },\n    series: [\n      {\n        areaStyle: {},\n        data: [\n          111, 2000, 6000, 16_000, 33_333, 55_555, 64_000, 33_333, 18_000,\n          36_000, 70_000, 42_444, 23_222, 13_000, 8000, 4000, 1200, 333, 222,\n          111,\n        ],\n        itemStyle: {\n          color: '#5ab1ef',\n        },\n        smooth: true,\n        type: 'line',\n      },\n      {\n        areaStyle: {},\n        data: [\n          33, 66, 88, 333, 3333, 6200, 20_000, 3000, 1200, 13_000, 22_000,\n          11_000, 2221, 1201, 390, 198, 60, 30, 22, 11,\n        ],\n        itemStyle: {\n          color: '#019680',\n        },\n        smooth: true,\n        type: 'line',\n      },\n    ],\n    tooltip: {\n      axisPointer: {\n        lineStyle: {\n          color: '#019680',\n          width: 1,\n        },\n      },\n      trigger: 'axis',\n    },\n    // xAxis: {\n    //   axisTick: {\n    //     show: false,\n    //   },\n    //   boundaryGap: false,\n    //   data: Array.from({ length: 18 }).map((_item, index) => `${index + 6}:00`),\n    //   type: 'category',\n    // },\n    xAxis: {\n      axisTick: {\n        show: false,\n      },\n      boundaryGap: false,\n      data: Array.from({ length: 18 }).map((_item, index) => `${index + 6}:00`),\n      splitLine: {\n        lineStyle: {\n          type: 'solid',\n          width: 1,\n        },\n        show: true,\n      },\n      type: 'category',\n    },\n    yAxis: [\n      {\n        axisTick: {\n          show: false,\n        },\n        max: 80_000,\n        splitArea: {\n          show: true,\n        },\n        splitNumber: 4,\n        type: 'value',\n      },\n    ],\n  });\n});\n</script>\n\n<template>\n  <EchartsUI ref=\"chartRef\" />\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/dashboard/analytics/analytics-visits-data.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { EchartsUIType } from '@vben/plugins/echarts';\n\nimport { EchartsUI, useEcharts } from '@vben/plugins/echarts';\nimport { onMounted, ref } from 'vue';\n\nconst chartRef = ref<EchartsUIType>();\nconst { renderEcharts } = useEcharts(chartRef);\n\nonMounted(() => {\n  renderEcharts({\n    legend: {\n      bottom: 0,\n      data: ['访问', '趋势'],\n    },\n    radar: {\n      indicator: [\n        {\n          name: '网页',\n        },\n        {\n          name: '移动端',\n        },\n        {\n          name: 'Ipad',\n        },\n        {\n          name: '客户端',\n        },\n        {\n          name: '第三方',\n        },\n        {\n          name: '其它',\n        },\n      ],\n      radius: '60%',\n      splitNumber: 8,\n    },\n    series: [\n      {\n        areaStyle: {\n          opacity: 1,\n          shadowBlur: 0,\n          shadowColor: 'rgba(0,0,0,.2)',\n          shadowOffsetX: 0,\n          shadowOffsetY: 10,\n        },\n        data: [\n          {\n            itemStyle: {\n              color: '#b6a2de',\n            },\n            name: '访问',\n            value: [90, 50, 86, 40, 50, 20],\n          },\n          {\n            itemStyle: {\n              color: '#5ab1ef',\n            },\n            name: '趋势',\n            value: [70, 75, 70, 76, 20, 85],\n          },\n        ],\n        itemStyle: {\n          // borderColor: '#fff',\n          borderRadius: 10,\n          borderWidth: 2,\n        },\n        symbolSize: 0,\n        type: 'radar',\n      },\n    ],\n    tooltip: {},\n  });\n});\n</script>\n\n<template>\n  <EchartsUI ref=\"chartRef\" />\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/dashboard/analytics/analytics-visits-sales.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { EchartsUIType } from '@vben/plugins/echarts';\n\nimport { EchartsUI, useEcharts } from '@vben/plugins/echarts';\nimport { onMounted, ref } from 'vue';\n\nconst chartRef = ref<EchartsUIType>();\nconst { renderEcharts } = useEcharts(chartRef);\n\nonMounted(() => {\n  renderEcharts({\n    series: [\n      {\n        animationDelay() {\n          return Math.random() * 400;\n        },\n        animationEasing: 'exponentialInOut',\n        animationType: 'scale',\n        center: ['50%', '50%'],\n        color: ['#5ab1ef', '#b6a2de', '#67e0e3', '#2ec7c9'],\n        data: [\n          { name: '外包', value: 500 },\n          { name: '定制', value: 310 },\n          { name: '技术支持', value: 274 },\n          { name: '远程', value: 400 },\n        ].sort((a, b) => {\n          return a.value - b.value;\n        }),\n        name: '商业占比',\n        radius: '80%',\n        roseType: 'radius',\n        type: 'pie',\n      },\n    ],\n\n    tooltip: {\n      trigger: 'item',\n    },\n  });\n});\n</script>\n\n<template>\n  <EchartsUI ref=\"chartRef\" />\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/dashboard/analytics/analytics-visits-source.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { EchartsUIType } from '@vben/plugins/echarts';\n\nimport { EchartsUI, useEcharts } from '@vben/plugins/echarts';\nimport { onMounted, ref } from 'vue';\n\nconst chartRef = ref<EchartsUIType>();\nconst { renderEcharts } = useEcharts(chartRef);\n\nonMounted(() => {\n  renderEcharts({\n    legend: {\n      bottom: '2%',\n      left: 'center',\n    },\n    series: [\n      {\n        animationDelay() {\n          return Math.random() * 100;\n        },\n        animationEasing: 'exponentialInOut',\n        animationType: 'scale',\n        avoidLabelOverlap: false,\n        color: ['#5ab1ef', '#b6a2de', '#67e0e3', '#2ec7c9'],\n        data: [\n          { name: '搜索引擎', value: 1048 },\n          { name: '直接访问', value: 735 },\n          { name: '邮件营销', value: 580 },\n          { name: '联盟广告', value: 484 },\n        ],\n        emphasis: {\n          label: {\n            fontSize: '12',\n            fontWeight: 'bold',\n            show: true,\n          },\n        },\n        itemStyle: {\n          // borderColor: '#fff',\n          borderRadius: 10,\n          borderWidth: 2,\n        },\n        label: {\n          position: 'center',\n          show: false,\n        },\n        labelLine: {\n          show: false,\n        },\n        name: '访问来源',\n        radius: ['40%', '65%'],\n        type: 'pie',\n      },\n    ],\n    tooltip: {\n      trigger: 'item',\n    },\n  });\n});\n</script>\n\n<template>\n  <EchartsUI ref=\"chartRef\" />\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/dashboard/analytics/analytics-visits.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { EchartsUIType } from '@vben/plugins/echarts';\n\nimport { EchartsUI, useEcharts } from '@vben/plugins/echarts';\nimport { onMounted, ref } from 'vue';\n\nconst chartRef = ref<EchartsUIType>();\nconst { renderEcharts } = useEcharts(chartRef);\n\nonMounted(() => {\n  renderEcharts({\n    grid: {\n      bottom: 0,\n      containLabel: true,\n      left: '1%',\n      right: '1%',\n      top: '2 %',\n    },\n    series: [\n      {\n        barMaxWidth: 80,\n        // color: '#4f69fd',\n        data: [\n          3000, 2000, 3333, 5000, 3200, 4200, 3200, 2100, 3000, 5100, 6000,\n          3200, 4800,\n        ],\n        type: 'bar',\n      },\n    ],\n    tooltip: {\n      axisPointer: {\n        lineStyle: {\n          // color: '#4f69fd',\n          width: 1,\n        },\n      },\n      trigger: 'axis',\n    },\n    xAxis: {\n      data: Array.from({ length: 12 }).map((_item, index) => `${index + 1}月`),\n      type: 'category',\n    },\n    yAxis: {\n      max: 8000,\n      splitNumber: 4,\n      type: 'value',\n    },\n  });\n});\n</script>\n\n<template>\n  <EchartsUI ref=\"chartRef\" />\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/dashboard/analytics/index.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { AnalysisOverviewItem } from '@vben/common-ui';\nimport type { TabOption } from '@vben/types';\n\nimport {\n  AnalysisChartCard,\n  AnalysisChartsTabs,\n  AnalysisOverview,\n} from '@vben/common-ui';\nimport {\n  SvgBellIcon,\n  SvgCakeIcon,\n  SvgCardIcon,\n  SvgDownloadIcon,\n} from '@vben/icons';\n\nimport AnalyticsTrends from './analytics-trends.vue';\nimport AnalyticsVisitsData from './analytics-visits-data.vue';\nimport AnalyticsVisitsSales from './analytics-visits-sales.vue';\nimport AnalyticsVisitsSource from './analytics-visits-source.vue';\nimport AnalyticsVisits from './analytics-visits.vue';\n\nconst overviewItems: AnalysisOverviewItem[] = [\n  {\n    icon: SvgCardIcon,\n    title: '用户量',\n    totalTitle: '总用户量',\n    totalValue: 120_000,\n    value: 2000,\n  },\n  {\n    icon: SvgCakeIcon,\n    title: '访问量',\n    totalTitle: '总访问量',\n    totalValue: 500_000,\n    value: 20_000,\n  },\n  {\n    icon: SvgDownloadIcon,\n    title: '下载量',\n    totalTitle: '总下载量',\n    totalValue: 120_000,\n    value: 8000,\n  },\n  {\n    icon: SvgBellIcon,\n    title: '使用量',\n    totalTitle: '总使用量',\n    totalValue: 50_000,\n    value: 5000,\n  },\n];\n\nconst chartTabs: TabOption[] = [\n  {\n    label: '流量趋势',\n    value: 'trends',\n  },\n  {\n    label: '月访问量',\n    value: 'visits',\n  },\n];\n</script>\n\n<template>\n  <div class=\"p-5\">\n    <AnalysisOverview :items=\"overviewItems\" />\n    <AnalysisChartsTabs :tabs=\"chartTabs\" class=\"mt-5\">\n      <template #trends>\n        <AnalyticsTrends />\n      </template>\n      <template #visits>\n        <AnalyticsVisits />\n      </template>\n    </AnalysisChartsTabs>\n\n    <div class=\"mt-5 w-full md:flex\">\n      <AnalysisChartCard class=\"mt-5 md:mr-4 md:mt-0 md:w-1/3\" title=\"访问数量\">\n        <AnalyticsVisitsData />\n      </AnalysisChartCard>\n      <AnalysisChartCard class=\"mt-5 md:mr-4 md:mt-0 md:w-1/3\" title=\"访问来源\">\n        <AnalyticsVisitsSource />\n      </AnalysisChartCard>\n      <AnalysisChartCard class=\"mt-5 md:mt-0 md:w-1/3\" title=\"访问来源\">\n        <AnalyticsVisitsSales />\n      </AnalysisChartCard>\n    </div>\n  </div>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/dashboard/workspace/index.vue",
    "content": "<script lang=\"ts\" setup>\nimport type {\n  WorkbenchProjectItem,\n  WorkbenchQuickNavItem,\n  WorkbenchTodoItem,\n  WorkbenchTrendItem,\n} from '@vben/common-ui';\n\nimport { ref } from 'vue';\nimport { useRouter } from 'vue-router';\n\nimport {\n  AnalysisChartCard,\n  WorkbenchHeader,\n  WorkbenchProject,\n  WorkbenchQuickNav,\n  WorkbenchTodo,\n  WorkbenchTrends,\n} from '@vben/common-ui';\nimport { preferences } from '@vben/preferences';\nimport { useUserStore } from '@vben/stores';\nimport { openWindow } from '@vben/utils';\n\nimport AnalyticsVisitsSource from '../analytics/analytics-visits-source.vue';\n\nconst userStore = useUserStore();\n\n// 这是一个示例数据，实际项目中需要根据实际情况进行调整\n// url 也可以是内部路由，在 navTo 方法中识别处理，进行内部跳转\n// 例如：url: /dashboard/workspace\nconst projectItems: WorkbenchProjectItem[] = [\n  {\n    color: '',\n    content: '不要等待机会，而要创造机会。',\n    date: '2021-04-01',\n    group: '开源组',\n    icon: 'carbon:logo-github',\n    title: 'Github',\n    url: 'https://github.com',\n  },\n  {\n    color: '#3fb27f',\n    content: '现在的你决定将来的你。',\n    date: '2021-04-01',\n    group: '算法组',\n    icon: 'ion:logo-vue',\n    title: 'Vue',\n    url: 'https://vuejs.org',\n  },\n  {\n    color: '#e18525',\n    content: '没有什么才能比努力更重要。',\n    date: '2021-04-01',\n    group: '上班摸鱼',\n    icon: 'ion:logo-html5',\n    title: 'Html5',\n    url: 'https://developer.mozilla.org/zh-CN/docs/Web/HTML',\n  },\n  {\n    color: '#bf0c2c',\n    content: '热情和欲望可以突破一切难关。',\n    date: '2021-04-01',\n    group: 'UI',\n    icon: 'ion:logo-angular',\n    title: 'Angular',\n    url: 'https://angular.io',\n  },\n  {\n    color: '#00d8ff',\n    content: '健康的身体是实现目标的基石。',\n    date: '2021-04-01',\n    group: '技术牛',\n    icon: 'bx:bxl-react',\n    title: 'React',\n    url: 'https://reactjs.org',\n  },\n  {\n    color: '#EBD94E',\n    content: '路是走出来的，而不是空想出来的。',\n    date: '2021-04-01',\n    group: '架构组',\n    icon: 'ion:logo-javascript',\n    title: 'Js',\n    url: 'https://developer.mozilla.org/zh-CN/docs/Web/JavaScript',\n  },\n];\n\n// 同样，这里的 url 也可以使用以 http 开头的外部链接\nconst quickNavItems: WorkbenchQuickNavItem[] = [\n  {\n    color: '#1fdaca',\n    icon: 'ion:home-outline',\n    title: '首页',\n    url: '/',\n  },\n  {\n    color: '#bf0c2c',\n    icon: 'ion:grid-outline',\n    title: '仪表盘',\n    url: '/dashboard',\n  },\n  {\n    color: '#e18525',\n    icon: 'ion:layers-outline',\n    title: '组件',\n    url: '/demos/features/icons',\n  },\n  {\n    color: '#3fb27f',\n    icon: 'ion:settings-outline',\n    title: '系统管理',\n    url: '/demos/features/login-expired', // 这里的 URL 是示例，实际项目中需要根据实际情况进行调整\n  },\n  {\n    color: '#4daf1bc9',\n    icon: 'ion:key-outline',\n    title: '权限管理',\n    url: '/demos/access/page-control',\n  },\n  {\n    color: '#00d8ff',\n    icon: 'ion:bar-chart-outline',\n    title: '图表',\n    url: '/analytics',\n  },\n];\n\nconst todoItems = ref<WorkbenchTodoItem[]>([\n  {\n    completed: false,\n    content: `审查最近提交到Git仓库的前端代码，确保代码质量和规范。`,\n    date: '2024-07-30 11:00:00',\n    title: '审查前端代码提交',\n  },\n  {\n    completed: true,\n    content: `检查并优化系统性能，降低CPU使用率。`,\n    date: '2024-07-30 11:00:00',\n    title: '系统性能优化',\n  },\n  {\n    completed: false,\n    content: `进行系统安全检查，确保没有安全漏洞或未授权的访问。 `,\n    date: '2024-07-30 11:00:00',\n    title: '安全检查',\n  },\n  {\n    completed: false,\n    content: `更新项目中的所有npm依赖包，确保使用最新版本。`,\n    date: '2024-07-30 11:00:00',\n    title: '更新项目依赖',\n  },\n  {\n    completed: false,\n    content: `修复用户报告的页面UI显示问题，确保在不同浏览器中显示一致。 `,\n    date: '2024-07-30 11:00:00',\n    title: '修复UI显示问题',\n  },\n]);\nconst trendItems: WorkbenchTrendItem[] = [\n  {\n    avatar: 'svg:avatar-1',\n    content: `在 <a>开源组</a> 创建了项目 <a>Vue</a>`,\n    date: '刚刚',\n    title: '威廉',\n  },\n  {\n    avatar: 'svg:avatar-2',\n    content: `关注了 <a>威廉</a> `,\n    date: '1个小时前',\n    title: '艾文',\n  },\n  {\n    avatar: 'svg:avatar-3',\n    content: `发布了 <a>个人动态</a> `,\n    date: '1天前',\n    title: '克里斯',\n  },\n  {\n    avatar: 'svg:avatar-4',\n    content: `发表文章 <a>如何编写一个Vite插件</a> `,\n    date: '2天前',\n    title: 'Vben',\n  },\n  {\n    avatar: 'svg:avatar-1',\n    content: `回复了 <a>杰克</a> 的问题 <a>如何进行项目优化？</a>`,\n    date: '3天前',\n    title: '皮特',\n  },\n  {\n    avatar: 'svg:avatar-2',\n    content: `关闭了问题 <a>如何运行项目</a> `,\n    date: '1周前',\n    title: '杰克',\n  },\n  {\n    avatar: 'svg:avatar-3',\n    content: `发布了 <a>个人动态</a> `,\n    date: '1周前',\n    title: '威廉',\n  },\n  {\n    avatar: 'svg:avatar-4',\n    content: `推送了代码到 <a>Github</a>`,\n    date: '2021-04-01 20:00',\n    title: '威廉',\n  },\n  {\n    avatar: 'svg:avatar-4',\n    content: `发表文章 <a>如何编写使用 Admin Vben</a> `,\n    date: '2021-03-01 20:00',\n    title: 'Vben',\n  },\n];\n\nconst router = useRouter();\n\n// 这是一个示例方法，实际项目中需要根据实际情况进行调整\n// This is a sample method, adjust according to the actual project requirements\nfunction navTo(nav: WorkbenchProjectItem | WorkbenchQuickNavItem) {\n  if (nav.url?.startsWith('http')) {\n    openWindow(nav.url);\n    return;\n  }\n  if (nav.url?.startsWith('/')) {\n    router.push(nav.url).catch((error) => {\n      console.error('Navigation failed:', error);\n    });\n  } else {\n    console.warn(`Unknown URL for navigation item: ${nav.title} -> ${nav.url}`);\n  }\n}\n</script>\n\n<template>\n  <div class=\"p-5\">\n    <WorkbenchHeader\n      :avatar=\"userStore.userInfo?.avatar || preferences.app.defaultAvatar\"\n    >\n      <template #title>\n        早安, {{ userStore.userInfo?.realName }}, 开始您一天的工作吧！\n      </template>\n      <template #description> 今日晴，20℃ - 32℃！ </template>\n    </WorkbenchHeader>\n\n    <div class=\"mt-5 flex flex-col lg:flex-row\">\n      <div class=\"mr-4 w-full lg:w-3/5\">\n        <WorkbenchProject :items=\"projectItems\" title=\"项目\" @click=\"navTo\" />\n        <WorkbenchTrends :items=\"trendItems\" class=\"mt-5\" title=\"最新动态\" />\n      </div>\n      <div class=\"w-full lg:w-2/5\">\n        <WorkbenchQuickNav\n          :items=\"quickNavItems\"\n          class=\"mt-5 lg:mt-0\"\n          title=\"快捷导航\"\n          @click=\"navTo\"\n        />\n        <WorkbenchTodo :items=\"todoItems\" class=\"mt-5\" title=\"待办事项\" />\n        <AnalysisChartCard class=\"mt-5\" title=\"访问来源\">\n          <AnalyticsVisitsSource />\n        </AnalysisChartCard>\n      </div>\n    </div>\n  </div>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/knowledge/attach/attach-modal.vue",
    "content": "<!--\n使用 Ant Design Vue 原生 Form 组件生成表单\n详细用法参考: https://antdv.com/components/form-cn\n注意: 如果 VSCode 配置了自动移除未使用的导入，可能会误删某些组件导入\n-->\n<script setup lang=\"ts\">\nimport type { RuleObject } from 'ant-design-vue/es/form';\n\nimport type { AttachForm } from '#/api/knowledge/attach/model';\n\nimport { computed, ref } from 'vue';\n\nimport { useVbenModal } from '@vben/common-ui';\nimport { $t } from '@vben/locales';\nimport { cloneDeep } from '@vben/utils';\n\nimport {\n  Form,\n  FormItem,\n  Input,\n  Textarea,\n  Select,\n  InputNumber,\n} from 'ant-design-vue';\nimport { pick } from 'lodash-es';\n\nimport { attachAdd, attachInfo, attachUpdate } from '#/api/knowledge/attach';\nimport { Tinymce } from '#/components/tinymce';\n\nconst emit = defineEmits<{ reload: [] }>();\n\nconst isUpdate = ref(false);\nconst title = computed(() => {\n  return isUpdate.value ? $t('pages.common.edit') : $t('pages.common.add');\n});\n\n/**\n * 定义默认值 用于reset\n */\nconst defaultValues: Partial<AttachForm> = {\n  id: undefined,\n  knowledgeId: undefined,\n  name: undefined,\n  type: undefined,\n  ossId: undefined,\n  content: undefined,\n  remark: undefined,\n};\n\n/**\n * 表单数据ref\n */\nconst formData = ref(defaultValues);\n\ntype AntdFormRules<T> = Partial<Record<keyof T, RuleObject[]>> & {\n  [key: string]: RuleObject[];\n};\n/**\n * 表单校验规则\n */\nconst formRules = ref<AntdFormRules<AttachForm>>({\n  knowledgeId: [{ required: true, message: '知识库ID不能为空' }],\n  name: [{ required: true, message: '附件名称不能为空' }],\n  type: [{ required: true, message: '附件类型不能为空' }],\n});\n\nconst attachTypeOptions = [\n  { label: 'txt', value: 'txt' },\n  { label: 'pdf', value: 'pdf' },\n  { label: 'docx', value: 'docx' },\n  { label: 'xlsx', value: 'xlsx' },\n  { label: 'xls', value: 'xls' },\n  { label: 'csv', value: 'csv' },\n  { label: 'json', value: 'json' },\n  { label: 'pptx', value: 'pptx' },\n];\n\n/**\n * useForm解构出表单方法\n */\nconst { validate, validateInfos, resetFields } = Form.useForm(\n  formData,\n  formRules,\n);\n\nconst [BasicModal, modalApi] = useVbenModal({\n  class: 'w-[550px]',\n  fullscreenButton: false,\n  closeOnClickModal: false,\n  onClosed: handleCancel,\n  onConfirm: handleConfirm,\n  onOpenChange: async (isOpen) => {\n    if (!isOpen) {\n      return null;\n    }\n    modalApi.modalLoading(true);\n\n    const { id } = modalApi.getData() as { id?: number | string };\n    isUpdate.value = !!id;\n\n    if (isUpdate.value && id) {\n      const record = await attachInfo(id);\n      // 只赋值存在的字段\n      const filterRecord = pick(record, Object.keys(defaultValues));\n      formData.value = filterRecord;\n    }\n\n    modalApi.modalLoading(false);\n  },\n});\n\nasync function handleConfirm() {\n  try {\n    modalApi.modalLoading(true);\n    await validate();\n    // 可能会做数据处理 使用cloneDeep深拷贝\n    const data = cloneDeep(formData.value);\n    await (isUpdate.value ? attachUpdate(data) : attachAdd(data));\n    emit('reload');\n    await handleCancel();\n  } catch (error) {\n    console.error(error);\n  } finally {\n    modalApi.modalLoading(false);\n  }\n}\n\nasync function handleCancel() {\n  modalApi.close();\n  formData.value = cloneDeep(defaultValues);\n  resetFields();\n}\n</script>\n\n<template>\n  <BasicModal :title=\"title\">\n    <Form :label-col=\"{ span: 4 }\">\n      <FormItem label=\"知识库ID\" v-bind=\"validateInfos.knowledgeId\">\n        <Input\n          v-model:value=\"formData.knowledgeId\"\n          type=\"number\"\n          :placeholder=\"$t('ui.formRules.required')\"\n        />\n      </FormItem>\n      <FormItem label=\"附件名称\" v-bind=\"validateInfos.name\">\n        <Input\n          v-model:value=\"formData.name\"\n          :placeholder=\"$t('ui.formRules.required')\"\n        />\n      </FormItem>\n      <FormItem label=\"附件类型\" v-bind=\"validateInfos.type\">\n        <Select\n          v-model:value=\"formData.type\"\n          :options=\"attachTypeOptions\"\n          :placeholder=\"$t('ui.formRules.required')\"\n        />\n      </FormItem>\n      <FormItem label=\"对象存储ID\" v-bind=\"validateInfos.ossId\">\n        <Input\n          v-model:value=\"formData.ossId\"\n          :placeholder=\"$t('ui.formRules.required')\"\n        />\n      </FormItem>\n      <FormItem label=\"文档内容\" v-bind=\"validateInfos.content\">\n        <Tinymce :options=\"{ readonly: false }\" v-model=\"formData.content\" />\n      </FormItem>\n      <FormItem label=\"备注\" v-bind=\"validateInfos.remark\">\n        <Textarea\n          v-model:value=\"formData.remark\"\n          :placeholder=\"$t('ui.formRules.required')\"\n          :rows=\"4\"\n        />\n      </FormItem>\n    </Form>\n  </BasicModal>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/knowledge/attach/data.ts",
    "content": "import type { FormSchemaGetter } from '#/adapter/form';\nimport type { VxeGridProps } from '#/adapter/vxe-table';\n\nexport const querySchema: FormSchemaGetter = () => [\n  {\n    component: 'Input',\n    fieldName: 'name',\n    label: '附件名称',\n  },\n];\n\n/**\n * 表格列配置\n * 如果需要使用 i18n 国际化，请使用 getter 函数形式，否则切换语言时列配置不会刷新\n * 使用方式: export const columns: () => VxeGridProps['columns'] = () => [...]\n */\nexport const columns: VxeGridProps['columns'] = [\n  { type: 'checkbox', width: 60 },\n  {\n    title: '附件名称',\n    field: 'name',\n  },\n  {\n    title: '附件类型',\n    field: 'type',\n  },\n  {\n    title: '文档内容',\n    field: 'content',\n  },\n  {\n    title: '备注',\n    field: 'remark',\n  },\n  {\n    field: 'action',\n    fixed: 'right',\n    slots: { default: 'action' },\n    title: '操作',\n    width: 180,\n  },\n];\n"
  },
  {
    "path": "apps/web-antd/src/views/knowledge/attach/index.vue",
    "content": "<script setup lang=\"ts\">\nimport type { VbenFormProps } from '@vben/common-ui';\n\nimport type { VxeGridProps } from '#/adapter/vxe-table';\nimport type { AttachForm } from '#/api/knowledge/attach/model';\n\nimport { Page, useVbenModal } from '@vben/common-ui';\nimport { getVxePopupContainer } from '@vben/utils';\n\nimport { Modal, Popconfirm, Space } from 'ant-design-vue';\n\nimport { useVbenVxeGrid, vxeCheckboxChecked } from '#/adapter/vxe-table';\nimport { attachExport, attachList, attachRemove } from '#/api/knowledge/attach';\nimport { commonDownloadExcel } from '#/utils/file/download';\n\nimport attachModal from './attach-modal.vue';\nimport { columns, querySchema } from './data';\n\nconst formOptions: VbenFormProps = {\n  commonConfig: {\n    labelWidth: 80,\n    componentProps: {\n      allowClear: true,\n    },\n  },\n  schema: querySchema(),\n  wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4',\n  // 处理区间选择器 RangePicker 时间格式映射\n  // 将一个时间区间字段映射为两个独立的开始/结束时间字段，用于搜索和导出\n  // 示例: 将 createTime 字段映射为 params[beginTime] 和 params[endTime]\n  // fieldMappingTime: [\n  //   [\n  //     'createTime', // 表单中的字段名\n  //     ['params[beginTime]', 'params[endTime]'], // 映射后的字段名\n  //     ['YYYY-MM-DD 00:00:00', 'YYYY-MM-DD 23:59:59'], // 时间格式\n  //   ],\n  // ],\n};\n\nconst gridOptions: VxeGridProps = {\n  checkboxConfig: {\n    // 高亮\n    highlight: true,\n    // 翻页时保留选中状态\n    reserve: true,\n    // 点击行选中\n    // trigger: 'row',\n  },\n  // 需要使用i18n注意这里要改成getter形式 否则切换语言不会刷新\n  // columns: columns(),\n  columns,\n  height: 'auto',\n  keepSource: true,\n  pagerConfig: {},\n  proxyConfig: {\n    ajax: {\n      query: async ({ page }, formValues = {}) => {\n        return await attachList({\n          pageNum: page.currentPage,\n          pageSize: page.pageSize,\n          ...formValues,\n        });\n      },\n    },\n  },\n  rowConfig: {\n    keyField: 'id',\n  },\n  // 表格全局唯一标识，用于保存列配置\n  id: 'system-attach-index',\n};\n\nconst [BasicTable, tableApi] = useVbenVxeGrid({\n  formOptions,\n  gridOptions,\n});\n\nconst [AttachModal, modalApi] = useVbenModal({\n  connectedComponent: attachModal,\n});\n\nfunction handleAdd() {\n  modalApi.setData({});\n  modalApi.open();\n}\n\nasync function handleEdit(row: Required<AttachForm>) {\n  modalApi.setData({ id: row.id });\n  modalApi.open();\n}\n\nasync function handleDelete(row: Required<AttachForm>) {\n  await attachRemove(row.id);\n  await tableApi.query();\n}\n\nfunction handleMultiDelete() {\n  const rows = tableApi.grid.getCheckboxRecords();\n  const ids = rows.map((row: Required<AttachForm>) => row.id);\n  Modal.confirm({\n    title: '提示',\n    okType: 'danger',\n    content: `确认删除选中的${ids.length}条记录吗？`,\n    onOk: async () => {\n      await attachRemove(ids);\n      await tableApi.query();\n    },\n  });\n}\n\nfunction handleDownloadExcel() {\n  commonDownloadExcel(\n    attachExport,\n    '知识库附件数据',\n    tableApi.formApi.form.values,\n    {\n      fieldMappingTime: formOptions.fieldMappingTime,\n    },\n  );\n}\n</script>\n\n<template>\n  <Page :auto-content-height=\"true\">\n    <BasicTable table-title=\"知识库附件列表\">\n      <template #toolbar-tools>\n        <Space>\n          <a-button\n            v-access:code=\"['system:attach:export']\"\n            @click=\"handleDownloadExcel\"\n          >\n            {{ $t('pages.common.export') }}\n          </a-button>\n          <a-button\n            :disabled=\"!vxeCheckboxChecked(tableApi)\"\n            danger\n            type=\"primary\"\n            v-access:code=\"['system:attach:remove']\"\n            @click=\"handleMultiDelete\"\n          >\n            {{ $t('pages.common.delete') }}\n          </a-button>\n          <a-button\n            type=\"primary\"\n            v-access:code=\"['system:attach:add']\"\n            @click=\"handleAdd\"\n          >\n            {{ $t('pages.common.add') }}\n          </a-button>\n        </Space>\n      </template>\n      <template #action=\"{ row }\">\n        <Space>\n          <ghost-button\n            v-access:code=\"['system:attach:edit']\"\n            @click.stop=\"handleEdit(row)\"\n          >\n            {{ $t('pages.common.edit') }}\n          </ghost-button>\n          <Popconfirm\n            :get-popup-container=\"getVxePopupContainer\"\n            placement=\"left\"\n            title=\"确认删除？\"\n            @confirm=\"handleDelete(row)\"\n          >\n            <ghost-button\n              danger\n              v-access:code=\"['system:attach:remove']\"\n              @click.stop=\"\"\n            >\n              {{ $t('pages.common.delete') }}\n            </ghost-button>\n          </Popconfirm>\n        </Space>\n      </template>\n    </BasicTable>\n    <AttachModal @reload=\"tableApi.query()\" />\n  </Page>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/knowledge/fragment/data.ts",
    "content": "import type { FormSchemaGetter } from '#/adapter/form';\nimport type { VxeGridProps } from '#/adapter/vxe-table';\n\nexport const querySchema: FormSchemaGetter = () => [\n  {\n    component: 'Input',\n    fieldName: 'content',\n    label: '文档内容',\n  },\n];\n\n/**\n * 表格列配置\n * 如果需要使用 i18n 国际化，请使用 getter 函数形式，否则切换语言时列配置不会刷新\n * 使用方式: export const columns: () => VxeGridProps['columns'] = () => [...]\n */\nexport const columns: VxeGridProps['columns'] = [\n  { type: 'checkbox', width: 60 },\n  {\n    title: '片段索引下标',\n    field: 'idx',\n  },\n  {\n    title: '文档内容',\n    field: 'content',\n  },\n  {\n    title: '备注',\n    field: 'remark',\n  },\n  {\n    field: 'action',\n    fixed: 'right',\n    slots: { default: 'action' },\n    title: '操作',\n    width: 180,\n  },\n];\n"
  },
  {
    "path": "apps/web-antd/src/views/knowledge/fragment/fragment-modal.vue",
    "content": "<!--\n使用 Ant Design Vue 原生 Form 组件生成表单\n详细用法参考: https://antdv.com/components/form-cn\n注意: 如果 VSCode 配置了自动移除未使用的导入，可能会误删某些组件导入\n-->\n<script setup lang=\"ts\">\nimport type { RuleObject } from 'ant-design-vue/es/form';\n\nimport type { FragmentForm } from '#/api/knowledge/fragment/model';\n\nimport { computed, ref } from 'vue';\n\nimport { useVbenModal } from '@vben/common-ui';\nimport { $t } from '@vben/locales';\nimport { cloneDeep } from '@vben/utils';\n\nimport { Form, FormItem, Input, Textarea, InputNumber } from 'ant-design-vue';\nimport { pick } from 'lodash-es';\n\nimport {\n  fragmentAdd,\n  fragmentInfo,\n  fragmentUpdate,\n} from '#/api/knowledge/fragment';\nimport { Tinymce } from '#/components/tinymce';\n\nconst emit = defineEmits<{ reload: [] }>();\n\nconst isUpdate = ref(false);\nconst title = computed(() => {\n  return isUpdate.value ? $t('pages.common.edit') : $t('pages.common.add');\n});\n\n/**\n * 定义默认值 用于reset\n */\nconst defaultValues: Partial<FragmentForm> = {\n  id: undefined,\n  attachId: undefined,\n  idx: undefined,\n  content: undefined,\n  remark: undefined,\n};\n\n/**\n * 表单数据ref\n */\nconst formData = ref(defaultValues);\n\ntype AntdFormRules<T> = Partial<Record<keyof T, RuleObject[]>> & {\n  [key: string]: RuleObject[];\n};\n/**\n * 表单校验规则\n */\nconst formRules = ref<AntdFormRules<FragmentForm>>({\n  attachId: [{ required: true, message: '附件ID不能为空' }],\n  idx: [{ required: true, message: '片段索引下标不能为空' }],\n  content: [{ required: true, message: '文档内容不能为空' }],\n});\n\n/**\n * useForm解构出表单方法\n */\nconst { validate, validateInfos, resetFields } = Form.useForm(\n  formData,\n  formRules,\n);\n\nconst [BasicModal, modalApi] = useVbenModal({\n  class: 'w-[550px]',\n  fullscreenButton: false,\n  closeOnClickModal: false,\n  onClosed: handleCancel,\n  onConfirm: handleConfirm,\n  onOpenChange: async (isOpen) => {\n    if (!isOpen) {\n      return null;\n    }\n    modalApi.modalLoading(true);\n\n    const { id } = modalApi.getData() as { id?: number | string };\n    isUpdate.value = !!id;\n\n    if (isUpdate.value && id) {\n      const record = await fragmentInfo(id);\n      // 只赋值存在的字段\n      const filterRecord = pick(record, Object.keys(defaultValues));\n      formData.value = filterRecord;\n    }\n\n    modalApi.modalLoading(false);\n  },\n});\n\nasync function handleConfirm() {\n  try {\n    modalApi.modalLoading(true);\n    await validate();\n    // 可能会做数据处理 使用cloneDeep深拷贝\n    const data = cloneDeep(formData.value);\n    await (isUpdate.value ? fragmentUpdate(data) : fragmentAdd(data));\n    emit('reload');\n    await handleCancel();\n  } catch (error) {\n    console.error(error);\n  } finally {\n    modalApi.modalLoading(false);\n  }\n}\n\nasync function handleCancel() {\n  modalApi.close();\n  formData.value = cloneDeep(defaultValues);\n  resetFields();\n}\n</script>\n\n<template>\n  <BasicModal :title=\"title\">\n    <Form :label-col=\"{ span: 4 }\">\n      <FormItem label=\"附件ID\" v-bind=\"validateInfos.attachId\">\n        <Input\n          v-model:value=\"formData.attachId\"\n          type=\"number\"\n          :placeholder=\"$t('ui.formRules.required')\"\n        />\n      </FormItem>\n      <FormItem label=\"片段索引下标\" v-bind=\"validateInfos.idx\">\n        <InputNumber\n          v-model:value=\"formData.idx\"\n          style=\"width: 100%\"\n          :placeholder=\"$t('ui.formRules.required')\"\n          :min=\"0\"\n        />\n      </FormItem>\n      <FormItem label=\"文档内容\" v-bind=\"validateInfos.content\">\n        <Tinymce :options=\"{ readonly: false }\" v-model=\"formData.content\" />\n      </FormItem>\n      <FormItem label=\"备注\" v-bind=\"validateInfos.remark\">\n        <Textarea\n          v-model:value=\"formData.remark\"\n          :placeholder=\"$t('ui.formRules.required')\"\n          :rows=\"4\"\n        />\n      </FormItem>\n    </Form>\n  </BasicModal>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/knowledge/fragment/index.vue",
    "content": "<script setup lang=\"ts\">\nimport type { VbenFormProps } from '@vben/common-ui';\n\nimport type { VxeGridProps } from '#/adapter/vxe-table';\nimport type { FragmentForm } from '#/api/knowledge/fragment/model';\n\nimport { Page, useVbenModal } from '@vben/common-ui';\nimport { getVxePopupContainer } from '@vben/utils';\n\nimport { Modal, Popconfirm, Space } from 'ant-design-vue';\n\nimport { useVbenVxeGrid, vxeCheckboxChecked } from '#/adapter/vxe-table';\nimport {\n  fragmentExport,\n  fragmentList,\n  fragmentRemove,\n} from '#/api/knowledge/fragment';\nimport { commonDownloadExcel } from '#/utils/file/download';\n\nimport { columns, querySchema } from './data';\nimport fragmentModal from './fragment-modal.vue';\n\nconst formOptions: VbenFormProps = {\n  commonConfig: {\n    labelWidth: 80,\n    componentProps: {\n      allowClear: true,\n    },\n  },\n  schema: querySchema(),\n  wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4',\n  // 处理区间选择器 RangePicker 时间格式映射\n  // 将一个时间区间字段映射为两个独立的开始/结束时间字段，用于搜索和导出\n  // 示例: 将 createTime 字段映射为 params[beginTime] 和 params[endTime]\n  // fieldMappingTime: [\n  //   [\n  //     'createTime', // 表单中的字段名\n  //     ['params[beginTime]', 'params[endTime]'], // 映射后的字段名\n  //     ['YYYY-MM-DD 00:00:00', 'YYYY-MM-DD 23:59:59'], // 时间格式\n  //   ],\n  // ],\n};\n\nconst gridOptions: VxeGridProps = {\n  checkboxConfig: {\n    // 高亮\n    highlight: true,\n    // 翻页时保留选中状态\n    reserve: true,\n    // 点击行选中\n    // trigger: 'row',\n  },\n  // 需要使用i18n注意这里要改成getter形式 否则切换语言不会刷新\n  // columns: columns(),\n  columns,\n  height: 'auto',\n  keepSource: true,\n  pagerConfig: {},\n  proxyConfig: {\n    ajax: {\n      query: async ({ page }, formValues = {}) => {\n        return await fragmentList({\n          pageNum: page.currentPage,\n          pageSize: page.pageSize,\n          ...formValues,\n        });\n      },\n    },\n  },\n  rowConfig: {\n    keyField: 'id',\n  },\n  // 表格全局唯一标识，用于保存列配置\n  id: 'system-fragment-index',\n};\n\nconst [BasicTable, tableApi] = useVbenVxeGrid({\n  formOptions,\n  gridOptions,\n});\n\nconst [FragmentModal, modalApi] = useVbenModal({\n  connectedComponent: fragmentModal,\n});\n\nfunction handleAdd() {\n  modalApi.setData({});\n  modalApi.open();\n}\n\nasync function handleEdit(row: Required<FragmentForm>) {\n  modalApi.setData({ id: row.id });\n  modalApi.open();\n}\n\nasync function handleDelete(row: Required<FragmentForm>) {\n  await fragmentRemove(row.id);\n  await tableApi.query();\n}\n\nfunction handleMultiDelete() {\n  const rows = tableApi.grid.getCheckboxRecords();\n  const ids = rows.map((row: Required<FragmentForm>) => row.id);\n  Modal.confirm({\n    title: '提示',\n    okType: 'danger',\n    content: `确认删除选中的${ids.length}条记录吗？`,\n    onOk: async () => {\n      await fragmentRemove(ids);\n      await tableApi.query();\n    },\n  });\n}\n\nfunction handleDownloadExcel() {\n  commonDownloadExcel(\n    fragmentExport,\n    '知识片段数据',\n    tableApi.formApi.form.values,\n    {\n      fieldMappingTime: formOptions.fieldMappingTime,\n    },\n  );\n}\n</script>\n\n<template>\n  <Page :auto-content-height=\"true\">\n    <BasicTable table-title=\"知识片段列表\">\n      <template #toolbar-tools>\n        <Space>\n          <a-button\n            v-access:code=\"['system:fragment:export']\"\n            @click=\"handleDownloadExcel\"\n          >\n            {{ $t('pages.common.export') }}\n          </a-button>\n          <a-button\n            :disabled=\"!vxeCheckboxChecked(tableApi)\"\n            danger\n            type=\"primary\"\n            v-access:code=\"['system:fragment:remove']\"\n            @click=\"handleMultiDelete\"\n          >\n            {{ $t('pages.common.delete') }}\n          </a-button>\n          <a-button\n            type=\"primary\"\n            v-access:code=\"['system:fragment:add']\"\n            @click=\"handleAdd\"\n          >\n            {{ $t('pages.common.add') }}\n          </a-button>\n        </Space>\n      </template>\n      <template #action=\"{ row }\">\n        <Space>\n          <ghost-button\n            v-access:code=\"['system:fragment:edit']\"\n            @click.stop=\"handleEdit(row)\"\n          >\n            {{ $t('pages.common.edit') }}\n          </ghost-button>\n          <Popconfirm\n            :get-popup-container=\"getVxePopupContainer\"\n            placement=\"left\"\n            title=\"确认删除？\"\n            @confirm=\"handleDelete(row)\"\n          >\n            <ghost-button\n              danger\n              v-access:code=\"['system:fragment:remove']\"\n              @click.stop=\"\"\n            >\n              {{ $t('pages.common.delete') }}\n            </ghost-button>\n          </Popconfirm>\n        </Space>\n      </template>\n    </BasicTable>\n    <FragmentModal @reload=\"tableApi.query()\" />\n  </Page>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/knowledge/info/components/KnowledgeAddModal.vue",
    "content": "<script setup lang=\"ts\">\nimport type { RuleObject } from 'ant-design-vue/es/form';\nimport type { InfoForm } from '#/api/knowledge/info/model';\n\nimport { ref, computed, h } from 'vue';\nimport { useVbenModal } from '@vben/common-ui';\nimport { $t } from '@vben/locales';\nimport { cloneDeep } from '@vben/utils';\n\nimport {\n  Form,\n  FormItem,\n  Input,\n  Select,\n  Radio,\n  RadioGroup,\n  Switch,\n  Slider,\n  InputNumber,\n  message,\n  Tooltip,\n  Tag\n} from 'ant-design-vue';\nimport { QuestionCircleOutlined } from '@ant-design/icons-vue';\n\nimport { infoAdd } from '#/api/knowledge/info';\nimport { modelList } from '#/api/chat/model';\n\nconst emit = defineEmits<{ reload: [] }>();\n\nconst isUpdate = ref(false);\nconst title = computed(() => '新增知识库');\n\nconst defaultValues: Partial<InfoForm> = {\n  name: '',\n  share: 0,\n  description: '',\n  remark: '',\n  separator: '\\\\n',\n  overlapChar: 10,\n  retrieveLimit: 10,\n  textBlockSize: 1000,\n  vectorModel: 'weaviate',\n  embeddingModel: undefined,\n  enableRerank: 0,\n  rerankModel: undefined,\n  enableHybrid: 0,\n  hybridAlpha: 0.5,\n};\n\nconst formData = ref<Partial<InfoForm>>({ ...defaultValues });\n\ntype AntdFormRules<T> = Partial<Record<keyof T, RuleObject[]>> & {\n  [key: string]: RuleObject[];\n};\n\nconst formRules = ref<AntdFormRules<InfoForm>>({\n  name: [{ required: true, message: '知识库名称不能为空' }],\n  share: [{ required: true, message: '请选择是否公开' }],\n  embeddingModel: [{ required: true, message: '请选择向量模型' }],\n  vectorModel: [{ required: true, message: '请选择向量库' }],\n});\n\nconst embeddingModelOptions = ref<Array<{ label: string; value: string }>>([]);\nconst rerankModelOptions = ref<Array<{ label: string; value: string }>>([]);\nconst vectorModelOptions = [\n  { label: 'Weaviate', value: 'weaviate' },\n  { label: 'Milvus', value: 'milvus' },\n  { label: 'Qdrant', value: 'qdrant' },\n];\n\nconst shareOptions = [\n  { label: '是', value: 1 },\n  { label: '否', value: 0 },\n];\n\nconst { validate, validateInfos, resetFields } = Form.useForm(\n  formData,\n  formRules,\n);\n\nasync function fetchEmbeddingModels() {\n  try {\n    const response = await modelList({ category: 'vector', pageSize: 1000 });\n    const models = Array.isArray(response) ? response : (response.rows || response.records || []);\n    embeddingModelOptions.value = models.map((model: any) => ({\n      label: model.modelDescribe || model.modelName,\n      value: model.modelName,\n    }));\n    if (embeddingModelOptions.value.length > 0 && !formData.value.embeddingModel) {\n      formData.value.embeddingModel = embeddingModelOptions.value[0]?.value;\n    }\n  } catch (error) {\n    console.error('Failed to fetch embedding models:', error);\n  }\n}\n\nasync function fetchRerankModels() {\n  try {\n    const response = await modelList({ category: 'rerank', pageSize: 1000 });\n    const models = Array.isArray(response) ? response : (response.rows || response.records || []);\n    rerankModelOptions.value = models.map((model: any) => ({\n      label: model.modelDescribe || model.modelName,\n      value: model.modelName,\n    }));\n  } catch (error) {\n    console.error('Failed to fetch rerank models:', error);\n  }\n}\n\nconst [BasicModal, modalApi] = useVbenModal({\n  class: 'w-[600px]',\n  onClosed: handleClosed,\n  onConfirm: handleConfirm,\n  onOpenChange: async (isOpen) => {\n    if (!isOpen) return;\n    modalApi.modalLoading(true);\n    await Promise.all([fetchEmbeddingModels(), fetchRerankModels()]);\n    modalApi.modalLoading(false);\n  },\n});\n\nasync function handleConfirm() {\n  try {\n    modalApi.lock(true);\n    await validate();\n    const data = cloneDeep(formData.value) as InfoForm;\n    await infoAdd(data);\n    message.success('新增成功');\n    emit('reload');\n    modalApi.close();\n  } catch (error) {\n    console.error(error);\n  } finally {\n    modalApi.lock(false);\n  }\n}\n\nfunction handleClosed() {\n  formData.value = { ...defaultValues };\n  resetFields();\n}\n// 预设刻度样式\nconst renderMark = (label: string) => h('span', { style: { fontSize: '10px', opacity: 0.7 } }, label);\n\nconst alphaMarks = {\n  0.3: renderMark('偏向量'),\n  0.5: renderMark('平衡'),\n  0.7: renderMark('偏全文')\n};\n\nconst limitMarks = {\n  3: renderMark('精简'),\n  5: renderMark('默认'),\n  10: renderMark('丰富'),\n  20: renderMark('20')\n};\n</script>\n\n<template>\n  <BasicModal :title=\"title\">\n    <Form layout=\"vertical\">\n      <FormItem label=\"知识库名称\" v-bind=\"validateInfos.name\">\n        <Input v-model:value=\"formData.name\" placeholder=\"请输入知识库名称\" />\n      </FormItem>\n      \n      <FormItem label=\"是否公开\" v-bind=\"validateInfos.share\">\n        <RadioGroup v-model:value=\"formData.share\" :options=\"shareOptions\" option-type=\"button\" button-style=\"solid\" />\n      </FormItem>\n\n      <FormItem label=\"向量库\" v-bind=\"validateInfos.vectorModel\">\n        <Select\n          v-model:value=\"formData.vectorModel\"\n          :options=\"vectorModelOptions\"\n          placeholder=\"请选择向量库\"\n        />\n      </FormItem>\n\n      <FormItem label=\"向量模型\" v-bind=\"validateInfos.embeddingModel\">\n        <Select\n          v-model:value=\"formData.embeddingModel\"\n          :options=\"embeddingModelOptions\"\n          placeholder=\"请选择向量模型\"\n          show-search\n        />\n      </FormItem>\n\n      <FormItem>\n        <template #label>\n          <div class=\"flex items-center gap-1\">\n            <span>启用重排</span>\n            <Tooltip placement=\"top\">\n              <template #title>\n                在初步检索后的结果中，使用重排模型对待选文本块与原始问题进行二次相关性精打分。这能有效提升回答的准确度。\n              </template>\n              <QuestionCircleOutlined class=\"text-gray-400 text-xs cursor-help\" />\n            </Tooltip>\n          </div>\n        </template>\n        <Switch \n          v-model:checked=\"formData.enableRerank\" \n          :un-checked-value=\"0\" \n          :checked-value=\"1\" \n        />\n        <span class=\"ml-2 text-gray-400 text-xs text-opacity-70\">开启后将对检索结果进行精排，提升准确率</span>\n      </FormItem>\n\n      <FormItem v-if=\"formData.enableRerank\" label=\"重排模型\">\n        <Select\n          v-model:value=\"formData.rerankModel\"\n          :options=\"rerankModelOptions\"\n          placeholder=\"请选择重排模型\"\n          show-search\n        />\n      </FormItem>\n\n      <FormItem>\n        <template #label>\n          <div class=\"flex items-center gap-1\">\n            <span>混合检索</span>\n            <Tooltip placement=\"top\">\n              <template #title>\n                系统采用 RRF (Reciprocal Rank Fusion) 算法合并检索结果。该算法通过综合文本块在向量搜索和全文搜索中的“排名顺序”计算融合得分，能够给予两路同时命中的内容更高权重，显著提升搜索精准度。\n              </template>\n              <QuestionCircleOutlined class=\"text-gray-400 text-xs cursor-help\" />\n            </Tooltip>\n          </div>\n        </template>\n        <Switch \n          v-model:checked=\"formData.enableHybrid\" \n          :un-checked-value=\"0\" \n          :checked-value=\"1\" \n        />\n      </FormItem>\n \n      <FormItem v-if=\"formData.enableHybrid\" label=\"检索权重 (α)\">\n        <div class=\"flex flex-col w-full\">\n          <div class=\"flex justify-between items-center mb-1 pr-4\">\n            <div class=\"flex items-center gap-2\">\n              <span class=\"italic text-gray-500 text-xs\">vector</span>\n              <span class=\"bg-gray-100 dark:bg-zinc-800 text-primary px-1.5 py-0.5 rounded text-[10px] font-mono border border-gray-200 dark:border-gray-700 leading-none\">\n                {{ (1 - (formData.hybridAlpha || 0.5)).toFixed(2) }}\n              </span>\n            </div>\n            <div class=\"flex items-center gap-2\">\n              <span class=\"italic text-gray-500 text-xs\">full-text</span>\n              <span class=\"bg-gray-100 dark:bg-zinc-800 text-primary px-1.5 py-0.5 rounded text-[10px] font-mono border border-gray-200 dark:border-gray-700 leading-none\">\n                {{ (formData.hybridAlpha || 0.5).toFixed(2) }}\n              </span>\n            </div>\n          </div>\n          <div class=\"flex items-center gap-4 pb-6\">\n            <Slider \n              v-model:value=\"formData.hybridAlpha\" \n              :min=\"0\" :max=\"1\" :step=\"0.01\" \n              :marks=\"alphaMarks\"\n              class=\"flex-1\"\n            />\n            <InputNumber v-model:value=\"formData.hybridAlpha\" :min=\"0\" :max=\"1\" :step=\"0.01\" size=\"small\" class=\"text-xs w-16\" />\n          </div>\n        </div>\n      </FormItem>\n\n      <FormItem label=\"检索条数\">\n        <div class=\"flex items-center gap-4 pb-6\">\n          <Slider \n            v-model:value=\"formData.retrieveLimit\" \n            :min=\"1\" :max=\"20\" \n            :marks=\"limitMarks\"\n            class=\"flex-1\"\n          />\n          <InputNumber v-model:value=\"formData.retrieveLimit\" :min=\"1\" :max=\"20\" size=\"small\" class=\"text-xs w-16\" />\n        </div>\n      </FormItem>\n\n      <FormItem label=\"备注\" v-bind=\"validateInfos.remark\">\n        <Input.TextArea v-model:value=\"formData.remark\" placeholder=\"请输入备注\" :rows=\"2\" />\n      </FormItem>\n\n      <FormItem label=\"描述\" v-bind=\"validateInfos.description\">\n        <Input.TextArea v-model:value=\"formData.description\" placeholder=\"请输入描述\" :rows=\"3\" />\n      </FormItem>\n    </Form>\n  </BasicModal>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/knowledge/info/data.ts",
    "content": "import type { FormSchemaGetter } from '#/adapter/form';\nimport type { VxeGridProps } from '#/adapter/vxe-table';\n\nexport const querySchema: FormSchemaGetter = () => [\n  {\n    component: 'Input',\n    fieldName: 'name',\n    label: '知识库名称',\n  },\n];\n\n/**\n * 表格列配置\n * 如果需要使用 i18n 国际化，请使用 getter 函数形式，否则切换语言时列配置不会刷新\n * 使用方式: export const columns: () => VxeGridProps['columns'] = () => [...]\n */\nexport const columns: VxeGridProps['columns'] = [\n  { type: 'checkbox', width: 60 },\n  {\n    title: '知识名称',\n    field: 'name',\n  },\n  {\n    title: '知识描述',\n    field: 'description',\n  },\n  {\n    title: '备注',\n    field: 'remark',\n  },\n  {\n    title: '文档数',\n    field: 'documentCount', // Use documentCount or fileCount, depending on backend. We can set it to field 'docCount'\n  },\n  {\n    field: 'action',\n    fixed: 'right',\n    slots: { default: 'action' },\n    title: '操作',\n    width: 180,\n  },\n];\n"
  },
  {
    "path": "apps/web-antd/src/views/knowledge/info/detail/components/FileManagement.vue",
    "content": "<script setup lang=\"ts\">\nimport { ref, computed, watch } from 'vue';\nimport {\n  Button,\n  Upload,\n  Table,\n  Space,\n  Popconfirm,\n  message,\n  Tooltip,\n  Modal,\n  Descriptions,\n  DescriptionsItem,\n  Image,\n  Spin,\n  Badge,\n  Switch,\n  Typography,\n  TypographyParagraph,\n  Drawer,\n} from 'ant-design-vue';\nimport { InboxOutlined, CopyOutlined } from '@ant-design/icons-vue';\nimport { useAppConfig } from '@vben/hooks';\nimport { useAccessStore } from '@vben/stores';\nimport { attachList, attachRemove, attachParse } from '#/api/knowledge/attach';\nimport { fragmentList } from '#/api/knowledge/fragment';\nimport { ossInfo, checkLoginBeforeDownload } from '#/api/system/oss';\nimport { downloadByUrl } from '#/utils/file/download';\nimport { stringify } from '@vben/request';\nimport { requestClient } from '#/api/request';\n\nconst props = defineProps<{\n  knowledgeId?: string | number;\n}>();\n\nconst { apiURL, clientId } = useAppConfig(\n  import.meta.env,\n  import.meta.env.PROD,\n);\nconst accessStore = useAccessStore();\n\nconst attachmentData = ref([]);\nconst uploadUrl = `${apiURL}/system/attach/upload`;\nconst loading = ref(false);\nconst uploading = ref(false);\nconst headers = {\n  Authorization: `Bearer ${accessStore.accessToken}`,\n  clientId,\n};\n\nconst autoParse = ref(true);\n\nconst uploadPayload = computed(() => ({\n  knowledgeId: props.knowledgeId,\n  autoParse: autoParse.value,\n}));\n\nconst columns = [\n  { title: '附件名称', dataIndex: 'name', key: 'name' },\n  { title: '附件类型', dataIndex: 'type', key: 'type' },\n  { title: '状态', dataIndex: 'status', key: 'status', width: 100 },\n  { title: '上传时间', dataIndex: 'createTime', key: 'createTime', width: 180 },\n  { title: '分块数', dataIndex: 'fragmentCount', key: 'fragmentCount', width: 80 },\n  { title: '操作', key: 'action', width: 280 },\n];\n\nconst statusMap = {\n  0: { text: '待解析', status: 'default' },\n  1: { text: '解析中', status: 'processing' },\n  2: { text: '已解析', status: 'success' },\n  3: { text: '解析失败', status: 'error' },\n};\n\nconst fragmentVisible = ref(false);\nconst fragmentLoading = ref(false);\nconst fragmentData = ref([]);\nconst fragmentColumns = [\n  { title: '序号', dataIndex: 'idx', key: 'idx', width: 80 },\n  { title: '片段内容', dataIndex: 'content', key: 'content' },\n];\n\nconst fileDetailVisible = ref(false);\nconst fileDetailLoading = ref(false);\nconst fileDetailData = ref<any>(null);\n\nconst uploadModalVisible = ref(false);\nconst fileList = ref<any[]>([]);\n\nconst fragmentDetailVisible = ref(false);\nconst currentFragment = ref<any>(null);\nconst currentSourceName = ref('');\n\nfunction handleViewFragmentDetail(record: any, sourceName?: string) {\n  currentFragment.value = record;\n  currentSourceName.value = sourceName || '';\n  fragmentDetailVisible.value = true;\n}\n\nasync function handleCopy(text: string) {\n  try {\n    await navigator.clipboard.writeText(text);\n    message.success('已复制到剪贴板');\n  } catch (err) {\n    message.error('复制失败');\n  }\n}\n\nfunction handleOpenUpload() {\n  fileList.value = [];\n  uploadModalVisible.value = true;\n}\n\nasync function loadAttachments() {\n  if (!props.knowledgeId) return;\n  loading.value = true;\n  try {\n    const res = await attachList({\n      knowledgeId: props.knowledgeId,\n      pageSize: 100,\n    });\n    attachmentData.value = res.rows || [];\n  } finally {\n    loading.value = false;\n  }\n}\n\nwatch(() => props.knowledgeId, () => {\n  if (props.knowledgeId) {\n    loadAttachments();\n  }\n}, { immediate: true });\n\nfunction handleBeforeUpload(file: any) {\n  fileList.value = [...fileList.value, file];\n  return false;\n}\n\nfunction handleRemove(file: any) {\n  const index = fileList.value.indexOf(file);\n  const newFileList = fileList.value.slice();\n  newFileList.splice(index, 1);\n  fileList.value = newFileList;\n}\n\nasync function handleManualUpload() {\n  if (fileList.value.length === 0) {\n    message.warning('请先选择要上传的文件');\n    return;\n  }\n\n  uploading.value = true;\n  try {\n    for (const file of fileList.value) {\n      const formData = new FormData();\n      // ant-design-vue 的 fileList 中的项可能是包装过的，需获取 originFileObj\n      const rawFile = file.originFileObj || file;\n      formData.append('file', rawFile);\n      formData.append('knowledgeId', String(props.knowledgeId));\n      formData.append('autoParse', String(autoParse.value));\n\n      // 使用原生 requestClient 发送请求\n      await requestClient.post('/system/attach/upload', formData);\n    }\n    \n    message.success('所有文件上传成功');\n    fileList.value = [];\n    uploadModalVisible.value = false;\n    await loadAttachments();\n  } catch (error: any) {\n    message.error(error.message || '上传失败');\n  } finally {\n    uploading.value = false;\n  }\n}\n\nasync function handleDeleteAttachment(record: any) {\n  try {\n    await attachRemove(record.id);\n    await loadAttachments();\n  } catch (error) {\n    message.error('删除失败');\n  }\n}\n\nasync function handleParse(record: any) {\n  try {\n    loading.value = true;\n    await attachParse(record.id);\n    message.success('已触发解析，请稍后刷新查看状态');\n    await loadAttachments();\n  } catch (error) {\n    message.error('触发解析失败');\n  } finally {\n    loading.value = false;\n  }\n}\n\nasync function handleFragment(record: any) {\n  fragmentLoading.value = true;\n  fragmentVisible.value = true;\n  try {\n    const res = await fragmentList({ docId: record.docId, pageSize: 100 });\n    // 初始化展开状态\n    fragmentData.value = (res.rows || []).map((item: any) => ({ ...item, _expanded: false }));\n  } catch (error) {\n    message.error('加载片段失败');\n  } finally {\n    fragmentLoading.value = false;\n  }\n}\n\nfunction closeFragment() {\n  fragmentVisible.value = false;\n  fragmentData.value = [];\n}\n\nasync function handleViewFile(record: any) {\n  if (!record.ossId) return message.error('文件信息缺失');\n  fileDetailLoading.value = true;\n  fileDetailVisible.value = true;\n  try {\n    const res = await ossInfo(record.ossId);\n    if (res && res.length > 0) fileDetailData.value = res[0];\n    else { message.error('未找到对应文件'); closeFileDetail(); }\n  } catch (error) {\n    message.error('获取文件详情失败'); closeFileDetail();\n  } finally { fileDetailLoading.value = false; }\n}\n\nasync function handleDownloadFile(ossId: string, fileName: string) {\n  try {\n    await checkLoginBeforeDownload();\n    const params = { clientid: clientId, Authorization: `Bearer ${accessStore.accessToken}` };\n    downloadByUrl({ fileName, url: `${apiURL}/resource/oss/download/${ossId}?${stringify(params)}` });\n  } catch (error) { message.error('下载失败'); }\n}\n\nfunction closeFileDetail() { fileDetailVisible.value = false; fileDetailData.value = null; }\n\nfunction isImageFile(fileSuffix: string) {\n  return fileSuffix ? ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'].some(t => fileSuffix.toLowerCase().includes(t)) : false;\n}\n</script>\n\n<template>\n  <div class=\"p-2\">\n    <!-- 顶部操作栏 -->\n    <div class=\"mb-4 flex items-center justify-between\">\n      <div class=\"flex items-center gap-2\">\n        <Button type=\"primary\" @click=\"handleOpenUpload\">\n          <template #icon><InboxOutlined /></template>\n          上传文档\n        </Button>\n      </div>\n      <div class=\"flex items-center gap-2\">\n        <Tooltip title=\"刷新列表以获取最新解析状态\">\n          <Button @click=\"loadAttachments\">\n            刷新\n          </Button>\n        </Tooltip>\n      </div>\n    </div>\n\n    <div class=\"relative\">\n      <div v-if=\"uploading\" class=\"absolute inset-0 bg-white/50 backdrop-blur-sm z-10 flex items-center justify-center\">\n        <Spin />\n      </div>\n      \n      <Table\n        :columns=\"columns\"\n        :data-source=\"attachmentData\"\n        :loading=\"loading\"\n        :pagination=\"false\"\n        size=\"middle\"\n        bordered\n        row-key=\"id\"\n      >\n        <template #bodyCell=\"{ column, record }\">\n          <template v-if=\"column.key === 'status'\">\n            <Tooltip v-if=\"record.status === 3 && record.remark\" :title=\"record.remark\">\n               <Badge v-bind=\"statusMap[record.status]\" style=\"cursor: help\" />\n            </Tooltip>\n            <Badge v-else-if=\"statusMap[record.status]\" v-bind=\"statusMap[record.status]\" />\n            <span v-else>未知</span>\n          </template>\n          <template v-else-if=\"column.key === 'createTime'\">\n            {{ record.createTime ? new Date(record.createTime).toLocaleString('zh-CN', { hour12: false }) : '-' }}\n          </template>\n          <template v-else-if=\"column.key === 'action'\">\n            <Space>\n              <Button \n                v-if=\"record.status === 0 || record.status === 3\"\n                type=\"link\" \n                size=\"small\" \n                @click=\"handleParse(record)\"\n              >\n                解析\n              </Button>\n              <Button \n                v-if=\"record.status === 2\"\n                type=\"link\" \n                size=\"small\" \n                @click=\"handleFragment(record)\"\n              >\n                知识片段\n              </Button>\n              <Button type=\"link\" size=\"small\" @click=\"handleViewFile(record)\">查看源文件</Button>\n              <Popconfirm title=\"确定要删除这个文件吗？\" @confirm=\"handleDeleteAttachment(record)\">\n                <Button type=\"link\" danger size=\"small\">删除</Button>\n              </Popconfirm>\n            </Space>\n          </template>\n        </template>\n      </Table>\n    </div>\n\n    <!-- Modals -->\n    <Modal v-model:open=\"uploadModalVisible\" title=\"上传文档\" :width=\"600\" :footer=\"null\">\n      <div class=\"mb-6 p-4 bg-blue-50/50 rounded-lg border border-blue-100\">\n        <div class=\"flex items-center justify-between mb-2\">\n          <span class=\"text-sm font-semibold text-blue-800\">解析策略设置</span>\n          <Switch v-model:checked=\"autoParse\" checked-children=\"自动解析\" un-checked-children=\"仅上传\" />\n        </div>\n        <p class=\"text-xs text-gray-500 leading-relaxed\">\n          <b>自动解析</b>：上传完成后立即开始文档切块并存入向量库，适用于需要立即检索的文件。<br/>\n          <b>仅上传</b>：仅保存到云端存储，您可以稍后在列表中手动触发解析。\n        </p>\n      </div>\n\n      <Upload.Dragger\n        v-model:file-list=\"fileList\"\n        :before-upload=\"handleBeforeUpload\"\n        @remove=\"handleRemove\"\n        :show-upload-list=\"true\"\n        accept=\".txt,.pdf,.docx,.pptx,.xlsx,.xls,.csv,.json\"\n        multiple\n        name=\"file\"\n      >\n        <p class=\"ant-upload-drag-icon\">\n          <InboxOutlined />\n        </p>\n        <p class=\"ant-upload-text font-medium\">点击或将文件拖拽到此区域上传</p>\n        <p class=\"ant-upload-hint\">支持多文件选择，点击下方“开始上传”按钮触发保存</p>\n      </Upload.Dragger>\n\n      <div class=\"mt-6 flex justify-end gap-3\">\n        <Button @click=\"uploadModalVisible = false\">取消</Button>\n        <Button \n          type=\"primary\" \n          :loading=\"uploading\" \n          @click=\"handleManualUpload\"\n        >\n          确定并保存\n        </Button>\n      </div>\n    </Modal>\n\n  <Modal v-model:open=\"fragmentVisible\" title=\"知识分片列表\" :width=\"1000\" :footer=\"null\" :destroyOnClose=\"true\">\n    <Table :columns=\"fragmentColumns\" :data-source=\"fragmentData\" :loading=\"fragmentLoading\" :pagination=\"{ pageSize: 10 }\" size=\"middle\" bordered row-key=\"id\">\n      <template #bodyCell=\"{ column, record }\">\n        <template v-if=\"column.key === 'idx'\">\n          {{ Number(record.idx) + 1 }}\n        </template>\n        <template v-else-if=\"column.key === 'content'\">\n          <div \n            class=\"cursor-pointer hover:text-blue-600 transition-colors\"\n            @click=\"handleViewFragmentDetail(record)\"\n          >\n            <div class=\"line-clamp-3\" style=\"white-space: pre-wrap; font-size: 14px; line-height: 1.6;\">\n              {{ record.content }}\n            </div>\n          </div>\n        </template>\n      </template>\n    </Table>\n  </Modal>\n\n    <Modal v-model:open=\"fileDetailVisible\" title=\"文件详情\" :width=\"600\" :footer=\"null\">\n      <div v-if=\"fileDetailLoading\" class=\"flex justify-center p-4\"><Spin /></div>\n      <div v-else-if=\"fileDetailData\">\n        <Descriptions :column=\"1\" bordered size=\"small\">\n          <DescriptionsItem label=\"系统文件名\">{{ fileDetailData.fileName }}</DescriptionsItem>\n          <DescriptionsItem label=\"原始文件名\">{{ fileDetailData.originalName }}</DescriptionsItem>\n          <DescriptionsItem label=\"扩展名\">{{ fileDetailData.fileSuffix }}</DescriptionsItem>\n          <DescriptionsItem label=\"创建时间\">{{ fileDetailData.createTime }}</DescriptionsItem>\n          <DescriptionsItem label=\"上传人\">{{ fileDetailData.createByName }}</DescriptionsItem>\n        </Descriptions>\n        \n        <div v-if=\"isImageFile(fileDetailData.fileSuffix)\" class=\"mt-4 text-center\">\n          <Image :src=\"fileDetailData.url\" :preview=\"true\" style=\"max-height: 200px\" />\n        </div>\n        <div v-else class=\"mt-4 text-center\">\n          <a :href=\"fileDetailData.url\" target=\"_blank\">{{ fileDetailData.url }}</a>\n        </div>\n\n        <div class=\"flex justify-center gap-4 mt-6\">\n          <Button @click=\"closeFileDetail\">关闭</Button>\n          <Button type=\"primary\" @click=\"handleDownloadFile(fileDetailData.ossId, fileDetailData.originalName)\">下载</Button>\n        </div>\n      </div>\n    </Modal>\n    \n    <!-- 详情抽屉 -->\n    <Drawer\n      v-model:open=\"fragmentDetailVisible\"\n      title=\"知识片段详情\"\n      placement=\"right\"\n      :width=\"600\"\n    >\n      <div v-if=\"currentFragment\" class=\"flex flex-col h-full\">\n        <div class=\"mb-4 p-4 bg-gray-50 rounded-lg border border-gray-100 relative group\">\n          <div class=\"absolute right-2 top-2 opacity-0 group-hover:opacity-100 transition-opacity\">\n             <Button type=\"link\" size=\"small\" @click=\"handleCopy(currentFragment.content)\">\n               <template #icon><CopyOutlined /></template>\n               复制\n             </Button>\n          </div>\n          <div style=\"white-space: pre-wrap; font-size: 15px; line-height: 1.8; color: #333;\">\n            {{ currentFragment.content }}\n          </div>\n        </div>\n\n        <Descriptions title=\"元数据信息\" :column=\"1\" size=\"small\" bordered>\n          <DescriptionsItem label=\"片段 ID\">{{ currentFragment.id }}</DescriptionsItem>\n          <DescriptionsItem label=\"所属文档 ID\">{{ currentFragment.docId }}</DescriptionsItem>\n          <DescriptionsItem label=\"字符数量\">{{ currentFragment.content?.length || 0 }} 字</DescriptionsItem>\n          <DescriptionsItem v-if=\"currentSourceName\" label=\"来源文件\">{{ currentSourceName }}</DescriptionsItem>\n        </Descriptions>\n        \n        <div class=\"mt-auto pt-6 flex justify-end\">\n          <Button @click=\"fragmentDetailVisible = false\">关闭</Button>\n        </div>\n      </div>\n    </Drawer>\n  </div>\n</template>\n\n<style scoped>\n.line-clamp-3 {\n  display: -webkit-box;\n  -webkit-line-clamp: 3;\n  -webkit-box-orient: vertical;\n  overflow: hidden;\n}\n</style>\n"
  },
  {
    "path": "apps/web-antd/src/views/knowledge/info/detail/components/KnowledgeConfig.vue",
    "content": "<script setup lang=\"ts\">\nimport type { RuleObject } from 'ant-design-vue/es/form';\nimport type { InfoForm } from '#/api/knowledge/info/model';\n\nimport { ref, watch, h } from 'vue';\nimport { $t } from '@vben/locales';\nimport { cloneDeep } from '@vben/utils';\n\nimport {\n  Form,\n  FormItem,\n  Input,\n  InputNumber,\n  Select,\n  Radio,\n  RadioGroup,\n  Button,\n  Switch,\n  message,\n  Card,\n  Row,\n  Col,\n  Tooltip,\n  Tag,\n  Slider\n} from 'ant-design-vue';\nimport { QuestionCircleOutlined } from '@ant-design/icons-vue';\nimport { pick } from 'lodash-es';\n\nimport { infoAdd, infoInfo, infoUpdate } from '#/api/knowledge/info';\nimport { modelList } from '#/api/chat/model';\n\nconst props = defineProps<{\n  knowledgeId?: string | number;\n  refreshTrigger?: number;\n}>();\n\nconst emit = defineEmits<{ saved: [id: string | number] }>();\n\nconst loading = ref(false);\nconst isUpdate = ref(false);\n\nconst defaultValues: Partial<InfoForm> = {\n  id: undefined,\n  name: undefined,\n  share: undefined,\n  description: undefined,\n  separator: undefined,\n  overlapChar: undefined,\n  retrieveLimit: undefined,\n  textBlockSize: undefined,\n  vectorModel: undefined,\n  embeddingModel: undefined,\n  enableRerank: 0,\n  rerankModel: undefined,\n  enableHybrid: 0,\n  hybridAlpha: 0.5,\n  similarityThreshold: 0.5,\n  remark: undefined,\n};\n\nconst formData = ref<Partial<InfoForm>>({ ...defaultValues });\n\ntype AntdFormRules<T> = Partial<Record<keyof T, RuleObject[]>> & {\n  [key: string]: RuleObject[];\n};\n\nconst formRules = ref<AntdFormRules<InfoForm>>({\n  name: [{ required: true, message: '知识库名称不能为空' }],\n  share: [{ required: true, message: '请选择是否公开' }],\n  vectorModel: [{ required: true, message: '请选择向量库' }],\n  embeddingModel: [{ required: true, message: '请选择向量模型' }],\n  retrieveLimit: [{ required: true, message: '知识库检索条数不能为空' }],\n  textBlockSize: [{ required: true, message: '文本块大小不能为空' }],\n  overlapChar: [{ required: true, message: '重叠字符数不能为空' }],\n});\n\nconst vectorModelOptions = [\n  { label: 'Weaviate', value: 'weaviate' },\n  { label: 'Milvus', value: 'milvus' },\n  { label: 'Qdrant', value: 'qdrant' },\n];\n\nconst embeddingModelOptions = ref<Array<{ label: string; value: string }>>([]);\nconst rerankModelOptions = ref<Array<{ label: string; value: string }>>([]);\n\nconst shareOptions = [\n  { label: '是', value: 1 },\n  { label: '否', value: 0 },\n];\n\nconst { validate, validateInfos } = Form.useForm(\n  formData,\n  formRules,\n);\n\nasync function fetchEmbeddingModels() {\n  try {\n    const response = await modelList({ category: 'vector', pageSize: 1000 });\n    const models = Array.isArray(response) ? response : (response.rows || response.records || []);\n    embeddingModelOptions.value = models.map((model: any) => ({\n      label: model.modelDescribe || model.modelName,\n      value: model.modelName,\n    }));\n  } catch (error) {\n    console.error('Failed to fetch embedding models:', error);\n  }\n}\n\nasync function fetchRerankModels() {\n  try {\n    const response = await modelList({ category: 'rerank', pageSize: 1000 });\n    const models = Array.isArray(response) ? response : (response.rows || response.records || []);\n    rerankModelOptions.value = models.map((model: any) => ({\n      label: model.modelDescribe || model.modelName,\n      value: model.modelName,\n    }));\n  } catch (error) {\n    console.error('Failed to fetch rerank models:', error);\n  }\n}\n\nasync function loadData() {\n  loading.value = true;\n  try {\n    await Promise.all([fetchEmbeddingModels(), fetchRerankModels()]);\n\n    isUpdate.value = !!props.knowledgeId;\n\n    if (isUpdate.value && props.knowledgeId) {\n      const record = await infoInfo(props.knowledgeId);\n      const filterRecord = pick(record, Object.keys(defaultValues));\n      formData.value = filterRecord;\n    } else {\n      const defaultEmbeddingModel = embeddingModelOptions.value.length > 0\n        ? embeddingModelOptions.value[0].value\n        : undefined;\n\n      formData.value = {\n        ...defaultValues,\n        share: 0,\n        vectorModel: 'weaviate',\n        embeddingModel: defaultEmbeddingModel,\n        retrieveLimit: 5,\n        similarityThreshold: 0.5,\n        textBlockSize: 300,\n        overlapChar: 30,\n      };\n    }\n  } finally {\n    loading.value = false;\n  }\n}\n\nwatch(() => props.knowledgeId, () => {\n  loadData();\n}, { immediate: true });\n\nwatch(() => props.refreshTrigger, () => {\n  loadData();\n});\n\nasync function handleSubmit() {\n  try {\n    loading.value = true;\n    await validate();\n    const data = cloneDeep(formData.value);\n    \n    if (isUpdate.value) {\n      await infoUpdate(data);\n      message.success('更新成功');\n      emit('saved', data.id!);\n    } else {\n      const res = await infoAdd(data);\n      message.success('新增成功');\n      const newId = (res as any)?.id || (res as any)?.data?.id || new Date().getTime();\n      emit('saved', newId);\n    }\n  } catch (error) {\n    console.error(error);\n  } finally {\n    loading.value = false;\n  }\n}\n\n// 预设刻度样式\nconst renderMark = (label: string) => h('span', { style: { fontSize: '10px', opacity: 0.7 } }, label);\n\nconst alphaMarks = {\n  0.3: renderMark('偏向量'),\n  0.5: renderMark('平衡'),\n  0.7: renderMark('偏全文')\n};\n\nconst limitMarks = {\n  3: renderMark('精简'),\n  5: renderMark('默认'),\n  10: renderMark('丰富'),\n  20: renderMark('20')\n};\n\nconst thresholdMarks = {\n  0.2: renderMark('宽松'),\n  0.5: renderMark('标准'),\n  0.8: renderMark('严谨')\n};\n</script>\n\n<template>\n  <div class=\"px-4 py-8 max-w-4xl\">\n    <Form :model=\"formData\" :label-col=\"{ span: 4 }\" :wrapper-col=\"{ span: 18 }\">\n      <FormItem label=\"知识名称\" v-bind=\"validateInfos.name\">\n        <Input v-model:value=\"formData.name\" placeholder=\"请输入知识库名称\" />\n      </FormItem>\n\n      <FormItem label=\"是否公开\" v-bind=\"validateInfos.share\">\n        <RadioGroup v-model:value=\"formData.share\">\n          <Radio v-for=\"option in shareOptions\" :key=\"option.value\" :value=\"option.value\">\n            {{ option.label }}\n          </Radio>\n        </RadioGroup>\n      </FormItem>\n\n      <FormItem label=\"知识库描述\" v-bind=\"validateInfos.description\">\n        <Input.TextArea\n          v-model:value=\"formData.description\"\n          :rows=\"3\"\n          placeholder=\"请输入知识库描述\"\n        />\n      </FormItem>\n\n      <FormItem label=\"知识分隔符\" v-bind=\"validateInfos.separator\">\n        <Input v-model:value=\"formData.separator\" placeholder=\"知识分隔符\" />\n      </FormItem>\n\n      <FormItem label=\"文本块大小\" v-bind=\"validateInfos.textBlockSize\">\n        <InputNumber v-model:value=\"formData.textBlockSize\" :min=\"1\" class=\"w-full\" />\n      </FormItem>\n\n      <FormItem label=\"重叠字符数\" v-bind=\"validateInfos.overlapChar\">\n        <InputNumber v-model:value=\"formData.overlapChar\" :min=\"0\" class=\"w-full\" />\n      </FormItem>\n\n      <FormItem label=\"向量库\" v-bind=\"validateInfos.vectorModel\">\n        <Select\n          v-model:value=\"formData.vectorModel\"\n          :options=\"vectorModelOptions\"\n        />\n      </FormItem>\n\n      <FormItem label=\"向量模型\" v-bind=\"validateInfos.embeddingModel\">\n        <Select\n          :key=\"`embedding-${embeddingModelOptions.length}`\"\n          v-model:value=\"formData.embeddingModel\"\n          :options=\"embeddingModelOptions\"\n          placeholder=\"请选择向量模型\"\n        />\n      </FormItem>\n\n      <FormItem>\n        <template #label>\n          <div class=\"flex items-center gap-1\">\n            <span>启用重排</span>\n            <Tooltip placement=\"top\">\n              <template #title>\n                在初步检索后的结果中，使用重排模型对待选文本块与原始问题进行二次相关性精打分。这能有效提升回答的准确度。\n              </template>\n              <QuestionCircleOutlined class=\"text-gray-400 text-xs cursor-help\" />\n            </Tooltip>\n          </div>\n        </template>\n        <Switch \n          v-model:checked=\"formData.enableRerank\" \n          :un-checked-value=\"0\" \n          :checked-value=\"1\" \n        />\n        <span class=\"ml-2 text-gray-400 text-xs\">开启后将对检索结果进行精排，提升准确率</span>\n      </FormItem>\n\n      <FormItem v-if=\"formData.enableRerank\" label=\"重排模型\">\n        <Select\n          v-model:value=\"formData.rerankModel\"\n          :options=\"rerankModelOptions\"\n          placeholder=\"请选择重排模型\"\n          show-search\n        />\n      </FormItem>\n\n      <FormItem>\n        <template #label>\n          <div class=\"flex items-center gap-1\">\n            <span>混合检索</span>\n            <Tooltip placement=\"top\">\n              <template #title>\n                系统采用 RRF (Reciprocal Rank Fusion) 算法合并检索结果。该算法通过综合文本块在向量搜索和全文搜索中的“排名顺序”计算融合得分，能够给予两路同时命中的内容更高权重，显著提升搜索精准度。\n              </template>\n              <QuestionCircleOutlined class=\"text-gray-400 text-xs cursor-help\" />\n            </Tooltip>\n          </div>\n        </template>\n        <Switch \n          v-model:checked=\"formData.enableHybrid\" \n          :un-checked-value=\"0\" \n          :checked-value=\"1\" \n        />\n        <span class=\"ml-2 text-gray-400 text-xs\">融合向量搜素与关键词搜索，提升非语义匹配场景的精度</span>\n      </FormItem>\n \n      <FormItem v-if=\"formData.enableHybrid\" label=\"检索权重 (α)\">\n        <div class=\"flex flex-col w-full pr-4\">\n          <div class=\"flex justify-between items-center mb-1\">\n            <div class=\"flex items-center gap-2\">\n              <span class=\"italic text-gray-500 text-xs text-opacity-70\">vector</span>\n              <span class=\"bg-gray-100 dark:bg-zinc-800 text-primary px-1.5 py-0.5 rounded text-[10px] font-mono border border-gray-200 dark:border-gray-700 leading-none\">\n                {{ (1 - (formData.hybridAlpha || 0.5)).toFixed(2) }}\n              </span>\n            </div>\n            <div class=\"flex items-center gap-2\">\n              <span class=\"italic text-gray-500 text-xs text-opacity-70\">full-text</span>\n              <span class=\"bg-gray-100 dark:bg-zinc-800 text-primary px-1.5 py-0.5 rounded text-[10px] font-mono border border-gray-200 dark:border-gray-700 leading-none\">\n                {{ (formData.hybridAlpha || 0.5).toFixed(2) }}\n              </span>\n            </div>\n          </div>\n          <div class=\"flex items-center gap-4 pb-6\">\n            <Slider \n              v-model:value=\"formData.hybridAlpha\" \n              :min=\"0\" :max=\"1\" :step=\"0.01\" \n              :marks=\"alphaMarks\"\n              class=\"flex-1\"\n            />\n            <InputNumber v-model:value=\"formData.hybridAlpha\" :min=\"0\" :max=\"1\" :step=\"0.01\" size=\"small\" class=\"text-xs w-16\" />\n          </div>\n        </div>\n      </FormItem>\n\n      <FormItem v-bind=\"validateInfos.retrieveLimit\">\n        <template #label>\n          <div class=\"flex items-center gap-1\">\n            <span>检索条数</span>\n            <Tooltip placement=\"top\">\n              <template #title>\n                指定从知识库中检索并提供给大模型的最大文本块数量。建议配置在 3-10 条之间。\n              </template>\n              <QuestionCircleOutlined class=\"text-gray-400 text-xs cursor-help\" />\n            </Tooltip>\n          </div>\n        </template>\n        <div class=\"flex items-center gap-4 pb-6\">\n          <Slider \n            v-model:value=\"formData.retrieveLimit\" \n            :min=\"1\" :max=\"20\" \n            :marks=\"limitMarks\"\n            class=\"flex-1\"\n          />\n          <InputNumber v-model:value=\"formData.retrieveLimit\" :min=\"1\" :max=\"20\" size=\"small\" class=\"text-xs w-16\" />\n        </div>\n      </FormItem>\n\n      <FormItem v-bind=\"validateInfos.similarityThreshold\">\n        <template #label>\n          <div class=\"flex items-center gap-1\">\n            <span>相似度阈值</span>\n            <Tooltip placement=\"top\">\n              <template #title>\n                设置检索结果的最低相似度过滤分值。只有得分超过该阈值的文本块才会被返回。阈值越高，结果越精准但召回数量可能减少。推荐设置在 0.4-0.6 之间。\n              </template>\n              <QuestionCircleOutlined class=\"text-gray-400 text-xs cursor-help\" />\n            </Tooltip>\n          </div>\n        </template>\n        <div class=\"flex items-center gap-4 pb-6\">\n          <Slider \n            v-model:value=\"formData.similarityThreshold\" \n            :min=\"0\" :max=\"1\" :step=\"0.01\" \n            :marks=\"thresholdMarks\"\n            class=\"flex-1\"\n          />\n          <InputNumber v-model:value=\"formData.similarityThreshold\" :min=\"0\" :max=\"1\" :step=\"0.01\" size=\"small\" class=\"text-xs w-16\" />\n        </div>\n      </FormItem>\n\n      <FormItem label=\"备注\" v-bind=\"validateInfos.remark\">\n        <Input.TextArea\n          v-model:value=\"formData.remark\"\n          :rows=\"2\"\n          placeholder=\"备注\"\n        />\n      </FormItem>\n\n      <FormItem :wrapper-col=\"{ offset: 4, span: 18 }\">\n        <Button type=\"primary\" :loading=\"loading\" @click=\"handleSubmit\">\n          {{ isUpdate ? '保存更新' : '确认新增' }}\n        </Button>\n      </FormItem>\n    </Form>\n  </div>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/knowledge/info/detail/components/RetrievalTest.vue",
    "content": "<script setup lang=\"ts\">\nimport { ref, h, onMounted } from 'vue';\nimport {\n  Button,\n  Input,\n  Select,\n  Switch,\n  InputNumber,\n  Row,\n  Col,\n  Table,\n  Tooltip,\n  Card,\n  Form,\n  FormItem,\n  Tag,\n  Alert,\n  Slider,\n  Typography,\n  TypographyParagraph,\n  Drawer,\n  Descriptions,\n  DescriptionsItem,\n  Modal,\n} from 'ant-design-vue';\nimport { \n  SearchOutlined, \n  CopyOutlined,\n  CaretUpOutlined,\n  CaretDownOutlined,\n  LineOutlined,\n  ThunderboltFilled,\n  QuestionCircleOutlined,\n  CheckCircleOutlined\n} from '@ant-design/icons-vue';\nimport { knowledgeRetrieval, infoInfo, infoUpdate } from '#/api/knowledge/info';\nimport { modelList } from '#/api/chat/model';\nimport { message } from 'ant-design-vue';\n\nconst props = defineProps<{\n  knowledgeId?: string | number;\n}>();\n\nconst emit = defineEmits<{\n  (e: 'configUpdated'): void;\n}>();\n\nconst query = ref('');\nconst loading = ref(false);\n\nconst rerankOptions = ref<any[]>([]);\n\nconst config = ref({\n  similarityThreshold: 0.5,\n  enableHybridSearch: false,\n  hybridAlpha: 0.5,\n  enableRerank: false,\n  rerankModelName: undefined as string | undefined,\n  enableQueryRewrite: false,\n  topK: 10,\n});\n\nasync function loadRerankModels() {\n  try {\n    const res = await modelList({ category: 'rerank' });\n    rerankOptions.value = (res.rows || []).map((m: any) => ({\n      label: m.modelDescribe || m.modelName,\n      value: m.modelName\n    }));\n    if (rerankOptions.value.length > 0) {\n      config.value.rerankModelName = rerankOptions.value[0].value;\n    }\n  } catch (err) {\n    console.error('加载重排模型失败:', err);\n  }\n}\n\nonMounted(() => {\n  loadRerankModels();\n});\n\nconst renderMark = (label: string) => h('span', { style: { fontSize: '10px', opacity: 0.7 } }, label);\n\nconst thresholdMarks = {\n  0.2: renderMark('宽松'),\n  0.5: renderMark('标准'),\n  0.8: renderMark('严谨')\n};\n\nconst limitMarks = {\n  3: renderMark('精简'),\n  5: renderMark('默认'),\n  10: renderMark('丰富'),\n  20: renderMark('20')\n};\n\nconst alphaMarks = {\n  0.3: renderMark('偏向量'),\n  0.5: renderMark('平衡'),\n  0.7: renderMark('偏全文')\n};\n\nconst results = ref<any[]>([]);\n\nconst resultDetailVisible = ref(false);\nconst currentResult = ref<any>(null);\n\nfunction handleViewResultDetail(record: any) {\n  currentResult.value = record;\n  resultDetailVisible.value = true;\n}\n\nasync function handleCopy(text: string) {\n  try {\n    await navigator.clipboard.writeText(text);\n    message.success('已复制到剪贴板');\n  } catch (err) {\n    message.error('复制失败');\n  }\n}\n\nasync function handleTest() {\n  if (!query.value.trim()) {\n    message.warning('请输入检索词');\n    return;\n  }\n  if (!props.knowledgeId) {\n    message.error('知识库ID缺失');\n    return;\n  }\n\n  loading.value = true;\n  try {\n    const res = await knowledgeRetrieval({\n      knowledgeId: props.knowledgeId,\n      query: query.value,\n      topK: config.value.topK,\n      threshold: config.value.similarityThreshold,\n      enableRerank: config.value.enableRerank,\n      rerankModel: config.value.rerankModelName,\n      enableHybrid: config.value.enableHybridSearch,\n      hybridAlpha: config.value.hybridAlpha,\n    });\n    results.value = (res || []).map((item: any, index: number) => ({ \n      ...item, \n      _currentIndex: index,\n      _expanded: false \n    }));\n  } catch (error) {\n    console.error('检索测试失败:', error);\n    message.error('检索测试请求失败');\n  } finally {\n    loading.value = false;\n  }\n}\n\nasync function handleApplyConfig() {\n  if (!props.knowledgeId) {\n    message.error('知识库ID缺失');\n    return;\n  }\n\n  Modal.confirm({\n    title: '确认应用配置？',\n    content: '此操作将当前的检索测试参数（TopK、相似度阈值、混合检索、重排配置等）永久保存至该知识库的全局配置中。',\n    okText: '确定应用',\n    cancelText: '取消',\n    onOk: async () => {\n      try {\n        loading.value = true;\n        // 1. 获取最新详情以便完整更新\n        const record = await infoInfo(props.knowledgeId as string | number);\n        \n        // 2. 合并当前配置\n        const updatedData = {\n          ...record,\n          retrieveLimit: config.value.topK,\n          similarityThreshold: config.value.similarityThreshold,\n          enableHybrid: config.value.enableHybridSearch ? 1 : 0,\n          hybridAlpha: config.value.hybridAlpha,\n          enableRerank: config.value.enableRerank ? 1 : 0,\n          rerankModel: config.value.rerankModelName,\n        };\n\n        // 3. 提交更新\n        await infoUpdate(updatedData);\n        message.success('配置已成功应用至知识库');\n        emit('configUpdated');\n      } catch (error) {\n        console.error('应用配置失败:', error);\n        message.error('应用配置失败，请检查网络或后端服务');\n      } finally {\n        loading.value = false;\n      }\n    }\n  });\n}\n\nconst tableColumns = [\n  { title: '位次/变动', key: 'rank', width: 100, align: 'center' },\n  { title: '片段内容', dataIndex: 'content', key: 'content', width: '50%' },\n  { title: '得分对比', dataIndex: 'score', key: 'score', align: 'center', width: 140 },\n  { title: '来源文档', dataIndex: 'sourceName', key: 'sourceName', width: '20%' },\n];\n</script>\n\n<template>\n  <div class=\"test-container pt-2\">\n    <Row :gutter=\"16\">\n      <Col :span=\"8\">\n        <Card title=\"检索参数配置\" size=\"small\" :bordered=\"true\" class=\"mb-4\">\n          <Form layout=\"vertical\">\n            <FormItem label=\"检索词 (Query)\">\n              <Input\n                v-model:value=\"query\"\n                placeholder=\"请输入检索词\"\n                @pressEnter=\"handleTest\"\n              >\n                <template #prefix>\n                  <SearchOutlined />\n                </template>\n              </Input>\n            </FormItem>\n\n            <Row :gutter=\"16\">\n              <Col :span=\"24\">\n                <FormItem>\n                  <template #label>\n                    <div class=\"flex items-center gap-1\">\n                      <span>相似度阈值</span>\n                      <Tooltip placement=\"top\">\n                        <template #title>\n                          设置召回结果的最低相似度过滤分值。只有得分超过该阈值的文本块才会被返回。阈值越高，结果越精准但召回数量可能减少。\n                        </template>\n                        <QuestionCircleOutlined class=\"text-gray-400 text-xs cursor-help\" />\n                      </Tooltip>\n                    </div>\n                  </template>\n                  <Row>\n                    <Col :span=\"16\" class=\"pb-6\">\n                      <Slider \n                        v-model:value=\"config.similarityThreshold\" \n                        :min=\"0\" :max=\"1\" :step=\"0.01\" \n                        :marks=\"thresholdMarks\"\n                      />\n                    </Col>\n                    <Col :span=\"7\" :offset=\"1\">\n                      <InputNumber v-model:value=\"config.similarityThreshold\" :min=\"0\" :max=\"1\" :step=\"0.01\" class=\"w-full\" />\n                    </Col>\n                  </Row>\n                </FormItem>\n              </Col>\n              <Col :span=\"24\">\n                <FormItem>\n                  <template #label>\n                    <div class=\"flex items-center gap-1\">\n                      <span>返回数量 (Top K)</span>\n                      <Tooltip placement=\"top\">\n                        <template #title>\n                          指定从知识库中检索并提供给大模型的最大文本块数量。较多的数量能提供更丰富的内容，但也会增加上下文长度。\n                        </template>\n                        <QuestionCircleOutlined class=\"text-gray-400 text-xs cursor-help\" />\n                      </Tooltip>\n                    </div>\n                  </template>\n                  <Row>\n                    <Col :span=\"16\" class=\"pb-6\">\n                      <Slider \n                        v-model:value=\"config.topK\" \n                        :min=\"1\" :max=\"20\" \n                        :marks=\"limitMarks\"\n                      />\n                    </Col>\n                    <Col :span=\"7\" :offset=\"1\">\n                      <InputNumber v-model:value=\"config.topK\" :min=\"1\" :max=\"20\" size=\"small\" class=\"w-full text-xs\" />\n                    </Col>\n                  </Row>\n                </FormItem>\n              </Col>\n            </Row>\n\n            <FormItem>\n              <div class=\"flex items-center justify-between mb-2\">\n                <div class=\"flex items-center gap-1\">\n                  <span>混合检索</span>\n                  <Tooltip placement=\"top\">\n                    <template #title>\n                      结合向量检索与全文检索，通过 RRF 算法融合结果。Alpha 值决定了全文检索的权重，值越大全文检索影响越大。\n                    </template>\n                    <QuestionCircleOutlined class=\"text-gray-400 text-xs cursor-help\" />\n                  </Tooltip>\n                </div>\n                <Switch v-model:checked=\"config.enableHybridSearch\" />\n              </div>\n\n              <div v-if=\"config.enableHybridSearch\" class=\"mb-4 pl-4 border-l-2 border-primary/20\">\n                <div class=\"flex justify-between items-center mb-1\">\n                  <div class=\"flex items-center gap-2\">\n                    <span class=\"italic text-gray-500 text-xs\">vector</span>\n                    <span class=\"bg-gray-100 dark:bg-zinc-800 text-primary px-1.5 py-0.5 rounded text-[10px] font-mono border border-gray-200 dark:border-gray-700 leading-none\">\n                      {{ (1 - config.hybridAlpha).toFixed(2) }}\n                    </span>\n                  </div>\n                  <div class=\"flex items-center gap-2\">\n                    <span class=\"italic text-gray-500 text-xs\">full-text</span>\n                    <span class=\"bg-gray-100 dark:bg-zinc-800 text-primary px-1.5 py-0.5 rounded text-[10px] font-mono border border-gray-200 dark:border-gray-700 leading-none\">\n                      {{ config.hybridAlpha.toFixed(2) }}\n                    </span>\n                  </div>\n                </div>\n                <div class=\"flex items-center gap-3 pb-6\">\n                  <Slider \n                    v-model:value=\"config.hybridAlpha\" \n                    :min=\"0\" :max=\"1\" :step=\"0.01\"\n                    :marks=\"alphaMarks\"\n                    class=\"flex-1\"\n                  />\n                </div>\n              </div>\n\n              <div class=\"flex items-center justify-between mb-2 opacity-50 cursor-not-allowed\">\n                <div class=\"flex items-center gap-1\">\n                  <span>查询改写</span>\n                  <Tooltip placement=\"top\">\n                    <template #title>\n                      利用大模型对用户原始问题进行扩充或归一化，解决描述不清晰或语义偏移的问题，提升检索匹配率。\n                    </template>\n                    <QuestionCircleOutlined class=\"text-gray-400 text-xs cursor-help\" />\n                  </Tooltip>\n                </div>\n                <Switch :checked=\"false\" disabled />\n              </div>\n              <div class=\"flex items-center justify-between\">\n                <div class=\"flex items-center gap-1\">\n                  <span>启用重排</span>\n                  <Tooltip placement=\"top\">\n                    <template #title>\n                      在检索召回后的结果中，使用重排模型对待选文本块与原始问题进行二次相关性精打分。这能有效提升排序质量。\n                    </template>\n                    <QuestionCircleOutlined class=\"text-gray-400 text-xs cursor-help\" />\n                  </Tooltip>\n                </div>\n                <Switch v-model:checked=\"config.enableRerank\" />\n              </div>\n            </FormItem>\n\n            <FormItem label=\"重排序模型\" v-if=\"config.enableRerank\">\n              <Select\n                v-model:value=\"config.rerankModelName\"\n                :options=\"rerankOptions\"\n                class=\"w-full\"\n              />\n            </FormItem>\n\n            <Button \n              type=\"primary\" \n              class=\"w-full mt-2\"\n              :loading=\"loading\" \n              @click=\"handleTest\"\n            >\n              <template #icon><SearchOutlined /></template>\n              开始检索测试\n            </Button>\n\n            <Button \n              class=\"w-full mt-2 border-primary text-primary hover:bg-primary/5\"\n              :loading=\"loading\" \n              @click=\"handleApplyConfig\"\n            >\n              <template #icon><CheckCircleOutlined /></template>\n              应用至知识库配置\n            </Button>\n          </Form>\n        </Card>\n      </Col>\n\n      <!-- 右侧：结果展示 -->\n      <Col :span=\"16\">\n        <Card title=\"检索结果\" size=\"small\" :bordered=\"true\" class=\"h-full\">\n          <template #extra v-if=\"results.length\">\n            <Tag color=\"blue\">命中 {{ results.length }} 项</Tag>\n          </template>\n\n          <Alert\n            v-if=\"!results.length && !loading\"\n            message=\"请在左侧调整参数并点击开始检索以获取结果。\"\n            type=\"info\"\n            show-icon\n            class=\"mb-4\"\n          />\n\n          <Table\n            v-if=\"results.length || loading\"\n            :columns=\"tableColumns\"\n            :data-source=\"results\"\n            :loading=\"loading\"\n            :pagination=\"false\"\n            size=\"middle\"\n            rowKey=\"id\"\n            bordered\n          >\n            <template #bodyCell=\"{ column, record }\">\n              <template v-if=\"column.key === 'rank'\">\n                <div class=\"flex flex-col items-center justify-center\">\n                  <span class=\"text-base font-mono font-bold\">{{ record._currentIndex + 1 }}</span>\n                  <!-- 排名变动显示 -->\n                  <div v-if=\"record.originalIndex !== undefined && record.originalIndex !== null\" class=\"text-xs mt-0.5\">\n                    <template v-if=\"record.originalIndex > record._currentIndex\">\n                      <span class=\"text-green-500 flex items-center\">\n                        <CaretUpOutlined /> {{ record.originalIndex - record._currentIndex }}\n                      </span>\n                    </template>\n                    <template v-else-if=\"record.originalIndex < record._currentIndex\">\n                      <span class=\"text-orange-500 flex items-center\">\n                        <CaretDownOutlined /> {{ record._currentIndex - record.originalIndex }}\n                      </span>\n                    </template>\n                    <template v-else>\n                      <span class=\"text-gray-300\"><LineOutlined /></span>\n                    </template>\n                  </div>\n                </div>\n              </template>\n\n              <template v-else-if=\"column.key === 'content'\">\n                <div \n                  class=\"cursor-pointer hover:text-blue-600 transition-colors\"\n                  @click=\"handleViewResultDetail(record)\"\n                >\n                  <div class=\"line-clamp-3\" style=\"white-space: pre-wrap; font-size: 14px; line-height: 1.6;\">\n                    <Tag v-if=\"record.originalIndex > 5 && record._currentIndex < 5\" color=\"purple\" size=\"small\" class=\"mr-1\">\n                      <ThunderboltFilled /> 捞起\n                    </Tag>\n                    {{ record.content }}\n                  </div>\n                </div>\n              </template>\n              \n              <template v-else-if=\"column.key === 'sourceName'\">\n                <Tag color=\"cyan\">{{ record.sourceName }}</Tag>\n              </template>\n\n              <template v-else-if=\"column.key === 'score'\">\n                <div class=\"flex flex-col items-center\">\n                  <!-- 主得分 -->\n                  <div class=\"flex items-center gap-1\">\n                    <span class=\"text-sm font-bold\" :class=\"record.score > 0.7 ? 'text-green-600' : (record.score > 0.4 ? 'text-orange-500' : 'text-red-500')\">\n                      {{ (record.score * 100).toFixed(1) }}%\n                    </span>\n                  </div>\n\n                  <!-- 原始得分对比 -->\n                  <Tooltip v-if=\"record.rawScore !== undefined && record.rawScore !== null\" placement=\"bottom\">\n                    <template #title>\n                      原始向量分: {{ (record.rawScore * 100).toFixed(2) }}%\n                    </template>\n                    <div class=\"flex items-center gap-1 mt-0.5\">\n                      <span class=\"text-[10px] text-gray-400 opacity-80\">原: {{ (record.rawScore * 100).toFixed(1) }}%</span>\n                      <!-- 分数增减百分比 -->\n                      <span \n                        v-if=\"record.score !== record.rawScore\"\n                        :class=\"record.score > record.rawScore ? 'text-green-500' : 'text-red-400'\"\n                        class=\"text-[10px] font-bold\"\n                      >\n                        {{ record.score > record.rawScore ? '+' : '' }}{{ ((record.score - record.rawScore) * 100).toFixed(1) }}%\n                      </span>\n                    </div>\n                  </Tooltip>\n                </div>\n              </template>\n            </template>\n          </Table>\n        </Card>\n      </Col>\n    </Row>\n\n    <!-- 侧边详情抽屉 -->\n    <Drawer\n      v-model:open=\"resultDetailVisible\"\n      title=\"检索结果详情\"\n      placement=\"right\"\n      :width=\"600\"\n    >\n      <div v-if=\"currentResult\" class=\"flex flex-col h-full\">\n        <div class=\"mb-4 p-4 bg-gray-50 rounded-lg border border-gray-100 relative group\">\n          <div class=\"absolute right-2 top-2 opacity-0 group-hover:opacity-100 transition-opacity\">\n             <Button type=\"link\" size=\"small\" @click=\"handleCopy(currentResult.content)\">\n               <template #icon><CopyOutlined /></template>\n               复制\n             </Button>\n          </div>\n          <div style=\"white-space: pre-wrap; font-size: 15px; line-height: 1.8; color: #333;\">\n            {{ currentResult.content }}\n          </div>\n        </div>\n\n        <Descriptions title=\"匹配信息\" :column=\"1\" size=\"small\" bordered>\n          <DescriptionsItem label=\"相关性得分\">\n            <Tag :color=\"currentResult.score > 0.7 ? 'green' : (currentResult.score > 0.4 ? 'orange' : 'red')\">\n               {{ (currentResult.score * 100).toFixed(2) }}%\n            </Tag>\n          </DescriptionsItem>\n          <DescriptionsItem label=\"来源文档\">{{ currentResult.sourceName }}</DescriptionsItem>\n          <DescriptionsItem label=\"字符数量\">{{ currentResult.content?.length || 0 }} 字</DescriptionsItem>\n        </Descriptions>\n        \n        <div class=\"mt-auto pt-6 flex justify-end\">\n          <Button @click=\"resultDetailVisible = false\">关闭</Button>\n        </div>\n      </div>\n    </Drawer>\n  </div>\n</template>\n\n<style scoped>\n.test-container {\n  height: 100%;\n}\n.line-clamp-3 {\n  display: -webkit-box;\n  -webkit-line-clamp: 3;\n  -webkit-box-orient: vertical;\n  overflow: hidden;\n}\n</style>\n"
  },
  {
    "path": "apps/web-antd/src/views/knowledge/info/detail/index.vue",
    "content": "<template>\n  <Page title=\"知识库详情\" class=\"bg-surface\">\n    <template #extra>\n      <Button @click=\"handleBack\">返回</Button>\n    </template>\n    \n    <div class=\"p-4\">\n      <Tabs v-model:activeKey=\"activeKey\" class=\"w-full bg-white px-4 pt-2 pb-4 rounded shadow-sm\">\n        <TabPane key=\"file\" tab=\"文件管理\" v-if=\"!isNew\">\n          <FileManagement :knowledge-id=\"knowledgeId\" />\n        </TabPane>\n        <TabPane key=\"test\" tab=\"检索测试\" v-if=\"!isNew\">\n          <RetrievalTest :knowledge-id=\"knowledgeId\" @config-updated=\"handleConfigUpdated\" />\n        </TabPane>\n        <TabPane key=\"config\" tab=\"知识库配置\">\n          <KnowledgeConfig \n            :knowledge-id=\"isNew ? undefined : knowledgeId\" \n            :refresh-trigger=\"refreshTrigger\"\n            @saved=\"handleConfigSaved\" \n          />\n        </TabPane>\n      </Tabs>\n    </div>\n  </Page>\n</template>\n\n<script setup lang=\"ts\">\nimport { ref } from 'vue';\nimport { useRoute, useRouter } from 'vue-router';\nimport { Page } from '@vben/common-ui';\nimport { Tabs, TabPane, Button } from 'ant-design-vue';\n\nimport FileManagement from './components/FileManagement.vue';\nimport RetrievalTest from './components/RetrievalTest.vue';\nimport KnowledgeConfig from './components/KnowledgeConfig.vue';\n\nconst route = useRoute();\nconst router = useRouter();\n\nconst knowledgeIdStr = route.params.id as string;\nconst isNew = knowledgeIdStr === 'new';\nconst knowledgeId = isNew ? undefined : knowledgeIdStr;\n\nconst activeKey = ref(isNew ? 'config' : 'file');\nconst refreshTrigger = ref(0);\n\nfunction handleConfigUpdated() {\n  refreshTrigger.value += 1;\n}\n\nfunction handleBack() {\n  router.back();\n}\n\nfunction handleConfigSaved(newId: string | number) {\n  if (isNew) {\n    router.replace(`/knowledge/info/detail/${newId}`);\n  }\n}\n</script>\n"
  },
  {
    "path": "apps/web-antd/src/views/knowledge/info/index.vue",
    "content": "<script setup lang=\"ts\">\nimport type { VbenFormProps } from '@vben/common-ui';\n\nimport type { VxeGridProps } from '#/adapter/vxe-table';\nimport type { InfoForm } from '#/api/knowledge/info/model';\n\nimport { ref } from 'vue';\nimport { Page } from '@vben/common-ui';\nimport { getVxePopupContainer } from '@vben/utils';\n\nimport { Modal, Popconfirm, Space } from 'ant-design-vue';\n\nimport { useVbenModal } from '@vben/common-ui';\nimport { useVbenVxeGrid, vxeCheckboxChecked } from '#/adapter/vxe-table';\nimport { infoExport, infoList, infoRemove } from '#/api/knowledge/info';\nimport { commonDownloadExcel } from '#/utils/file/download';\n\nimport KnowledgeAddModal from './components/KnowledgeAddModal.vue';\nimport { columns, querySchema } from './data';\nimport { useRouter } from 'vue-router';\n\nconst formOptions: VbenFormProps = {\n  commonConfig: {\n    labelWidth: 80,\n    componentProps: {\n      allowClear: true,\n    },\n  },\n  schema: querySchema(),\n  wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4',\n  // 处理区间选择器 RangePicker 时间格式映射\n  // 将一个时间区间字段映射为两个独立的开始/结束时间字段，用于搜索和导出\n  // 示例: 将 createTime 字段映射为 params[beginTime] 和 params[endTime]\n  // fieldMappingTime: [\n  //   [\n  //     'createTime', // 表单中的字段名\n  //     ['params[beginTime]', 'params[endTime]'], // 映射后的字段名\n  //     ['YYYY-MM-DD 00:00:00', 'YYYY-MM-DD 23:59:59'], // 时间格式\n  //   ],\n  // ],\n};\n\nconst gridOptions: VxeGridProps = {\n  checkboxConfig: {\n    // 高亮\n    highlight: true,\n    // 翻页时保留选中状态\n    reserve: true,\n    // 点击行选中\n    // trigger: 'row',\n  },\n  // 需要使用i18n注意这里要改成getter形式 否则切换语言不会刷新\n  // columns: columns(),\n  columns,\n  height: 'auto',\n  keepSource: true,\n  pagerConfig: {},\n  proxyConfig: {\n    ajax: {\n      query: async ({ page }, formValues = {}) => {\n        return await infoList({\n          pageNum: page.currentPage,\n          pageSize: page.pageSize,\n          ...formValues,\n        });\n      },\n    },\n  },\n  rowConfig: {\n    keyField: 'id',\n  },\n  // 表格全局唯一标识，用于保存列配置\n  id: 'system-info-index',\n};\n\nconst [BasicTable, tableApi] = useVbenVxeGrid({\n  formOptions,\n  gridOptions,\n});\n\nconst router = useRouter();\n\nconst [AddModal, modalApi] = useVbenModal({\n  connectedComponent: KnowledgeAddModal,\n});\n\nfunction handleAdd() {\n  modalApi.open();\n}\n\nfunction handleDetail(row: Required<InfoForm>) {\n  router.push(`/knowledge/info/detail/${row.id}`);\n}\n\nasync function handleDelete(row: Required<InfoForm>) {\n  await infoRemove(row.id);\n  await tableApi.query();\n}\n\nfunction handleMultiDelete() {\n  const rows = tableApi.grid.getCheckboxRecords();\n  const ids = rows.map((row: Required<InfoForm>) => row.id);\n  Modal.confirm({\n    title: '提示',\n    okType: 'danger',\n    content: `确认删除选中的${ids.length}条记录吗？`,\n    onOk: async () => {\n      await infoRemove(ids);\n      await tableApi.query();\n    },\n  });\n}\n\nfunction handleDownloadExcel() {\n  commonDownloadExcel(infoExport, '知识库数据', tableApi.formApi.form.values, {\n    fieldMappingTime: formOptions.fieldMappingTime,\n  });\n}\n</script>\n\n<template>\n  <Page :auto-content-height=\"true\">\n    <BasicTable table-title=\"知识库列表\">\n      <template #toolbar-tools>\n        <Space>\n          <a-button\n            v-access:code=\"['system:info:export']\"\n            @click=\"handleDownloadExcel\"\n          >\n            {{ $t('pages.common.export') }}\n          </a-button>\n          <a-button\n            :disabled=\"!vxeCheckboxChecked(tableApi)\"\n            danger\n            type=\"primary\"\n            v-access:code=\"['system:info:remove']\"\n            @click=\"handleMultiDelete\"\n          >\n            {{ $t('pages.common.delete') }}\n          </a-button>\n          <a-button\n            type=\"primary\"\n            v-access:code=\"['system:info:add']\"\n            @click=\"handleAdd\"\n          >\n            {{ $t('pages.common.add') }}\n          </a-button>\n        </Space>\n      </template>\n      <template #action=\"{ row }\">\n        <Space>\n          <ghost-button\n            @click.stop=\"handleDetail(row)\"\n          >\n            详情\n          </ghost-button>\n          <Popconfirm\n            :get-popup-container=\"getVxePopupContainer\"\n            placement=\"left\"\n            title=\"确认删除？\"\n            @confirm=\"handleDelete(row)\"\n          >\n            <ghost-button\n              danger\n              v-access:code=\"['system:info:remove']\"\n              @click.stop=\"\"\n            >\n              {{ $t('pages.common.delete') }}\n            </ghost-button>\n          </Popconfirm>\n        </Space>\n      </template>\n    </BasicTable>\n    <AddModal @reload=\"tableApi.query()\" />\n  </Page>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/knowledge/info/info-drawer.vue",
    "content": "<script setup lang=\"ts\">\nimport type { RuleObject } from 'ant-design-vue/es/form';\nimport type { InfoForm } from '#/api/knowledge/info/model';\n\nimport { computed, ref, watch } from 'vue';\nimport { $t } from '@vben/locales';\nimport { cloneDeep } from '@vben/utils';\n\nimport {\n  Drawer,\n  Form,\n  FormItem,\n  Input,\n  InputNumber,\n  Select,\n  Radio,\n  RadioGroup,\n  Button,\n  Space,\n  message,\n  Switch,\n  Slider,\n} from 'ant-design-vue';\nimport { pick } from 'lodash-es';\n\nimport { infoAdd, infoInfo, infoUpdate } from '#/api/knowledge/info';\nimport { embeddingModelList, rerankModelList } from '#/api/chat/model';\n\nconst emit = defineEmits<{ reload: [] }>();\n\nconst visible = ref(false);\nconst loading = ref(false);\nconst isUpdate = ref(false);\n\nconst title = computed(() => {\n  return isUpdate.value ? $t('pages.common.edit') : $t('pages.common.add');\n});\n\nconst defaultValues: Partial<InfoForm> = {\n  id: undefined,\n  name: undefined,\n  share: undefined,\n  description: undefined,\n  separator: undefined,\n  overlapChar: undefined,\n  retrieveLimit: undefined,\n  textBlockSize: undefined,\n  vectorModel: undefined,\n  embeddingModel: undefined,\n  enableRerank: undefined,\n  rerankModel: undefined,\n  rerankTopN: undefined,\n  rerankScoreThreshold: undefined,\n  enableHybrid: undefined,\n  hybridAlpha: undefined,\n  remark: undefined,\n};\n\nconst formData = ref<Partial<InfoForm>>({ ...defaultValues });\n\ntype AntdFormRules<T> = Partial<Record<keyof T, RuleObject[]>> & {\n  [key: string]: RuleObject[];\n};\n\nconst formRules = ref<AntdFormRules<InfoForm>>({\n  name: [{ required: true, message: '知识库名称不能为空' }],\n  share: [{ required: true, message: '请选择是否公开' }],\n  vectorModel: [{ required: true, message: '请选择向量库' }],\n  embeddingModel: [{ required: true, message: '请选择向量模型' }],\n  retrieveLimit: [{ required: true, message: '知识库检索条数不能为空' }],\n  textBlockSize: [{ required: true, message: '文本块大小不能为空' }],\n  overlapChar: [{ required: true, message: '重叠字符数不能为空' }],\n  rerankModel: [{ required: true, message: '请选择重排序模型' }],\n  rerankTopN: [{ required: true, message: '重排序返回数量不能为空' }],\n  rerankScoreThreshold: [{ required: true, message: '分数阈值不能为空' }],\n});\n\nconst vectorModelOptions = [\n  { label: 'weaviate', value: 'weaviate' },\n  { label: 'milvus', value: 'milvus' },\n];\n\nconst embeddingModelOptions = ref<Array<{ label: string; value: string }>>([]);\n\nconst rerankModelOptions = ref<Array<{ label: string; value: string }>>([]);\n\nconst shareOptions = [\n  { label: '是', value: 1 },\n  { label: '否', value: 0 },\n];\n\nconst { validate, validateInfos, resetFields } = Form.useForm(\n  formData,\n  formRules,\n);\n\nasync function fetchEmbeddingModels() {\n  try {\n    const response = await embeddingModelList();\n    const models = Array.isArray(response) ? response : (response.rows || response.records || []);\n    embeddingModelOptions.value = models.map((model: any) => ({\n      label: model.modelDescribe,\n      value: model.modelName,\n    }));\n  } catch (error) {\n    console.error('Failed to fetch embedding models:', error);\n  }\n}\n\nasync function fetchRerankModels() {\n  try {\n    const response = await rerankModelList();\n    const models = Array.isArray(response) ? response : (response.rows || response.records || []);\n    rerankModelOptions.value = models.map((model: any) => ({\n      label: model.modelDescribe,\n      value: model.modelName,\n    }));\n  } catch (error) {\n    console.error('Failed to fetch rerank models:', error);\n  }\n}\n\n// 监听检索条数变化，确保重排序返回数量不超过检索条数\nwatch(() => formData.value.retrieveLimit, (newVal) => {\n  if (formData.value.rerankTopN && newVal && formData.value.rerankTopN > newVal) {\n    formData.value.rerankTopN = newVal;\n  }\n});\n\n// 监听启用重排序变化\nwatch(() => formData.value.enableRerank, (newVal) => {\n  if (newVal === 1) {\n    // 启用重排序时，设置默认值\n    if (!formData.value.rerankModel && rerankModelOptions.value.length > 0) {\n      formData.value.rerankModel = rerankModelOptions.value[0].value;\n    }\n    if (!formData.value.rerankTopN) {\n      formData.value.rerankTopN = Math.min(5, formData.value.retrieveLimit || 5);\n    }\n    if (formData.value.rerankScoreThreshold === undefined) {\n      formData.value.rerankScoreThreshold = 0.5;\n    }\n  }\n});\n\nasync function handleOpen(id?: string | number) {\n  loading.value = true;\n  try {\n    await Promise.all([fetchEmbeddingModels(), fetchRerankModels()]);\n\n    isUpdate.value = !!id;\n\n    if (isUpdate.value && id) {\n      const record = await infoInfo(id);\n      const filterRecord = pick(record, Object.keys(defaultValues));\n      formData.value = filterRecord;\n    } else {\n      const defaultEmbeddingModel = embeddingModelOptions.value.length > 0\n        ? embeddingModelOptions.value[0].value\n        : undefined;\n      const defaultRerankModel = rerankModelOptions.value.length > 0\n        ? rerankModelOptions.value[0].value\n        : undefined;\n\n      formData.value = {\n        ...defaultValues,\n        share: 0,\n        vectorModel: 'weaviate',\n        embeddingModel: defaultEmbeddingModel,\n        retrieveLimit: 5,\n        textBlockSize: 300,\n        overlapChar: 30,\n        enableRerank: 0,\n        rerankModel: defaultRerankModel,\n        rerankTopN: 5,\n        rerankScoreThreshold: 0.5,\n      };\n    }\n\n    visible.value = true;\n  } finally {\n    loading.value = false;\n  }\n}\n\nasync function handleSubmit() {\n  try {\n    loading.value = true;\n    await validate();\n    const data = cloneDeep(formData.value);\n    await (isUpdate.value ? infoUpdate(data) : infoAdd(data));\n    //message.success(isUpdate.value ? '修改成功' : '新增成功');\n    emit('reload');\n    handleClose();\n  } catch (error) {\n    console.error(error);\n  } finally {\n    loading.value = false;\n  }\n}\n\nfunction handleClose() {\n  visible.value = false;\n  formData.value = cloneDeep(defaultValues);\n  resetFields();\n}\n\ndefineExpose({\n  open: handleOpen,\n});\n</script>\n\n<template>\n  <Drawer\n    :title=\"title\"\n    :open=\"visible\"\n    @close=\"handleClose\"\n    :width=\"600\"\n    :body-style=\"{ paddingBottom: '80px' }\"\n  >\n    <Form :model=\"formData\" :label-col=\"{ span: 6 }\">\n      <FormItem label=\"知识名称\" v-bind=\"validateInfos.name\">\n        <Input\n          v-model:value=\"formData.name\"\n          :placeholder=\"$t('ui.formRules.required')\"\n        />\n      </FormItem>\n      <FormItem label=\"是否公开\" v-bind=\"validateInfos.share\">\n        <RadioGroup v-model:value=\"formData.share\">\n          <Radio v-for=\"option in shareOptions\" :key=\"option.value\" :value=\"option.value\">\n            {{ option.label }}\n          </Radio>\n        </RadioGroup>\n      </FormItem>\n      <FormItem label=\"知识库描述\" v-bind=\"validateInfos.description\">\n        <Input.TextArea\n          v-model:value=\"formData.description\"\n          :placeholder=\"$t('ui.formRules.required')\"\n          :rows=\"3\"\n        />\n      </FormItem>\n      <FormItem label=\"知识分隔符\" v-bind=\"validateInfos.separator\">\n        <Input\n          v-model:value=\"formData.separator\"\n          :placeholder=\"$t('ui.formRules.required')\"\n        />\n      </FormItem>\n      <FormItem label=\"重叠字符\" v-bind=\"validateInfos.overlapChar\">\n        <InputNumber\n          v-model:value=\"formData.overlapChar\"\n          style=\"width: 100%\"\n          :placeholder=\"$t('ui.formRules.required')\"\n          :min=\"0\"\n        />\n      </FormItem>\n      <FormItem label=\"检索条数\" v-bind=\"validateInfos.retrieveLimit\">\n        <InputNumber\n          v-model:value=\"formData.retrieveLimit\"\n          style=\"width: 100%\"\n          :placeholder=\"$t('ui.formRules.required')\"\n          :min=\"1\"\n        />\n      </FormItem>\n      <FormItem label=\"文本块大小\" v-bind=\"validateInfos.textBlockSize\">\n        <InputNumber\n          v-model:value=\"formData.textBlockSize\"\n          style=\"width: 100%\"\n          :placeholder=\"$t('ui.formRules.required')\"\n          :min=\"1\"\n        />\n      </FormItem>\n      <FormItem label=\"向量库\" v-bind=\"validateInfos.vectorModel\">\n        <Select\n          v-model:value=\"formData.vectorModel\"\n          :options=\"vectorModelOptions\"\n          :placeholder=\"$t('ui.formRules.required')\"\n        />\n      </FormItem>\n      <FormItem label=\"向量模型\" v-bind=\"validateInfos.embeddingModel\">\n        <Select\n          :key=\"`embedding-${embeddingModelOptions.length}`\"\n          v-model:value=\"formData.embeddingModel\"\n          :options=\"embeddingModelOptions\"\n          :placeholder=\"$t('ui.formRules.required')\"\n        />\n      </FormItem>\n      <FormItem label=\"启用混合检索\">\n        <Switch\n          v-model:checked=\"formData.enableHybrid\"\n          :checked-value=\"1\"\n          :un-checked-value=\"0\"\n        />\n      </FormItem>\n      <FormItem v-if=\"formData.enableHybrid === 1\" label=\"混合检索权重\">\n        <div class=\"flex items-center gap-3\">\n          <Slider\n            v-model:value=\"formData.hybridAlpha\"\n            :min=\"0\"\n            :max=\"1\"\n            :step=\"0.05\"\n            style=\"flex: 1\"\n          />\n          <span class=\"w-12 text-right\">{{\n            (formData.hybridAlpha || 0.5).toFixed(2)\n          }}</span>\n        </div>\n      </FormItem>\n      <FormItem label=\"启用重排序\">\n        <Switch\n          v-model:checked=\"formData.enableRerank\"\n          :checked-value=\"1\"\n          :un-checked-value=\"0\"\n        />\n      </FormItem>\n      <template v-if=\"formData.enableRerank === 1\">\n        <FormItem label=\"重排序模型\" v-bind=\"validateInfos.rerankModel\">\n          <Select\n            :key=\"`rerank-${rerankModelOptions.length}`\"\n            v-model:value=\"formData.rerankModel\"\n            :options=\"rerankModelOptions\"\n            :placeholder=\"$t('ui.formRules.required')\"\n          />\n        </FormItem>\n        <FormItem label=\"重排序数量\" v-bind=\"validateInfos.rerankTopN\">\n          <InputNumber\n            v-model:value=\"formData.rerankTopN\"\n            style=\"width: 100%\"\n            :placeholder=\"$t('ui.formRules.required')\"\n            :min=\"1\"\n            :max=\"formData.retrieveLimit || 100\"\n          />\n          <div v-if=\"formData.retrieveLimit\" class=\"text-gray-400 text-xs mt-1\">\n            不能超过检索条数 ({{ formData.retrieveLimit }})\n          </div>\n        </FormItem>\n        <FormItem label=\"分数阈值\" v-bind=\"validateInfos.rerankScoreThreshold\">\n          <div class=\"flex items-center gap-3\">\n            <Slider\n              v-model:value=\"formData.rerankScoreThreshold\"\n              :min=\"0\"\n              :max=\"1\"\n              :step=\"0.01\"\n              style=\"flex: 1\"\n            />\n            <span class=\"w-12 text-right\">{{ (formData.rerankScoreThreshold || 0).toFixed(2) }}</span>\n          </div>\n        </FormItem>\n      </template>\n      <FormItem label=\"备注\" v-bind=\"validateInfos.remark\">\n        <Input.TextArea\n          v-model:value=\"formData.remark\"\n          :placeholder=\"$t('ui.formRules.required')\"\n          :rows=\"3\"\n        />\n      </FormItem>\n    </Form>\n\n    <template #footer>\n      <Space style=\"float: right\">\n        <Button @click=\"handleClose\">取消</Button>\n        <Button type=\"primary\" :loading=\"loading\" @click=\"handleSubmit\">\n          {{ isUpdate ? '更新' : '新增' }}\n        </Button>\n      </Space>\n    </template>\n  </Drawer>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/knowledge/info/info-modal.vue",
    "content": "<!--\n使用 Ant Design Vue 原生 Form 组件生成表单\n详细用法参考: https://antdv.com/components/form-cn\n注意: 如果 VSCode 配置了自动移除未使用的导入，可能会误删某些组件导入\n-->\n<script setup lang=\"ts\">\nimport type { RuleObject } from 'ant-design-vue/es/form';\n\nimport type { InfoForm } from '#/api/system/info/model';\n\nimport { computed, ref, watch } from 'vue';\n\nimport { useVbenModal } from '@vben/common-ui';\nimport { $t } from '@vben/locales';\nimport { cloneDeep } from '@vben/utils';\nimport { useAccessStore } from '@vben/stores';\n\nimport {\n  Form,\n  FormItem,\n  Input,\n  Textarea,\n  InputNumber,\n  Select,\n  Radio,\n  RadioGroup,\n  Switch,\n  Slider,\n} from 'ant-design-vue';\nimport { pick } from 'lodash-es';\n\nimport { infoAdd, infoInfo, infoUpdate } from '#/api/knowledge/info';\nimport { embeddingModelList, rerankModelList } from '#/api/chat/model';\n\nconst accessStore = useAccessStore();\n\nconst emit = defineEmits<{ reload: [] }>();\n\nconst isUpdate = ref(false);\nconst title = computed(() => {\n  return isUpdate.value ? $t('pages.common.edit') : $t('pages.common.add');\n});\n\n/**\n * 定义默认值 用于reset\n */\nconst defaultValues: Partial<InfoForm> = {\n  id: undefined,\n  name: undefined,\n  share: undefined,\n  description: undefined,\n  separator: undefined,\n  overlapChar: undefined,\n  retrieveLimit: undefined,\n  textBlockSize: undefined,\n  vectorModel: undefined,\n  embeddingModel: undefined,\n  enableRerank: undefined,\n  rerankModel: undefined,\n  rerankTopN: undefined,\n  rerankScoreThreshold: undefined,\n  remark: undefined,\n};\n\n/**\n * 表单数据ref\n */\nconst formData = ref<Partial<InfoForm>>({ ...defaultValues });\n\ntype AntdFormRules<T> = Partial<Record<keyof T, RuleObject[]>> & {\n  [key: string]: RuleObject[];\n};\n/**\n * 表单校验规则\n */\nconst formRules = ref<AntdFormRules<InfoForm>>({\n  name: [{ required: true, message: '知识库名称不能为空' }],\n  share: [{ required: true, message: '请选择是否公开' }],\n  vectorModel: [{ required: true, message: '请选择向量库' }],\n  embeddingModel: [{ required: true, message: '请选择向量模型' }],\n  retrieveLimit: [{ required: true, message: '知识库检索条数不能为空' }],\n  textBlockSize: [{ required: true, message: '文本块大小不能为空' }],\n  overlapChar: [{ required: true, message: '重叠字符数不能为空' }],\n  rerankModel: [{ required: true, message: '请选择重排序模型' }],\n  rerankTopN: [{ required: true, message: '重排序返回数量不能为空' }],\n  rerankScoreThreshold: [{ required: true, message: '分数阈值不能为空' }],\n});\n\nconst vectorModelOptions = [\n  { label: 'weaviate', value: 'weaviate' },\n  { label: 'milvus', value: 'milvus' },\n];\n\nconst embeddingModelOptions = ref<Array<{ label: string; value: string }>>([]);\n\nconst rerankModelOptions = ref<Array<{ label: string; value: string }>>([]);\n\nconst shareOptions = [\n  { label: '是', value: 1 },\n  { label: '否', value: 0 },\n];\n\n/**\n * useForm解构出表单方法\n */\nconst { validate, validateInfos, resetFields } = Form.useForm(\n  formData,\n  formRules,\n);\n\nasync function fetchEmbeddingModels() {\n  try {\n    const response = await embeddingModelList();\n    const models = Array.isArray(response) ? response : (response.rows || response.records || []);\n    embeddingModelOptions.value = models.map((model: any) => ({\n      label: model.modelDescribe,\n      value: model.modelName,\n    }));\n  } catch (error) {\n    console.error('Failed to fetch embedding models:', error);\n  }\n}\n\nasync function fetchRerankModels() {\n  try {\n    const response = await rerankModelList();\n    const models = Array.isArray(response) ? response : (response.rows || response.records || []);\n    rerankModelOptions.value = models.map((model: any) => ({\n      label: model.modelDescribe,\n      value: model.modelName,\n    }));\n  } catch (error) {\n    console.error('Failed to fetch rerank models:', error);\n  }\n}\n\n// 监听检索条数变化，确保重排序返回数量不超过检索条数\nwatch(() => formData.value.retrieveLimit, (newVal) => {\n  if (formData.value.rerankTopN && newVal && formData.value.rerankTopN > newVal) {\n    formData.value.rerankTopN = newVal;\n  }\n});\n\n// 监听启用重排序变化\nwatch(() => formData.value.enableRerank, (newVal) => {\n  if (newVal === 1) {\n    // 启用重排序时，设置默认值\n    if (!formData.value.rerankModel && rerankModelOptions.value.length > 0) {\n      formData.value.rerankModel = rerankModelOptions.value[0].value;\n    }\n    if (!formData.value.rerankTopN) {\n      formData.value.rerankTopN = Math.min(5, formData.value.retrieveLimit || 5);\n    }\n    if (formData.value.rerankScoreThreshold === undefined) {\n      formData.value.rerankScoreThreshold = 0.5;\n    }\n  }\n});\n\nconst [BasicModal, modalApi] = useVbenModal({\n  class: 'w-[550px]',\n  fullscreenButton: false,\n  closeOnClickModal: false,\n  onClosed: handleCancel,\n  onConfirm: handleConfirm,\n  onOpenChange: async (isOpen) => {\n    if (!isOpen) {\n      return null;\n    }\n    modalApi.modalLoading(true);\n\n    await Promise.all([fetchEmbeddingModels(), fetchRerankModels()]);\n\n    const { id } = modalApi.getData() as { id?: number | string };\n    isUpdate.value = !!id;\n\n    if (isUpdate.value && id) {\n      const record = await infoInfo(id);\n      // 只赋值存在的字段\n      const filterRecord = pick(record, Object.keys(defaultValues));\n      formData.value = filterRecord;\n    } else {\n      // 设置默认值，embeddingModel 使用第一个可用的模型\n      const defaultEmbeddingModel = embeddingModelOptions.value.length > 0\n        ? embeddingModelOptions.value[0].value\n        : undefined;\n      const defaultRerankModel = rerankModelOptions.value.length > 0\n        ? rerankModelOptions.value[0].value\n        : undefined;\n\n      formData.value = {\n        ...defaultValues,\n        share: 0,\n        vectorModel: 'weaviate',\n        embeddingModel: defaultEmbeddingModel,\n        retrieveLimit: 5,\n        textBlockSize: 300,\n        overlapChar: 30,\n        enableRerank: 0,\n        rerankModel: defaultRerankModel,\n        rerankTopN: 5,\n        rerankScoreThreshold: 0.5,\n      };\n    }\n\n    modalApi.modalLoading(false);\n  },\n});\n\nasync function handleConfirm() {\n  try {\n    modalApi.modalLoading(true);\n    await validate();\n    // 可能会做数据处理 使用cloneDeep深拷贝\n    const data = cloneDeep(formData.value);\n    await (isUpdate.value ? infoUpdate(data) : infoAdd(data));\n    emit('reload');\n    await handleCancel();\n  } catch (error) {\n    console.error(error);\n  } finally {\n    modalApi.modalLoading(false);\n  }\n}\n\nasync function handleCancel() {\n  modalApi.close();\n  formData.value = cloneDeep(defaultValues);\n  resetFields();\n}\n</script>\n\n<template>\n  <BasicModal :title=\"title\">\n    <Form :model=\"formData\" :label-col=\"{ span: 4 }\">\n      <FormItem label=\"知识名称\" v-bind=\"validateInfos.name\">\n        <Input\n          v-model:value=\"formData.name\"\n          :placeholder=\"$t('ui.formRules.required')\"\n        />\n      </FormItem>\n      <FormItem label=\"是否公开\" v-bind=\"validateInfos.share\">\n        <RadioGroup v-model:value=\"formData.share\">\n          <Radio v-for=\"option in shareOptions\" :key=\"option.value\" :value=\"option.value\">\n            {{ option.label }}\n          </Radio>\n        </RadioGroup>\n      </FormItem>\n      <FormItem label=\"知识库描述\" v-bind=\"validateInfos.description\">\n        <Textarea\n          v-model:value=\"formData.description\"\n          :placeholder=\"$t('ui.formRules.required')\"\n          :rows=\"4\"\n        />\n      </FormItem>\n      <FormItem label=\"知识分隔符\" v-bind=\"validateInfos.separator\">\n        <Input\n          v-model:value=\"formData.separator\"\n          :placeholder=\"$t('ui.formRules.required')\"\n        />\n      </FormItem>\n      <FormItem label=\"重叠字符\" v-bind=\"validateInfos.overlapChar\">\n        <InputNumber\n          v-model:value=\"formData.overlapChar\"\n          style=\"width: 100%\"\n          :placeholder=\"$t('ui.formRules.required')\"\n          :min=\"0\"\n        />\n      </FormItem>\n      <FormItem label=\"检索条数\" v-bind=\"validateInfos.retrieveLimit\">\n        <InputNumber\n          v-model:value=\"formData.retrieveLimit\"\n          style=\"width: 100%\"\n          :placeholder=\"$t('ui.formRules.required')\"\n          :min=\"1\"\n        />\n      </FormItem>\n      <FormItem label=\"文本块大小\" v-bind=\"validateInfos.textBlockSize\">\n        <InputNumber\n          v-model:value=\"formData.textBlockSize\"\n          style=\"width: 100%\"\n          :placeholder=\"$t('ui.formRules.required')\"\n          :min=\"1\"\n        />\n      </FormItem>\n      <FormItem label=\"向量库\" v-bind=\"validateInfos.vectorModel\">\n        <Select\n          v-model:value=\"formData.vectorModel\"\n          :options=\"vectorModelOptions\"\n          :placeholder=\"$t('ui.formRules.required')\"\n        />\n      </FormItem>\n      <FormItem label=\"向量模型\" v-bind=\"validateInfos.embeddingModel\">\n        <Select\n          :key=\"`embedding-${embeddingModelOptions.length}`\"\n          v-model:value=\"formData.embeddingModel\"\n          :options=\"embeddingModelOptions\"\n          :placeholder=\"$t('ui.formRules.required')\"\n        />\n      </FormItem>\n      <FormItem label=\"启用重排序\">\n        <Switch\n          v-model:checked=\"formData.enableRerank\"\n          :checked-value=\"1\"\n          :un-checked-value=\"0\"\n        />\n      </FormItem>\n      <template v-if=\"formData.enableRerank === 1\">\n        <FormItem label=\"重排序模型\" v-bind=\"validateInfos.rerankModel\">\n          <Select\n            :key=\"`rerank-${rerankModelOptions.length}`\"\n            v-model:value=\"formData.rerankModel\"\n            :options=\"rerankModelOptions\"\n            :placeholder=\"$t('ui.formRules.required')\"\n          />\n        </FormItem>\n        <FormItem label=\"重排序数量\" v-bind=\"validateInfos.rerankTopN\">\n          <InputNumber\n            v-model:value=\"formData.rerankTopN\"\n            style=\"width: 100%\"\n            :placeholder=\"$t('ui.formRules.required')\"\n            :min=\"1\"\n            :max=\"formData.retrieveLimit || 100\"\n          />\n          <div v-if=\"formData.retrieveLimit\" class=\"text-gray-400 text-xs mt-1\">\n            不能超过检索条数 ({{ formData.retrieveLimit }})\n          </div>\n        </FormItem>\n        <FormItem label=\"分数阈值\" v-bind=\"validateInfos.rerankScoreThreshold\">\n          <div class=\"flex items-center gap-3\">\n            <Slider\n              v-model:value=\"formData.rerankScoreThreshold\"\n              :min=\"0\"\n              :max=\"1\"\n              :step=\"0.01\"\n              style=\"flex: 1\"\n            />\n            <span class=\"w-12 text-right\">{{ (formData.rerankScoreThreshold || 0).toFixed(2) }}</span>\n          </div>\n        </FormItem>\n      </template>\n      <FormItem label=\"备注\" v-bind=\"validateInfos.remark\">\n        <Textarea\n          v-model:value=\"formData.remark\"\n          :placeholder=\"$t('ui.formRules.required')\"\n          :rows=\"4\"\n        />\n      </FormItem>\n    </Form>\n  </BasicModal>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/mcp/market/data.tsx",
    "content": "import type { FormSchemaGetter } from '#/adapter/form';\nimport type { VxeGridProps } from '#/adapter/vxe-table';\n\nexport const querySchema: FormSchemaGetter = () => [\n  {\n    component: 'Input',\n    fieldName: 'name',\n    label: '市场名称',\n  },\n  {\n    component: 'Select',\n    componentProps: {\n      options: [\n        { label: '启用', value: 'ENABLED' },\n        { label: '禁用', value: 'DISABLED' },\n      ],\n    },\n    fieldName: 'status',\n    label: '状态',\n  },\n];\n\nexport const columns: VxeGridProps['columns'] = [\n  { type: 'checkbox', width: 60 },\n  {\n    title: 'ID',\n    field: 'id',\n    // width: 80,\n    visible:false\n  },\n  {\n    title: '市场名称',\n    field: 'name',\n    showOverflow: true,\n    width: 200,\n  },\n  {\n    title: '市场URL',\n    field: 'url',\n    showOverflow: true,\n    width: 250,\n  },\n  {\n    title: '市场描述',\n    field: 'description',\n    showOverflow: true,\n    // width: 200,\n  },\n  {\n    title: '状态',\n    field: 'status',\n    width: 100,\n    slots: {\n      default: 'status',\n    },\n  },\n  {\n    title: '创建时间',\n    field: 'createTime',\n    width: 160,\n  },\n  {\n    field: 'action',\n    fixed: 'right',\n    slots: { default: 'action' },\n    title: '操作',\n    resizable: false,\n    width: 'auto',\n  },\n];\n\nexport const drawerSchema: FormSchemaGetter = () => [\n  {\n    component: 'Input',\n    dependencies: {\n      show: () => false,\n      triggerFields: [''],\n    },\n    fieldName: 'id',\n    label: 'id',\n  },\n  {\n    component: 'Input',\n    fieldName: 'name',\n    label: '市场名称',\n    rules: 'required',\n    formItemClass: 'col-span-2',\n  },\n  {\n    component: 'Textarea',\n    componentProps: {\n      rows: 2,\n    },\n    fieldName: 'url',\n    formItemClass: 'col-span-2',\n    label: '市场URL',\n    rules: 'required',\n  },\n  {\n    component: 'Textarea',\n    componentProps: {\n      rows: 3,\n    },\n    fieldName: 'description',\n    formItemClass: 'col-span-2',\n    label: '市场描述',\n  },\n  {\n    component: 'Textarea',\n    componentProps: {\n      rows: 6,\n    },\n    fieldName: 'authConfig',\n    formItemClass: 'col-span-2',\n    help: '认证配置（JSON格式），例如：{\"type\": \"bearer\", \"token\": \"xxx\"}',\n    label: '认证配置',\n  },\n  {\n    component: 'RadioGroup',\n    componentProps: {\n      buttonStyle: 'solid',\n      options: [\n        { label: '启用', value: 'ENABLED' },\n        { label: '禁用', value: 'DISABLED' },\n      ],\n      optionType: 'button',\n    },\n    defaultValue: 'ENABLED',\n    fieldName: 'status',\n    label: '状态',\n  },\n];\n"
  },
  {
    "path": "apps/web-antd/src/views/mcp/market/index.vue",
    "content": "<script setup lang=\"ts\">\nimport type { VbenFormProps } from '@vben/common-ui';\n\nimport type { VxeGridProps } from '#/adapter/vxe-table';\nimport type { McpMarket } from '#/api/mcp/market/model';\n\nimport { useAccess } from '@vben/access';\nimport { Page, useVbenDrawer, useVbenModal } from '@vben/common-ui';\nimport { getVxePopupContainer } from '@vben/utils';\n\nimport { Modal, Popconfirm, Space, message } from 'ant-design-vue';\n\nimport { useVbenVxeGrid, vxeCheckboxChecked } from '#/adapter/vxe-table';\nimport {\n  mcpMarketChangeStatus,\n  mcpMarketExport,\n  mcpMarketList,\n  mcpMarketRefresh,\n  mcpMarketRemove,\n} from '#/api/mcp/market';\nimport { TableSwitch } from '#/components/table';\nimport { commonDownloadExcel } from '#/utils/file/download';\n\nimport marketDrawer from './market-drawer.vue';\nimport { columns, querySchema } from './data';\n\nconst formOptions: VbenFormProps = {\n  commonConfig: {\n    labelWidth: 80,\n    componentProps: {\n      allowClear: true,\n    },\n  },\n  schema: querySchema(),\n  wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4',\n};\n\nconst gridOptions: VxeGridProps = {\n  checkboxConfig: {\n    highlight: true,\n    reserve: true,\n  },\n  columns,\n  height: 'auto',\n  keepSource: true,\n  pagerConfig: {},\n  proxyConfig: {\n    ajax: {\n      query: async ({ page }, formValues = {}) => {\n        return await mcpMarketList({\n          pageNum: page.currentPage,\n          pageSize: page.pageSize,\n          ...formValues,\n        });\n      },\n    },\n  },\n  rowConfig: {\n    keyField: 'id',\n  },\n  id: 'mcp-market-index',\n  showOverflow: false,\n};\n\nconst [BasicTable, tableApi] = useVbenVxeGrid({\n  formOptions,\n  gridOptions,\n});\n\nconst [MarketDrawer, drawerApi] = useVbenDrawer({\n  connectedComponent: marketDrawer,\n});\n\nfunction handleAdd() {\n  drawerApi.setData({});\n  drawerApi.open();\n}\n\nasync function handleEdit(record: McpMarket) {\n  drawerApi.setData({ id: record.id });\n  drawerApi.open();\n}\n\nasync function handleDelete(row: McpMarket) {\n  await mcpMarketRemove([row.id]);\n  await tableApi.query();\n}\n\nfunction handleMultiDelete() {\n  const rows = tableApi.grid.getCheckboxRecords();\n  const ids = rows.map((row: McpMarket) => row.id);\n  Modal.confirm({\n    title: '提示',\n    okType: 'danger',\n    content: `确认删除选中的${ids.length}条记录吗？`,\n    onOk: async () => {\n      await mcpMarketRemove(ids);\n      await tableApi.query();\n    },\n  });\n}\n\nasync function handleRefresh(row: McpMarket) {\n  try {\n    const result = await mcpMarketRefresh(row.id);\n    message.success(`刷新成功，新增 ${result.addedCount} 个工具，更新 ${result.updatedCount} 个工具`);\n    await tableApi.query();\n  } catch (error) {\n    message.error('刷新失败');\n  }\n}\n\nfunction handleDownloadExcel() {\n  commonDownloadExcel(mcpMarketExport, 'MCP市场数据', tableApi.formApi.form.values);\n}\n\nconst { hasAccessByCodes } = useAccess();\n</script>\n\n<template>\n  <Page :auto-content-height=\"true\">\n    <BasicTable table-title=\"MCP市场列表\">\n      <template #toolbar-tools>\n        <Space>\n          <a-button\n            v-access:code=\"['mcp:market:export']\"\n            @click=\"handleDownloadExcel\"\n          >\n            {{ $t('pages.common.export') }}\n          </a-button>\n          <a-button\n            :disabled=\"!vxeCheckboxChecked(tableApi)\"\n            danger\n            type=\"primary\"\n            v-access:code=\"['mcp:market:remove']\"\n            @click=\"handleMultiDelete\"\n          >\n            {{ $t('pages.common.delete') }}\n          </a-button>\n          <a-button\n            type=\"primary\"\n            v-access:code=\"['mcp:market:add']\"\n            @click=\"handleAdd\"\n          >\n            {{ $t('pages.common.add') }}\n          </a-button>\n        </Space>\n      </template>\n      <template #status=\"{ row }\">\n        <TableSwitch\n          v-model:value=\"row.status\"\n          :api=\"() => mcpMarketChangeStatus(row)\"\n          :disabled=\"!hasAccessByCodes(['mcp:market:edit'])\"\n          :checked-value=\"'ENABLED'\"\n          :unchecked-value=\"'DISABLED'\"\n          @reload=\"tableApi.query()\"\n        />\n      </template>\n      <template #action=\"{ row }\">\n        <Space>\n          <ghost-button\n            v-access:code=\"['mcp:market:edit']\"\n            @click.stop=\"handleRefresh(row)\"\n          >\n            刷新\n          </ghost-button>\n          <ghost-button\n            v-access:code=\"['mcp:market:edit']\"\n            @click.stop=\"handleEdit(row)\"\n          >\n            {{ $t('pages.common.edit') }}\n          </ghost-button>\n          <Popconfirm\n            :get-popup-container=\"getVxePopupContainer\"\n            placement=\"left\"\n            title=\"确认删除？\"\n            @confirm=\"handleDelete(row)\"\n          >\n            <ghost-button\n              danger\n              v-access:code=\"['mcp:market:remove']\"\n              @click.stop=\"\"\n            >\n              {{ $t('pages.common.delete') }}\n            </ghost-button>\n          </Popconfirm>\n        </Space>\n      </template>\n    </BasicTable>\n    <MarketDrawer @reload=\"tableApi.query()\" />\n  </Page>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/mcp/market/market-drawer.vue",
    "content": "<script setup lang=\"ts\">\nimport { computed, ref } from 'vue';\n\nimport { useVbenDrawer } from '@vben/common-ui';\nimport { $t } from '@vben/locales';\nimport { cloneDeep } from '@vben/utils';\n\nimport { useVbenForm } from '#/adapter/form';\nimport { mcpMarketAdd, mcpMarketInfo, mcpMarketUpdate } from '#/api/mcp/market';\nimport { defaultFormValueGetter, useBeforeCloseDiff } from '#/utils/popup';\n\nimport { drawerSchema } from './data';\n\nconst emit = defineEmits<{ reload: [] }>();\n\nconst isUpdate = ref(false);\nconst title = computed(() => {\n  return isUpdate.value ? $t('pages.common.edit') : $t('pages.common.add');\n});\n\nconst [BasicForm, formApi] = useVbenForm({\n  commonConfig: {\n    formItemClass: 'col-span-2',\n    componentProps: {\n      class: 'w-full',\n    },\n  },\n  layout: 'vertical',\n  schema: drawerSchema(),\n  showDefaultActions: false,\n  wrapperClass: 'grid-cols-2 gap-x-4',\n});\n\nconst { onBeforeClose, markInitialized, resetInitialized } = useBeforeCloseDiff(\n  {\n    initializedGetter: defaultFormValueGetter(formApi),\n    currentGetter: defaultFormValueGetter(formApi),\n  },\n);\n\nconst [BasicDrawer, drawerApi] = useVbenDrawer({\n  onBeforeClose,\n  onClosed: handleClosed,\n  onConfirm: handleConfirm,\n  async onOpenChange(isOpen) {\n    if (!isOpen) {\n      return null;\n    }\n    drawerApi.drawerLoading(true);\n\n    const { id } = drawerApi.getData() as { id?: number | string };\n    isUpdate.value = !!id;\n    if (isUpdate.value && id) {\n      const record = await mcpMarketInfo(id);\n      await formApi.setValues(record);\n    }\n    await markInitialized();\n\n    drawerApi.drawerLoading(false);\n  },\n});\n\nasync function handleConfirm() {\n  try {\n    drawerApi.lock(true);\n    const { valid } = await formApi.validate();\n    if (!valid) {\n      return;\n    }\n    const data = cloneDeep(await formApi.getValues());\n    // 验证JSON格式\n    if (data.authConfig) {\n      try {\n        JSON.parse(data.authConfig);\n      } catch (e) {\n        formApi.setFieldValue('authConfig', data.authConfig);\n        drawerApi.lock(false);\n        return;\n      }\n    }\n    await (isUpdate.value ? mcpMarketUpdate(data) : mcpMarketAdd(data));\n    resetInitialized();\n    emit('reload');\n    drawerApi.close();\n  } catch (error) {\n    console.error(error);\n  } finally {\n    drawerApi.lock(false);\n  }\n}\n\nasync function handleClosed() {\n  await formApi.resetForm();\n  resetInitialized();\n}\n</script>\n\n<template>\n  <BasicDrawer :title=\"title\" class=\"w-[600px]\">\n    <BasicForm />\n  </BasicDrawer>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/mcp/tool/data.tsx",
    "content": "import type { FormSchemaGetter } from '#/adapter/form';\nimport type { VxeGridProps } from '#/adapter/vxe-table';\n\nexport const querySchema: FormSchemaGetter = () => [\n  {\n    component: 'Input',\n    fieldName: 'name',\n    label: '工具名称',\n  },\n  {\n    component: 'Select',\n    componentProps: {\n      options: [\n        { label: '本地工具', value: 'LOCAL' },\n        { label: '远程工具', value: 'REMOTE' },\n        { label: '内置工具', value: 'BUILTIN' },\n      ],\n    },\n    fieldName: 'type',\n    label: '工具类型',\n  },\n  {\n    component: 'Select',\n    componentProps: {\n      options: [\n        { label: '启用', value: 'ENABLED' },\n        { label: '禁用', value: 'DISABLED' },\n      ],\n    },\n    fieldName: 'status',\n    label: '状态',\n  },\n];\n\nexport const columns: VxeGridProps['columns'] = [\n  { type: 'checkbox', width: 60 },\n  {\n    title: 'ID',\n    field: 'id',\n    // width: 80,\n    visible:false\n  },\n  {\n    title: '工具名称',\n    field: 'name',\n    showOverflow: true,\n    width: 200,\n  },\n  {\n    title: '工具描述',\n    field: 'description',\n    showOverflow: true,\n  },\n  {\n    title: '工具类型',\n    field: 'type',\n    width: 100,\n    formatter({ cellValue }) {\n      const typeMap: Record<string, string> = {\n        LOCAL: '本地工具',\n        REMOTE: '远程工具',\n        BUILTIN: '内置工具',\n      };\n      return typeMap[cellValue] || cellValue;\n    },\n  },\n  {\n    title: '状态',\n    field: 'status',\n    width: 100,\n    slots: {\n      default: 'status',\n    },\n  },\n  {\n    title: '创建时间',\n    field: 'createTime',\n    width: 160,\n  },\n  {\n    field: 'action',\n    fixed: 'right',\n    slots: { default: 'action' },\n    title: '操作',\n    resizable: false,\n    width: 'auto',\n  },\n];\n\nexport const drawerSchema: FormSchemaGetter = () => [\n  {\n    component: 'Input',\n    dependencies: {\n      show: () => false,\n      triggerFields: [''],\n    },\n    fieldName: 'id',\n    label: 'id',\n  },\n  {\n    component: 'Input',\n    fieldName: 'name',\n    label: '工具名称',\n    rules: 'required',\n    formItemClass: 'col-span-2',\n  },\n  {\n    component: 'Textarea',\n    componentProps: {\n      rows: 3,\n    },\n    fieldName: 'description',\n    formItemClass: 'col-span-2',\n    label: '工具描述',\n  },\n  {\n    component: 'Select',\n    componentProps: {\n      options: [\n        { label: '本地工具', value: 'LOCAL' },\n        { label: '远程工具', value: 'REMOTE' },\n        { label: '内置工具', value: 'BUILTIN' },\n      ],\n    },\n    fieldName: 'type',\n    label: '工具类型',\n    rules: 'selectRequired',\n  },\n  {\n    component: 'RadioGroup',\n    componentProps: {\n      buttonStyle: 'solid',\n      options: [\n        { label: '启用', value: 'ENABLED' },\n        { label: '禁用', value: 'DISABLED' },\n      ],\n      optionType: 'button',\n    },\n    defaultValue: 'ENABLED',\n    fieldName: 'status',\n    label: '状态',\n  },\n  {\n    component: 'Textarea',\n    componentProps: {\n      rows: 8,\n    },\n    fieldName: 'configJson',\n    formItemClass: 'col-span-2',\n    help: '配置信息（JSON格式），例如：{\"command\": \"npx\", \"args\": [\"-y\", \"@modelcontextprotocol/server-everything\"]}',\n    label: '配置信息',\n  },\n];\n"
  },
  {
    "path": "apps/web-antd/src/views/mcp/tool/index.vue",
    "content": "<script setup lang=\"ts\">\nimport type { VbenFormProps } from '@vben/common-ui';\n\nimport type { VxeGridProps } from '#/adapter/vxe-table';\nimport type { McpTool } from '#/api/mcp/tool/model';\n\nimport { useAccess } from '@vben/access';\nimport { Page, useVbenDrawer } from '@vben/common-ui';\nimport { getVxePopupContainer } from '@vben/utils';\n\nimport { Modal, Popconfirm, Space, message } from 'ant-design-vue';\n\nimport { useVbenVxeGrid, vxeCheckboxChecked } from '#/adapter/vxe-table';\nimport {\n  mcpToolChangeStatus,\n  mcpToolExport,\n  mcpToolList,\n  mcpToolRemove,\n  mcpToolTest,\n} from '#/api/mcp/tool';\nimport { TableSwitch } from '#/components/table';\nimport { commonDownloadExcel } from '#/utils/file/download';\n\nimport toolDrawer from './tool-drawer.vue';\nimport { columns, querySchema } from './data';\n\nconst formOptions: VbenFormProps = {\n  commonConfig: {\n    labelWidth: 80,\n    componentProps: {\n      allowClear: true,\n    },\n  },\n  schema: querySchema(),\n  wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4',\n};\n\nconst gridOptions: VxeGridProps = {\n  checkboxConfig: {\n    highlight: true,\n    reserve: true,\n  },\n  columns,\n  height: 'auto',\n  keepSource: true,\n  pagerConfig: {},\n  proxyConfig: {\n    ajax: {\n      query: async ({ page }, formValues = {}) => {\n        return await mcpToolList({\n          pageNum: page.currentPage,\n          pageSize: page.pageSize,\n          ...formValues,\n        });\n      },\n    },\n  },\n  rowConfig: {\n    keyField: 'id',\n  },\n  id: 'mcp-tool-index',\n  showOverflow: false,\n};\n\nconst [BasicTable, tableApi] = useVbenVxeGrid({\n  formOptions,\n  gridOptions,\n});\n\nconst [ToolDrawer, drawerApi] = useVbenDrawer({\n  connectedComponent: toolDrawer,\n});\n\nfunction handleAdd() {\n  drawerApi.setData({});\n  drawerApi.open();\n}\n\nasync function handleEdit(record: McpTool) {\n  drawerApi.setData({ id: record.id });\n  drawerApi.open();\n}\n\nasync function handleDelete(row: McpTool) {\n  await mcpToolRemove([row.id]);\n  await tableApi.query();\n}\n\nfunction handleMultiDelete() {\n  const rows = tableApi.grid.getCheckboxRecords();\n  const ids = rows.map((row: McpTool) => row.id);\n  Modal.confirm({\n    title: '提示',\n    okType: 'danger',\n    content: `确认删除选中的${ids.length}条记录吗？`,\n    onOk: async () => {\n      await mcpToolRemove(ids);\n      await tableApi.query();\n    },\n  });\n}\n\nasync function handleTest(row: McpTool) {\n  try {\n    const result = await mcpToolTest(row.id);\n    if (result.success) {\n      message.success('工具连接测试成功');\n    } else {\n      message.error(`工具连接测试失败: ${result.message}`);\n    }\n  } catch (error) {\n    message.error('工具连接测试失败');\n  }\n}\n\nfunction handleDownloadExcel() {\n  commonDownloadExcel(mcpToolExport, 'MCP工具数据', tableApi.formApi.form.values);\n}\n\nconst { hasAccessByCodes } = useAccess();\n</script>\n\n<template>\n  <Page :auto-content-height=\"true\">\n    <BasicTable table-title=\"MCP工具列表\">\n      <template #toolbar-tools>\n        <Space>\n          <a-button\n            v-access:code=\"['mcp:tool:export']\"\n            @click=\"handleDownloadExcel\"\n          >\n            {{ $t('pages.common.export') }}\n          </a-button>\n          <a-button\n            :disabled=\"!vxeCheckboxChecked(tableApi)\"\n            danger\n            type=\"primary\"\n            v-access:code=\"['mcp:tool:remove']\"\n            @click=\"handleMultiDelete\"\n          >\n            {{ $t('pages.common.delete') }}\n          </a-button>\n          <a-button\n            type=\"primary\"\n            v-access:code=\"['mcp:tool:add']\"\n            @click=\"handleAdd\"\n          >\n            {{ $t('pages.common.add') }}\n          </a-button>\n        </Space>\n      </template>\n      <template #status=\"{ row }\">\n        <TableSwitch\n          v-model:value=\"row.status\"\n          :api=\"() => mcpToolChangeStatus(row)\"\n          :disabled=\"!hasAccessByCodes(['mcp:tool:edit'])\"\n          :checked-value=\"'ENABLED'\"\n          :unchecked-value=\"'DISABLED'\"\n          @reload=\"tableApi.query()\"\n        />\n      </template>\n      <template #action=\"{ row }\">\n        <Space>\n          <ghost-button\n            v-access:code=\"['mcp:tool:query']\"\n            @click.stop=\"handleTest(row)\"\n          >\n            测试\n          </ghost-button>\n          <ghost-button\n            v-access:code=\"['mcp:tool:edit']\"\n            @click.stop=\"handleEdit(row)\"\n          >\n            {{ $t('pages.common.edit') }}\n          </ghost-button>\n          <Popconfirm\n            :get-popup-container=\"getVxePopupContainer\"\n            placement=\"left\"\n            title=\"确认删除？\"\n            @confirm=\"handleDelete(row)\"\n          >\n            <ghost-button\n              danger\n              v-access:code=\"['mcp:tool:remove']\"\n              @click.stop=\"\"\n            >\n              {{ $t('pages.common.delete') }}\n            </ghost-button>\n          </Popconfirm>\n        </Space>\n      </template>\n    </BasicTable>\n    <ToolDrawer @reload=\"tableApi.query()\" />\n  </Page>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/mcp/tool/tool-drawer.vue",
    "content": "<script setup lang=\"ts\">\nimport { computed, ref } from 'vue';\n\nimport { useVbenDrawer } from '@vben/common-ui';\nimport { $t } from '@vben/locales';\nimport { cloneDeep } from '@vben/utils';\n\nimport { useVbenForm } from '#/adapter/form';\nimport { mcpToolAdd, mcpToolInfo, mcpToolUpdate } from '#/api/mcp/tool';\nimport { defaultFormValueGetter, useBeforeCloseDiff } from '#/utils/popup';\n\nimport { drawerSchema } from './data';\n\nconst emit = defineEmits<{ reload: [] }>();\n\nconst isUpdate = ref(false);\nconst title = computed(() => {\n  return isUpdate.value ? $t('pages.common.edit') : $t('pages.common.add');\n});\n\nconst [BasicForm, formApi] = useVbenForm({\n  commonConfig: {\n    formItemClass: 'col-span-2',\n    componentProps: {\n      class: 'w-full',\n    },\n  },\n  layout: 'vertical',\n  schema: drawerSchema(),\n  showDefaultActions: false,\n  wrapperClass: 'grid-cols-2 gap-x-4',\n});\n\nfunction setupForm(update: boolean) {\n  formApi.updateSchema([\n    {\n      componentProps: {\n        disabled: update,\n      },\n      fieldName: 'type',\n    },\n  ]);\n}\n\nconst { onBeforeClose, markInitialized, resetInitialized } = useBeforeCloseDiff(\n  {\n    initializedGetter: defaultFormValueGetter(formApi),\n    currentGetter: defaultFormValueGetter(formApi),\n  },\n);\n\nconst [BasicDrawer, drawerApi] = useVbenDrawer({\n  onBeforeClose,\n  onClosed: handleClosed,\n  onConfirm: handleConfirm,\n  async onOpenChange(isOpen) {\n    if (!isOpen) {\n      return null;\n    }\n    drawerApi.drawerLoading(true);\n\n    const { id } = drawerApi.getData() as { id?: number | string };\n    isUpdate.value = !!id;\n    setupForm(isUpdate.value);\n    if (isUpdate.value && id) {\n      const record = await mcpToolInfo(id);\n      await formApi.setValues(record);\n    }\n    await markInitialized();\n\n    drawerApi.drawerLoading(false);\n  },\n});\n\nasync function handleConfirm() {\n  try {\n    drawerApi.lock(true);\n    const { valid } = await formApi.validate();\n    if (!valid) {\n      return;\n    }\n    const data = cloneDeep(await formApi.getValues());\n    // 验证JSON格式\n    if (data.configJson) {\n      try {\n        JSON.parse(data.configJson);\n      } catch (e) {\n        formApi.setFieldValue('configJson', data.configJson);\n        drawerApi.lock(false);\n        return;\n      }\n    }\n    await (isUpdate.value ? mcpToolUpdate(data) : mcpToolAdd(data));\n    resetInitialized();\n    emit('reload');\n    drawerApi.close();\n  } catch (error) {\n    console.error(error);\n  } finally {\n    drawerApi.lock(false);\n  }\n}\n\nasync function handleClosed() {\n  await formApi.resetForm();\n  resetInitialized();\n}\n</script>\n\n<template>\n  <BasicDrawer :title=\"title\" class=\"w-[600px]\">\n    <BasicForm />\n  </BasicDrawer>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/monitor/admin/index.vue",
    "content": "<!-- 建议通过菜单配置成外链/内嵌 达到相同的效果且灵活性更高 -->\n<template>\n  <iframe\n    class=\"size-full\"\n    src=\"http://localhost:9090/admin/applications\"\n  ></iframe>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/monitor/cache/components/command-chart.vue",
    "content": "<script setup lang=\"ts\">\nimport type { EchartsUIType } from '@vben/plugins/echarts';\n\nimport { onActivated, onMounted, ref, watch } from 'vue';\n\nimport { EchartsUI, useEcharts } from '@vben/plugins/echarts';\n\ninterface Props {\n  data?: { name: string; value: string }[];\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n  data: () => [],\n});\n\nconst chartRef = ref<EchartsUIType>();\nconst { renderEcharts, resize } = useEcharts(chartRef);\n\nwatch(\n  () => props.data,\n  () => {\n    if (!chartRef.value) return;\n    setEchartsOption(props.data);\n  },\n  { immediate: true },\n);\n\nonMounted(() => {\n  setEchartsOption(props.data);\n});\n/**\n * 从其他页面切换回来会有一个奇怪的动画效果 需要调用resize\n * 该饼图组件需要关闭animation\n */\nonActivated(() => resize(false));\n\ntype EChartsOption = Parameters<typeof renderEcharts>['0'];\nfunction setEchartsOption(data: any[]) {\n  const option: EChartsOption = {\n    series: [\n      {\n        animationDuration: 1000,\n        animationEasing: 'cubicInOut',\n        center: ['50%', '50%'],\n        data,\n        name: '命令',\n        radius: [15, 95],\n        roseType: 'radius',\n        type: 'pie',\n      },\n    ],\n    tooltip: {\n      formatter: '{a} <br/>{b} : {c} ({d}%)',\n      trigger: 'item',\n    },\n  };\n  renderEcharts(option);\n}\n</script>\n\n<template>\n  <EchartsUI ref=\"chartRef\" height=\"400px\" width=\"100%\" />\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/monitor/cache/components/index.ts",
    "content": "export { default as CommandChart } from './command-chart.vue';\nexport { default as MemoryChart } from './memory-chart.vue';\nexport { default as RedisDescription } from './redis-description.vue';\n"
  },
  {
    "path": "apps/web-antd/src/views/monitor/cache/components/memory-chart.vue",
    "content": "<script setup lang=\"ts\">\nimport type { EchartsUIType } from '@vben/plugins/echarts';\n\nimport { onActivated, onMounted, ref, watch } from 'vue';\n\nimport { EchartsUI, useEcharts } from '@vben/plugins/echarts';\n\ninterface Props {\n  data?: string;\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n  data: '0',\n});\n\nconst memoryHtmlRef = ref<EchartsUIType>();\nconst { renderEcharts, resize } = useEcharts(memoryHtmlRef);\n\nwatch(\n  () => props.data,\n  () => {\n    if (!memoryHtmlRef.value) return;\n    setEchartsOption(props.data);\n  },\n  { immediate: true },\n);\n\nonMounted(() => {\n  setEchartsOption(props.data);\n});\n// 从其他页面切换回来会有一个奇怪的动画效果 需要调用resize\nonActivated(resize);\n\n/**\n * 获取最近的十的幂次\n * 该函数用于寻找大于给定数字num的最近的10的幂次\n * 主要解决的问题是确定一个数附近较大的十的幂次，这在某些算法中很有用\n *\n * @param num {number} 输入的数字，用于寻找最近的十的幂次\n */\nfunction getNearestPowerOfTen(num: number) {\n  let power = 10;\n  while (power <= num) {\n    power *= 10;\n  }\n  return power;\n}\n\ntype EChartsOption = Parameters<typeof renderEcharts>['0'];\nfunction setEchartsOption(value: string) {\n  // x10\n  const formattedValue = Math.floor(Number.parseFloat(value));\n  // 最大值 10以内取10  100以内取100 以此类推\n  const max = getNearestPowerOfTen(formattedValue);\n  const options: EChartsOption = {\n    series: [\n      {\n        animation: true,\n        animationDuration: 1000,\n        data: [\n          {\n            name: '内存消耗',\n            value: Number.parseFloat(value),\n          },\n        ],\n        detail: {\n          formatter: `${value}M`,\n          valueAnimation: true,\n        },\n        max,\n        min: 0,\n        name: '峰值',\n        progress: {\n          show: true,\n        },\n        type: 'gauge',\n      },\n    ],\n    tooltip: {\n      formatter: `{b} <br/>{a} : ${value}M`,\n    },\n  };\n  renderEcharts(options);\n}\n</script>\n\n<template>\n  <EchartsUI ref=\"memoryHtmlRef\" height=\"400px\" width=\"100%\" />\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/monitor/cache/components/redis-description.vue",
    "content": "<script setup lang=\"ts\">\nimport type { RedisInfo } from '#/api/monitor/cache';\n\nimport { Descriptions, DescriptionsItem } from 'ant-design-vue';\n\ninterface IRedisInfo extends RedisInfo {\n  dbSize: string;\n}\n\ndefineProps<{ data: IRedisInfo }>();\n</script>\n\n<template>\n  <Descriptions\n    bordered\n    :column=\"{ lg: 4, md: 3, sm: 1, xl: 4, xs: 1 }\"\n    size=\"small\"\n  >\n    <DescriptionsItem label=\"redis版本\">\n      {{ data.redis_version }}\n    </DescriptionsItem>\n    <DescriptionsItem label=\"redis模式\">\n      {{ data.redis_mode === 'standalone' ? '单机模式' : '集群模式' }}\n    </DescriptionsItem>\n    <DescriptionsItem label=\"tcp端口\">\n      {{ data.tcp_port }}\n    </DescriptionsItem>\n    <DescriptionsItem label=\"客户端数\">\n      {{ data.connected_clients }}\n    </DescriptionsItem>\n    <DescriptionsItem label=\"运行时间\">\n      {{ data.uptime_in_days }} 天\n    </DescriptionsItem>\n    <DescriptionsItem label=\"使用内存\">\n      {{ data.used_memory_human }}\n    </DescriptionsItem>\n    <DescriptionsItem label=\"使用CPU\">\n      {{ Number.parseFloat(data?.used_cpu_user_children ?? '0').toFixed(2) }}\n    </DescriptionsItem>\n    <DescriptionsItem label=\"内存配置\">\n      {{ data.maxmemory_human }}\n    </DescriptionsItem>\n    <DescriptionsItem label=\"AOF是否开启\">\n      {{ data.aof_enabled === '0' ? '否' : '是' }}\n    </DescriptionsItem>\n    <DescriptionsItem label=\"RDB是否成功\">\n      {{ data.rdb_last_bgsave_status }}\n    </DescriptionsItem>\n    <DescriptionsItem label=\"key数量\">\n      {{ data.dbSize }}\n    </DescriptionsItem>\n    <DescriptionsItem label=\"网络入口/出口\">\n      {{\n        `${data.instantaneous_input_kbps}kps/${data.instantaneous_output_kbps}kps`\n      }}\n    </DescriptionsItem>\n  </Descriptions>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/monitor/cache/index.vue",
    "content": "<script setup lang=\"ts\">\nimport type { RedisInfo } from '#/api/monitor/cache';\n\nimport { onMounted, reactive, ref } from 'vue';\n\nimport { Page } from '@vben/common-ui';\nimport { CommandLineIcon, MemoryIcon, RedisIcon } from '@vben/icons';\n\nimport { Button, Card, Col, Row } from 'ant-design-vue';\n\nimport { redisCacheInfo } from '#/api/monitor/cache';\n\nimport { CommandChart, MemoryChart, RedisDescription } from './components';\n\nconst baseSpan = { lg: 12, md: 24, sm: 24, xl: 12, xs: 24 };\n\nconst chartData = reactive<{\n  command: { name: string; value: string }[];\n  memory: string;\n}>({\n  command: [],\n  memory: '0',\n});\n\ninterface IRedisInfo extends RedisInfo {\n  dbSize: string;\n}\nconst redisInfo = ref<IRedisInfo>();\n\nonMounted(async () => {\n  await loadInfo();\n});\n\nasync function loadInfo() {\n  try {\n    const ret = await redisCacheInfo();\n\n    // 单位MB 保留两位小数\n    const usedMemory = (\n      Number.parseInt(ret.info.used_memory!) /\n      1024 /\n      1024\n    ).toFixed(2);\n    chartData.memory = usedMemory;\n    // 命令统计\n    chartData.command = ret.commandStats;\n    console.log(chartData.command);\n    // redis信息\n    redisInfo.value = { ...ret.info, dbSize: String(ret.dbSize) };\n  } catch (error) {\n    console.warn(error);\n  }\n}\n</script>\n\n<template>\n  <Page>\n    <Row :gutter=\"[15, 15]\">\n      <Col :span=\"24\">\n        <Card size=\"small\">\n          <template #title>\n            <div class=\"flex items-center justify-start gap-[6px]\">\n              <RedisIcon class=\"size-[16px]\" />\n              <span>redis信息</span>\n            </div>\n          </template>\n          <template #extra>\n            <Button size=\"small\" @click=\"loadInfo\">\n              <div class=\"flex\">\n                <span class=\"icon-[charm--refresh]\"></span>\n              </div>\n            </Button>\n          </template>\n          <RedisDescription v-if=\"redisInfo\" :data=\"redisInfo\" />\n        </Card>\n      </Col>\n      <Col v-bind=\"baseSpan\">\n        <Card size=\"small\">\n          <template #title>\n            <div class=\"flex items-center gap-[6px]\">\n              <CommandLineIcon class=\"size-[16px]\" />\n              <span>命令统计</span>\n            </div>\n          </template>\n          <CommandChart\n            v-if=\"chartData.command.length > 0\"\n            :data=\"chartData.command\"\n          />\n        </Card>\n      </Col>\n      <Col v-bind=\"baseSpan\">\n        <Card size=\"small\">\n          <template #title>\n            <div class=\"flex items-center justify-start gap-[6px]\">\n              <MemoryIcon class=\"size-[16px]\" />\n              <span>内存占用</span>\n            </div>\n          </template>\n          <MemoryChart\n            v-if=\"chartData.memory !== '0'\"\n            :data=\"chartData.memory\"\n          />\n        </Card>\n      </Col>\n    </Row>\n  </Page>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/monitor/logininfor/data.tsx",
    "content": "import type { VNode } from 'vue';\n\nimport type { FormSchemaGetter } from '#/adapter/form';\nimport type { VxeGridProps } from '#/adapter/vxe-table';\n\nimport { DictEnum } from '@vben/constants';\n\nimport { getDictOptions } from '#/utils/dict';\nimport { renderBrowserIcon, renderDict, renderOsIcon } from '#/utils/render';\n\nexport const querySchema: FormSchemaGetter = () => [\n  {\n    component: 'Input',\n    fieldName: 'ipaddr',\n    label: 'IP地址',\n  },\n  {\n    component: 'Input',\n    fieldName: 'userName',\n    label: '用户账号',\n  },\n  {\n    component: 'Select',\n    componentProps: {\n      options: getDictOptions(DictEnum.SYS_COMMON_STATUS),\n    },\n    fieldName: 'status',\n    label: '登录状态',\n  },\n  {\n    component: 'RangePicker',\n    fieldName: 'dateTime',\n    label: '登录日期',\n  },\n];\n\nexport const columns: VxeGridProps['columns'] = [\n  { type: 'checkbox', width: 60 },\n  {\n    title: '用户账号',\n    field: 'userName',\n  },\n  {\n    title: '登录平台',\n    field: 'clientKey',\n  },\n  {\n    title: 'IP地址',\n    field: 'ipaddr',\n  },\n  {\n    title: 'IP地点',\n    field: 'loginLocation',\n    width: 200,\n  },\n  {\n    title: '浏览器',\n    field: 'browser',\n    slots: {\n      default: ({ row }) => {\n        return renderBrowserIcon(row.browser, true) as VNode;\n      },\n    },\n  },\n  {\n    title: '系统',\n    field: 'os',\n    slots: {\n      default: ({ row }) => {\n        /**\n         *  Windows 10 or Windows Server 2016 太长了 分割一下 详情依旧能看到详细的\n         */\n        let value = row.os;\n        if (value) {\n          const split = value.split(' or ');\n          if (split.length === 2) {\n            value = split[0];\n          }\n        }\n        return renderOsIcon(value, true) as VNode;\n      },\n    },\n  },\n  {\n    title: '登录结果',\n    field: 'status',\n    slots: {\n      default: ({ row }) => {\n        return renderDict(row.status, DictEnum.SYS_COMMON_STATUS);\n      },\n    },\n  },\n  {\n    title: '信息',\n    field: 'msg',\n  },\n  {\n    title: '日期',\n    field: 'loginTime',\n  },\n  {\n    field: 'action',\n    fixed: 'right',\n    slots: { default: 'action' },\n    title: '操作',\n    width: 150,\n  },\n];\n"
  },
  {
    "path": "apps/web-antd/src/views/monitor/logininfor/index.vue",
    "content": "<script setup lang=\"ts\">\nimport type { VbenFormProps } from '@vben/common-ui';\n\nimport type { VxeGridProps } from '#/adapter/vxe-table';\nimport type { LoginLog } from '#/api/monitor/logininfo/model';\n\nimport { ref } from 'vue';\n\nimport { Page, useVbenModal } from '@vben/common-ui';\nimport { getVxePopupContainer } from '@vben/utils';\n\nimport { Modal, Popconfirm, Space } from 'ant-design-vue';\n\nimport { useVbenVxeGrid, vxeCheckboxChecked } from '#/adapter/vxe-table';\nimport {\n  loginInfoClean,\n  loginInfoExport,\n  loginInfoList,\n  loginInfoRemove,\n  userUnlock,\n} from '#/api/monitor/logininfo';\nimport { commonDownloadExcel } from '#/utils/file/download';\nimport { confirmDeleteModal } from '#/utils/modal';\n\nimport { columns, querySchema } from './data';\nimport loginInfoModal from './login-info-modal.vue';\n\nconst formOptions: VbenFormProps = {\n  commonConfig: {\n    labelWidth: 80,\n    componentProps: {\n      allowClear: true,\n    },\n  },\n  schema: querySchema(),\n  wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4',\n  // 日期选择格式化\n  fieldMappingTime: [\n    [\n      'dateTime',\n      ['params[beginTime]', 'params[endTime]'],\n      ['YYYY-MM-DD 00:00:00', 'YYYY-MM-DD 23:59:59'],\n    ],\n  ],\n};\n\nconst gridOptions: VxeGridProps = {\n  checkboxConfig: {\n    // 高亮\n    highlight: true,\n    // 翻页时保留选中状态\n    reserve: true,\n    // 点击行选中\n    trigger: 'row',\n  },\n  columns,\n  height: 'auto',\n  keepSource: true,\n  pagerConfig: {},\n  proxyConfig: {\n    ajax: {\n      query: async ({ page }, formValues = {}) => {\n        return await loginInfoList({\n          pageNum: page.currentPage,\n          pageSize: page.pageSize,\n          ...formValues,\n        });\n      },\n    },\n  },\n  rowConfig: {\n    keyField: 'infoId',\n  },\n  id: 'monitor-logininfo-index',\n};\n\nconst canUnlock = ref(false);\nconst [BasicTable, tableApi] = useVbenVxeGrid({\n  formOptions,\n  gridOptions,\n  gridEvents: {\n    checkboxChange: (e) => {\n      const records = e.$grid?.getCheckboxRecords?.() ?? [];\n      canUnlock.value = records.length === 1 && records[0]!.status === '1';\n    },\n  },\n});\n\nconst [LoginInfoModal, modalApi] = useVbenModal({\n  connectedComponent: loginInfoModal,\n});\n\nfunction handlePreview(record: LoginLog) {\n  modalApi.setData(record);\n  modalApi.open();\n}\n\nfunction handleClear() {\n  confirmDeleteModal({\n    onValidated: async () => {\n      await loginInfoClean();\n      await tableApi.reload();\n    },\n  });\n}\n\nasync function handleDelete(row: LoginLog) {\n  await loginInfoRemove([row.infoId]);\n  await tableApi.query();\n}\n\nfunction handleMultiDelete() {\n  const rows = tableApi.grid.getCheckboxRecords();\n  const ids = rows.map((row: LoginLog) => row.infoId);\n  Modal.confirm({\n    title: '提示',\n    okType: 'danger',\n    content: `确认删除选中的${ids.length}条记录吗？`,\n    onOk: async () => {\n      await loginInfoRemove(ids);\n      await tableApi.query();\n    },\n  });\n}\n\nasync function handleUnlock() {\n  const records = tableApi.grid.getCheckboxRecords();\n  if (records.length !== 1) {\n    return;\n  }\n  const { userName } = records[0];\n  await userUnlock(userName);\n  await tableApi.query();\n  canUnlock.value = false;\n  tableApi.grid.clearCheckboxRow();\n}\n\nfunction handleDownloadExcel() {\n  commonDownloadExcel(\n    loginInfoExport,\n    '登录日志',\n    tableApi.formApi.form.values,\n    {\n      fieldMappingTime: formOptions.fieldMappingTime,\n    },\n  );\n}\n</script>\n\n<template>\n  <Page auto-content-height>\n    <BasicTable table-title=\"登录日志列表\">\n      <template #toolbar-tools>\n        <Space>\n          <a-button\n            v-access:code=\"['monitor:logininfor:remove']\"\n            @click=\"handleClear\"\n          >\n            {{ $t('pages.common.clear') }}\n          </a-button>\n          <a-button\n            v-access:code=\"['monitor:logininfor:export']\"\n            @click=\"handleDownloadExcel\"\n          >\n            {{ $t('pages.common.export') }}\n          </a-button>\n          <a-button\n            :disabled=\"!vxeCheckboxChecked(tableApi)\"\n            danger\n            type=\"primary\"\n            v-access:code=\"['monitor:logininfor:remove']\"\n            @click=\"handleMultiDelete\"\n          >\n            {{ $t('pages.common.delete') }}\n          </a-button>\n          <a-button\n            :disabled=\"!canUnlock\"\n            type=\"primary\"\n            v-access:code=\"['monitor:logininfor:unlock']\"\n            @click=\"handleUnlock\"\n          >\n            {{ $t('pages.common.unlock') }}\n          </a-button>\n        </Space>\n      </template>\n      <template #action=\"{ row }\">\n        <Space>\n          <ghost-button @click.stop=\"handlePreview(row)\">\n            {{ $t('pages.common.info') }}\n          </ghost-button>\n          <Popconfirm\n            :get-popup-container=\"getVxePopupContainer\"\n            placement=\"left\"\n            title=\"确认删除?\"\n            @confirm=\"() => handleDelete(row)\"\n          >\n            <ghost-button\n              danger\n              v-access:code=\"['monitor:logininfor:remove']\"\n              @click.stop=\"\"\n            >\n              {{ $t('pages.common.delete') }}\n            </ghost-button>\n          </Popconfirm>\n        </Space>\n      </template>\n    </BasicTable>\n    <LoginInfoModal />\n  </Page>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/monitor/logininfor/login-info-modal.vue",
    "content": "<script setup lang=\"ts\">\nimport type { LoginLog } from '#/api/monitor/logininfo/model';\n\nimport { ref } from 'vue';\n\nimport { useVbenModal } from '@vben/common-ui';\nimport { DictEnum } from '@vben/constants';\n\nimport { Descriptions, DescriptionsItem } from 'ant-design-vue';\n\nimport { renderBrowserIcon, renderDict, renderOsIcon } from '#/utils/render';\n\nconst loginInfo = ref<LoginLog>();\nconst [BasicModal, modalApi] = useVbenModal({\n  onOpenChange: (isOpen) => {\n    if (!isOpen) {\n      return null;\n    }\n    const record = modalApi.getData() as LoginLog;\n    loginInfo.value = record;\n  },\n  onClosed() {\n    loginInfo.value = undefined;\n  },\n});\n</script>\n\n<template>\n  <BasicModal\n    :footer=\"false\"\n    :fullscreen-button=\"false\"\n    class=\"w-[550px]\"\n    title=\"登录日志\"\n  >\n    <Descriptions v-if=\"loginInfo\" size=\"small\" :column=\"1\" bordered>\n      <DescriptionsItem label=\"登录状态\">\n        <component\n          :is=\"renderDict(loginInfo.status, DictEnum.SYS_COMMON_STATUS)\"\n        />\n      </DescriptionsItem>\n      <DescriptionsItem label=\"登录平台\">\n        {{ loginInfo.clientKey.toLowerCase() }}\n      </DescriptionsItem>\n      <DescriptionsItem label=\"账号信息\">\n        {{\n          `账号: ${loginInfo.userName} / ${loginInfo.ipaddr} / ${loginInfo.loginLocation}`\n        }}\n      </DescriptionsItem>\n      <DescriptionsItem label=\"登录时间\">\n        {{ loginInfo.loginTime }}\n      </DescriptionsItem>\n      <DescriptionsItem label=\"登录信息\">\n        <span\n          class=\"font-semibold\"\n          :class=\"{ 'text-red-500': loginInfo.status !== '0' }\"\n        >\n          {{ loginInfo.msg }}\n        </span>\n      </DescriptionsItem>\n      <DescriptionsItem label=\"登录设备\">\n        <component :is=\"renderOsIcon(loginInfo.os)\" />\n      </DescriptionsItem>\n      <DescriptionsItem label=\"浏览器\">\n        <component :is=\"renderBrowserIcon(loginInfo.browser)\" />\n      </DescriptionsItem>\n    </Descriptions>\n  </BasicModal>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/monitor/online/data.ts",
    "content": "import type { VNode } from 'vue';\n\nimport type { FormSchemaGetter } from '#/adapter/form';\nimport type { VxeGridProps } from '#/adapter/vxe-table';\n\nimport dayjs from 'dayjs';\n\nimport { renderBrowserIcon, renderOsIcon } from '#/utils/render';\n\nexport const querySchema: FormSchemaGetter = () => [\n  {\n    component: 'Input',\n    fieldName: 'ipaddr',\n    label: 'IP地址',\n  },\n  {\n    component: 'Input',\n    fieldName: 'userName',\n    label: '用户账号',\n  },\n];\n\nexport const columns: VxeGridProps['columns'] = [\n  {\n    title: '登录平台',\n    field: 'deviceType',\n  },\n  {\n    title: '登录账号',\n    field: 'userName',\n  },\n  {\n    title: '部门名称',\n    field: 'deptName',\n  },\n  {\n    title: 'IP地址',\n    field: 'ipaddr',\n  },\n  {\n    title: '登录地址',\n    field: 'loginLocation',\n  },\n  {\n    title: '浏览器',\n    field: 'browser',\n    slots: {\n      default: ({ row }) => {\n        return renderBrowserIcon(row.browser, true) as VNode;\n      },\n    },\n  },\n  {\n    title: '系统',\n    field: 'os',\n    slots: {\n      default: ({ row }) => {\n        // Windows 10 or Windows Server 2016 太长了 分割一下 详情依旧能看到详细的\n        let value = row.os;\n        if (value) {\n          const split = value.split(' or ');\n          if (split.length === 2) {\n            value = split[0];\n          }\n        }\n        return renderOsIcon(value, true) as VNode;\n      },\n    },\n  },\n  {\n    title: '登录时间',\n    field: 'loginTime',\n    formatter: ({ cellValue }) => {\n      return dayjs(cellValue).format('YYYY-MM-DD HH:mm:ss');\n    },\n  },\n  {\n    field: 'action',\n    fixed: 'right',\n    slots: { default: 'action' },\n    title: '操作',\n    resizable: false,\n    width: 'auto',\n  },\n];\n"
  },
  {
    "path": "apps/web-antd/src/views/monitor/online/index.vue",
    "content": "<script setup lang=\"ts\">\nimport type { VbenFormProps } from '@vben/common-ui';\n\nimport type { VxeGridProps } from '#/adapter/vxe-table';\nimport type { OnlineUser } from '#/api/monitor/online/model';\n\nimport { ref } from 'vue';\n\nimport { Page } from '@vben/common-ui';\nimport { getVxePopupContainer } from '@vben/utils';\n\nimport { Popconfirm } from 'ant-design-vue';\nimport { slice } from 'lodash-es';\n\nimport { useVbenVxeGrid } from '#/adapter/vxe-table';\nimport { forceLogout, onlineList } from '#/api/monitor/online';\n\nimport { columns, querySchema } from './data';\n\nconst formOptions: VbenFormProps = {\n  commonConfig: {\n    labelWidth: 80,\n    componentProps: {\n      allowClear: true,\n    },\n  },\n  schema: querySchema(),\n  wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4',\n};\n\nconst onlineCount = ref(0);\nconst gridOptions: VxeGridProps = {\n  columns,\n  height: 'auto',\n  keepSource: true,\n  pagerConfig: {},\n  proxyConfig: {\n    ajax: {\n      // 后端其实是一个假分页 返回的是全部数据\n      query: async ({ page }, formValues = {}) => {\n        const resp = await onlineList({\n          ...formValues,\n        });\n        // 设置在线数\n        onlineCount.value = resp.total;\n\n        const { currentPage, pageSize } = page;\n        // 当前需要截取的index -> 当前page-1 * 每页条数\n        const currentIndex = (currentPage - 1) * pageSize;\n        // 当前需要截取的endIndex -> currentIndex + 每页条数\n        const endIndex = currentIndex + pageSize;\n        // 截取区间内的数据\n        const sliceRows = slice(resp.rows, currentIndex, endIndex);\n        resp.rows = sliceRows;\n\n        return resp;\n      },\n    },\n  },\n  scrollY: {\n    enabled: true,\n    gt: 0,\n  },\n  rowConfig: {\n    keyField: 'tokenId',\n  },\n  id: 'monitor-online-index',\n};\n\nconst [BasicTable, tableApi] = useVbenVxeGrid({ formOptions, gridOptions });\n\nasync function handleForceOffline(row: OnlineUser) {\n  await forceLogout(row.tokenId);\n  await tableApi.query();\n}\n</script>\n\n<template>\n  <Page :auto-content-height=\"true\">\n    <BasicTable>\n      <template #toolbar-actions>\n        <div class=\"mr-1 pl-1 text-[1rem]\">\n          <div>\n            在线用户列表 (共\n            <span class=\"text-primary font-bold\">{{ onlineCount }}</span>\n            人在线)\n          </div>\n        </div>\n      </template>\n      <template #action=\"{ row }\">\n        <Popconfirm\n          :get-popup-container=\"getVxePopupContainer\"\n          :title=\"`确认强制下线[${row.userName}]?`\"\n          placement=\"left\"\n          @confirm=\"handleForceOffline(row)\"\n        >\n          <ghost-button danger>强制下线</ghost-button>\n        </Popconfirm>\n      </template>\n    </BasicTable>\n  </Page>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/monitor/operlog/data.tsx",
    "content": "import type { FormSchemaGetter } from '#/adapter/form';\nimport type { VxeGridProps } from '#/adapter/vxe-table';\n\nimport { DictEnum } from '@vben/constants';\n\nimport { getDictOptions } from '#/utils/dict';\nimport { renderDict } from '#/utils/render';\n\nexport const querySchema: FormSchemaGetter = () => [\n  {\n    component: 'Input',\n    fieldName: 'title',\n    label: '系统模块',\n  },\n  {\n    component: 'Input',\n    fieldName: 'operName',\n    label: '操作人员',\n  },\n  {\n    component: 'Select',\n    componentProps: {\n      options: getDictOptions(DictEnum.SYS_OPER_TYPE),\n    },\n    fieldName: 'businessType',\n    label: '操作类型',\n  },\n  {\n    component: 'Input',\n    fieldName: 'operIp',\n    label: '操作IP',\n  },\n  {\n    component: 'Select',\n    componentProps: {\n      options: getDictOptions(DictEnum.SYS_COMMON_STATUS),\n    },\n    fieldName: 'status',\n    label: '状态',\n  },\n  {\n    component: 'RangePicker',\n    fieldName: 'createTime',\n    label: '操作时间',\n    componentProps: {\n      valueFormat: 'YYYY-MM-DD HH:mm:ss',\n    },\n  },\n];\n\nexport const columns: VxeGridProps['columns'] = [\n  { type: 'checkbox', width: 60 },\n  { field: 'title', title: '系统模块' },\n  {\n    title: '操作类型',\n    field: 'businessType',\n    slots: {\n      default: ({ row }) => {\n        return renderDict(row.businessType, DictEnum.SYS_OPER_TYPE);\n      },\n    },\n  },\n  { field: 'operName', title: '操作人员' },\n  { field: 'operIp', title: 'IP地址' },\n  { field: 'operLocation', title: 'IP信息' },\n  {\n    field: 'status',\n    title: '操作状态',\n    slots: {\n      default: ({ row }) => {\n        return renderDict(row.status, DictEnum.SYS_COMMON_STATUS);\n      },\n    },\n  },\n  { field: 'operTime', title: '操作日期', sortable: true },\n  {\n    field: 'costTime',\n    title: '操作耗时',\n    sortable: true,\n    formatter({ cellValue }) {\n      return `${cellValue} ms`;\n    },\n  },\n  {\n    field: 'action',\n    fixed: 'right',\n    slots: { default: 'action' },\n    title: '操作',\n    resizable: false,\n    width: 'auto',\n  },\n];\n"
  },
  {
    "path": "apps/web-antd/src/views/monitor/operlog/index.vue",
    "content": "<script setup lang=\"ts\">\nimport type { VbenFormProps } from '@vben/common-ui';\n\nimport type { VxeGridProps } from '#/adapter/vxe-table';\nimport type { PageQuery } from '#/api/common';\nimport type { OperationLog } from '#/api/monitor/operlog/model';\n\nimport { Page, useVbenDrawer } from '@vben/common-ui';\nimport { $t } from '@vben/locales';\n\nimport { Modal, Space } from 'ant-design-vue';\n\nimport {\n  addSortParams,\n  useVbenVxeGrid,\n  vxeCheckboxChecked,\n} from '#/adapter/vxe-table';\nimport {\n  operLogClean,\n  operLogDelete,\n  operLogExport,\n  operLogList,\n} from '#/api/monitor/operlog';\nimport { commonDownloadExcel } from '#/utils/file/download';\nimport { confirmDeleteModal } from '#/utils/modal';\n\nimport { columns, querySchema } from './data';\nimport operationPreviewDrawer from './operation-preview-drawer.vue';\n\nconst formOptions: VbenFormProps = {\n  commonConfig: {\n    labelWidth: 80,\n    componentProps: {\n      allowClear: true,\n    },\n  },\n  schema: querySchema(),\n  wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4',\n  // 日期选择格式化\n  fieldMappingTime: [\n    [\n      'createTime',\n      ['params[beginTime]', 'params[endTime]'],\n      ['YYYY-MM-DD 00:00:00', 'YYYY-MM-DD 23:59:59'],\n    ],\n  ],\n};\n\nconst gridOptions: VxeGridProps = {\n  checkboxConfig: {\n    // 高亮\n    highlight: true,\n    // 翻页时保留选中状态\n    reserve: true,\n    // 点击行选中\n    trigger: 'row',\n  },\n  columns,\n  height: 'auto',\n  keepSource: true,\n  pagerConfig: {},\n  proxyConfig: {\n    ajax: {\n      query: async ({ page, sorts }, formValues = {}) => {\n        const params: PageQuery = {\n          pageNum: page.currentPage,\n          pageSize: page.pageSize,\n          ...formValues,\n        };\n        // 添加排序参数\n        addSortParams(params, sorts);\n        return await operLogList(params);\n      },\n    },\n  },\n  rowConfig: {\n    keyField: 'operId',\n  },\n  sortConfig: {\n    // 远程排序\n    remote: true,\n    // 支持多字段排序 默认关闭\n    multiple: true,\n  },\n  id: 'monitor-operlog-index',\n};\n\nconst [BasicTable, tableApi] = useVbenVxeGrid({\n  formOptions,\n  gridOptions,\n  gridEvents: {\n    // 排序 重新请求接口\n    sortChange: () => tableApi.query(),\n  },\n});\n\nconst [OperationPreviewDrawer, drawerApi] = useVbenDrawer({\n  connectedComponent: operationPreviewDrawer,\n});\n\n/**\n * 预览\n * @param record 操作日志记录\n */\nfunction handlePreview(record: OperationLog) {\n  drawerApi.setData({ record });\n  drawerApi.open();\n}\n\n/**\n * 清空全部日志\n */\nfunction handleClear() {\n  confirmDeleteModal({\n    onValidated: async () => {\n      await operLogClean();\n      await tableApi.reload();\n    },\n  });\n}\n/**\n * 删除日志\n */\nasync function handleDelete() {\n  const rows = tableApi.grid.getCheckboxRecords();\n  const ids = rows.map((row: OperationLog) => row.operId);\n  Modal.confirm({\n    title: '提示',\n    okType: 'danger',\n    content: `确认删除选中的${ids.length}条操作日志吗？`,\n    onOk: async () => {\n      await operLogDelete(ids);\n      await tableApi.query();\n    },\n  });\n}\n\nfunction handleDownloadExcel() {\n  commonDownloadExcel(operLogExport, '操作日志', tableApi.formApi.form.values, {\n    fieldMappingTime: formOptions.fieldMappingTime,\n  });\n}\n</script>\n\n<template>\n  <Page :auto-content-height=\"true\">\n    <BasicTable table-title=\"操作日志列表\">\n      <template #toolbar-tools>\n        <Space>\n          <a-button\n            v-access:code=\"['monitor:operlog:remove']\"\n            @click=\"handleClear\"\n          >\n            {{ $t('pages.common.clear') }}\n          </a-button>\n          <a-button\n            v-access:code=\"['monitor:operlog:export']\"\n            @click=\"handleDownloadExcel\"\n          >\n            {{ $t('pages.common.export') }}\n          </a-button>\n          <a-button\n            :disabled=\"!vxeCheckboxChecked(tableApi)\"\n            danger\n            type=\"primary\"\n            v-access:code=\"['monitor:operlog:remove']\"\n            @click=\"handleDelete\"\n          >\n            {{ $t('pages.common.delete') }}\n          </a-button>\n        </Space>\n      </template>\n      <template #action=\"{ row }\">\n        <ghost-button\n          v-access:code=\"['monitor:operlog:list']\"\n          @click.stop=\"handlePreview(row)\"\n        >\n          {{ $t('pages.common.preview') }}\n        </ghost-button>\n      </template>\n    </BasicTable>\n    <OperationPreviewDrawer />\n  </Page>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/monitor/operlog/operation-preview-drawer.vue",
    "content": "<script setup lang=\"ts\">\nimport type { OperationLog } from '#/api/monitor/operlog/model';\n\nimport { computed, shallowRef } from 'vue';\n\nimport { useVbenDrawer } from '@vben/common-ui';\nimport { DictEnum } from '@vben/constants';\n\nimport { Descriptions, DescriptionsItem, Tag } from 'ant-design-vue';\n\nimport {\n  renderDict,\n  renderHttpMethodTag,\n  renderJsonPreview,\n} from '#/utils/render';\n\nconst [BasicDrawer, drawerApi] = useVbenDrawer({\n  onOpenChange: handleOpenChange,\n  onClosed() {\n    currentLog.value = null;\n  },\n});\n\nconst currentLog = shallowRef<null | OperationLog>(null);\nfunction handleOpenChange(open: boolean) {\n  if (!open) {\n    return null;\n  }\n  const { record } = drawerApi.getData() as { record: OperationLog };\n  currentLog.value = record;\n}\n\nconst actionInfo = computed(() => {\n  if (!currentLog.value) {\n    return '-';\n  }\n  const data = currentLog.value;\n  return `账号: ${data.operName} / ${data.deptName} / ${data.operIp} / ${data.operLocation}`;\n});\n</script>\n\n<template>\n  <BasicDrawer :footer=\"false\" class=\"w-[600px]\" title=\"查看日志\">\n    <Descriptions v-if=\"currentLog\" size=\"small\" bordered :column=\"1\">\n      <DescriptionsItem label=\"日志编号\" :label-style=\"{ minWidth: '120px' }\">\n        {{ currentLog.operId }}\n      </DescriptionsItem>\n      <DescriptionsItem label=\"操作结果\">\n        <component\n          :is=\"renderDict(currentLog.status, DictEnum.SYS_COMMON_STATUS)\"\n        />\n      </DescriptionsItem>\n      <DescriptionsItem label=\"操作模块\">\n        <div class=\"flex items-center\">\n          <Tag>{{ currentLog.title }}</Tag>\n          <component\n            :is=\"renderDict(currentLog.businessType, DictEnum.SYS_OPER_TYPE)\"\n          />\n        </div>\n      </DescriptionsItem>\n      <DescriptionsItem label=\"操作信息\">\n        {{ actionInfo }}\n      </DescriptionsItem>\n      <DescriptionsItem label=\"请求信息\">\n        <component :is=\"renderHttpMethodTag(currentLog.requestMethod)\" />\n        {{ currentLog.operUrl }}\n      </DescriptionsItem>\n      <DescriptionsItem v-if=\"currentLog.errorMsg\" label=\"异常信息\">\n        <span class=\"font-semibold text-red-600\">\n          {{ currentLog.errorMsg }}\n        </span>\n      </DescriptionsItem>\n      <DescriptionsItem label=\"方法\">\n        {{ currentLog.method }}\n      </DescriptionsItem>\n      <DescriptionsItem label=\"请求参数\">\n        <div class=\"max-h-[300px] overflow-y-auto\">\n          <component :is=\"renderJsonPreview(currentLog.operParam)\" />\n        </div>\n      </DescriptionsItem>\n      <DescriptionsItem v-if=\"currentLog.jsonResult\" label=\"响应参数\">\n        <div class=\"max-h-[300px] overflow-y-auto\">\n          <component :is=\"renderJsonPreview(currentLog.jsonResult)\" />\n        </div>\n      </DescriptionsItem>\n      <DescriptionsItem label=\"请求耗时\">\n        {{ `${currentLog.costTime} ms` }}\n      </DescriptionsItem>\n      <DescriptionsItem label=\"操作时间\">\n        {{ `${currentLog.operTime}` }}\n      </DescriptionsItem>\n    </Descriptions>\n  </BasicDrawer>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/monitor/snailjob/index.vue",
    "content": "<!-- 建议通过菜单配置成外链/内嵌 达到相同的效果且灵活性更高 -->\n<template>\n  <iframe class=\"size-full\" src=\"http://localhost:8800/snail-job\"></iframe>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/nodeManage/data.ts",
    "content": "import type { VxeGridProps } from '#/adapter/vxe-table';\nimport { h } from 'vue';\nimport { Tag } from 'ant-design-vue';\nexport const columns: VxeGridProps['columns'] = [\n    { type: 'checkbox', width: 60 },\n    {\n        title: '组件名称',\n        field: 'name',\n        minWidth: 180,\n    },\n    {\n        title: '组件标题',\n        field: 'title',\n        minWidth: 180,\n    },\n    {\n        title: '是否启用',\n        field: 'isEnable',\n        slots: {\n            default: ({ row }) => {\n                if (row.isEnable) {\n                    return h(Tag, { color: 'green' }, () => '是');\n                }\n                return h(Tag, { color: 'default' }, () => '否');\n            },\n        },\n    },\n    {\n        field: 'action',\n        fixed: 'right',\n        slots: { default: 'action' },\n        title: '操作',\n        width: 320,\n    },\n];"
  },
  {
    "path": "apps/web-antd/src/views/nodeManage/index.d.ts",
    "content": "export interface NodeInfo {\n  uuid: string\n  name:String,\n  title: string,\n  remark: string,\n  isEnable: Boolean,\n}"
  },
  {
    "path": "apps/web-antd/src/views/nodeManage/index.vue",
    "content": "<script setup lang=\"ts\">\nimport { Page, useVbenModal } from '@vben/common-ui';\nimport type { VbenFormProps } from '@vben/common-ui';\nimport type { VxeGridProps } from '#/adapter/vxe-table';\nimport { useVbenVxeGrid, vxeCheckboxChecked } from '#/adapter/vxe-table';\nimport { columns } from './data';\nimport type { NodeInfo } from './index.d';\nimport { workflowApi } from '#/api/aiflow';\nimport { Modal, Popconfirm, Space, Button } from 'ant-design-vue';\nimport AddModal from './modal.vue';\n// const formOptions: VbenFormProps = {\n//   commonConfig: {\n//     labelWidth: 80,\n//     componentProps: {\n//       allowClear: true,\n//     },\n//   },\n//   //schema: querySchema(),\n//   wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4',\n// };\n\nconst [AddNodeModal, modalApi] = useVbenModal({\n  connectedComponent: AddModal,\n});\nconst gridOptions: VxeGridProps = {\n  checkboxConfig: {\n    highlight: true,\n    reserve: true,\n  },\n  columns,\n  height: 'auto',\n  keepSource: true,\n  pagerConfig: {},\n  proxyConfig: {\n    ajax: {\n      query: async ({ page }, formValues = {}) => {\n        const result = await workflowApi.workflowComponents({\n          currentPage: page.currentPage,\n          pageSize: page.pageSize,\n          wfSearchReq: {\n            ...formValues,\n          },\n        });\n        // 转换数据结构以匹配 VXE 表格期望的格式\n        return {\n          rows: result,\n          total: result.total ? result.total : 0,\n        };\n      },\n    },\n  },\n  rowConfig: {\n    keyField: 'uuid',\n  },\n  id: 'workflow-index',\n};\nconst [BasicTable, tableApi] = useVbenVxeGrid({\n  // formOptions,\n  gridOptions,\n});\n\n// 新建工作流\nfunction handleAdd() {\n  console.log('modelApi', modalApi);\n  modalApi.setData({});\n  modalApi.open();\n}\n\n// 编辑工作流基本信息\nfunction handleEditInfo(record: NodeInfo) {\n  modalApi.setData({\n    uuid: record.uuid,\n    name: record.name,\n    title: record.title,\n    remark: record.remark,\n    isEnable: record.isEnable,\n  });\n  modalApi.open();\n}\n</script>\n<template>\n  <Page :auto-content-height=\"true\">\n    <BasicTable table-title=\"节点列表\">\n      <template #toolbar-tools>\n        <Space>\n          <Button type=\"primary\" @click=\"handleAdd\"> 新建节点 </Button>\n        </Space>\n      </template>\n      <template #action=\"{ row }\">\n        <Space>\n          <ghost-button @click.stop=\"handleEditInfo(row)\"> 编辑 </ghost-button>\n        </Space></template\n      >\n    </BasicTable>\n    <AddNodeModal @reload=\"tableApi.query()\" />\n  </Page>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/nodeManage/modal.vue",
    "content": "\n\n<script setup lang=\"ts\">\nimport { computed, ref } from 'vue';\nimport { Form, Input, Switch, message } from 'ant-design-vue';\nimport { useVbenModal } from '@vben/common-ui';\n\nimport { workflowApi } from '#/api/aiflow';\nconst defaultValues = {\n  uuid: '',\n  name: '',\n  title: '',\n  remark:'',\n  isEnable: true,\n};\nconst formData = ref({ ...defaultValues });\nconst isEdit = ref(false);\nconst title = '新增节点';\nconst emit = defineEmits<{\n  (e: 'reload', result: any): void;\n}>();\n\nconst [BasicModal, modalApi] = useVbenModal({\n  class: 'w-[550px]',\n  fullscreenButton: false,\n  closeOnClickModal: false,\n  onClosed: handleCancel,\n  onConfirm: handleConfirm,\n  onOpenChange: async (isOpen) => {\n    if (!isOpen) {\n      return null;\n    }\n\n    const data = modalApi.getData() as {\n      uuid?: string;\n      name?: string;\n      title?: string;\n      remark?: string;\n      isEnable?: boolean;\n    };\n    isEdit.value = !!data?.uuid;\n\n    if (data) {\n      formData.value = {\n        uuid: data.uuid || '',\n        name: data.name || '',\n        title: data.title || '',\n        remark: data.remark || '',\n        isEnable: data.isEnable || true,\n      };\n    } else {\n      formData.value = { ...defaultValues };\n    }\n  },\n});\n\nasync function handleConfirm() {\n  try {\n    modalApi.modalLoading(true);\n\n    let result;\n    if (isEdit.value) {\n      // 编辑模式：更新基本信息\n      result = await workflowApi.addNode({\n        uuid: formData.value.uuid,\n        name: formData.value.name.trim(),\n        title: formData.value.title.trim(),\n        remark: formData.value.remark.trim(),\n        isEnable: formData.value.isEnable,\n      });\n    } else {\n      // 新建模式：创建新工作流\n      result = await workflowApi.addNode({\n        name: formData.value.name.trim(),\n        title: formData.value.title.trim(),\n        remark: formData.value.remark.trim(),\n        isEnable: formData.value.isEnable,\n      });\n    }\n\n    message.success(isEdit.value ? '更新成功' : '创建成功');\n    emit('reload', result);\n    await handleCancel();\n  } catch (error: any) {\n    message.error(error.message || (isEdit.value ? '更新失败' : '创建失败'));\n  } finally {\n    modalApi.modalLoading(false);\n  }\n}\nasync function handleCancel() {\n  modalApi.close();\n  formData.value = { ...defaultValues };\n}\n</script>\n\n<template>\n  <BasicModal :title=\"title\"> \n      <Form\n      :model=\"formData\"\n      :label-col=\"{ span: 5 }\"\n      :wrapper-col=\"{ span: 19 }\"\n    >\n      <Form.Item label=\"组件名称\" required>\n        <Input\n          v-model:value=\"formData.name\"\n          placeholder=\"请输入组件名称\"\n          :maxlength=\"100\"\n          show-count\n        />\n      </Form.Item>\n       <Form.Item label=\"组件标题\" required>\n        <Input\n          v-model:value=\"formData.title\"\n          placeholder=\"请输入组件标题\"\n          :maxlength=\"100\"\n          show-count\n        />\n      </Form.Item>\n          <Form.Item label=\"备注\" required>\n        <Input\n          v-model:value=\"formData.remark\"\n          placeholder=\"请输入备注\"\n          :maxlength=\"100\"\n          show-count\n        />\n      </Form.Item>\n       <Form.Item label=\"是否启用\">\n        <div class=\"flex items-center\">\n          <Switch v-model:checked=\"formData.isEnable\" />\n          \n        </div>\n      </Form.Item>\n      </Form>\n  </BasicModal>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/system/client/client-drawer.vue",
    "content": "<script setup lang=\"ts\">\nimport { computed, ref } from 'vue';\n\nimport { useVbenDrawer } from '@vben/common-ui';\nimport { $t } from '@vben/locales';\nimport { cloneDeep } from '@vben/utils';\n\nimport { useVbenForm } from '#/adapter/form';\nimport { clientAdd, clientInfo, clientUpdate } from '#/api/system/client';\nimport { defaultFormValueGetter, useBeforeCloseDiff } from '#/utils/popup';\n\nimport { drawerSchema } from './data';\nimport SecretInput from './secret-input.vue';\n\nconst emit = defineEmits<{ reload: [] }>();\n\nconst isUpdate = ref(false);\nconst title = computed(() => {\n  return isUpdate.value ? $t('pages.common.edit') : $t('pages.common.add');\n});\n\nconst [BasicForm, formApi] = useVbenForm({\n  commonConfig: {\n    formItemClass: 'col-span-2',\n    componentProps: {\n      class: 'w-full',\n    },\n  },\n  layout: 'vertical',\n  schema: drawerSchema(),\n  showDefaultActions: false,\n  wrapperClass: 'grid-cols-2 gap-x-4',\n});\n\nfunction setupForm(update: boolean) {\n  formApi.updateSchema([\n    {\n      dependencies: {\n        show: () => update,\n        triggerFields: [''],\n      },\n      fieldName: 'clientId',\n    },\n    {\n      componentProps: {\n        disabled: update,\n      },\n      fieldName: 'clientKey',\n    },\n    {\n      componentProps: {\n        disabled: update,\n      },\n      fieldName: 'clientSecret',\n    },\n  ]);\n}\n\nconst { onBeforeClose, markInitialized, resetInitialized } = useBeforeCloseDiff(\n  {\n    initializedGetter: defaultFormValueGetter(formApi),\n    currentGetter: defaultFormValueGetter(formApi),\n  },\n);\n\n// 提取生成状态字段Schema的函数\nconst getStatusSchema = (disabled: boolean) => [\n  {\n    componentProps: { disabled },\n    fieldName: 'status',\n  },\n];\n\nconst [BasicDrawer, drawerApi] = useVbenDrawer({\n  onBeforeClose,\n  onClosed: handleClosed,\n  onConfirm: handleConfirm,\n  async onOpenChange(isOpen) {\n    if (!isOpen) {\n      return null;\n    }\n    drawerApi.drawerLoading(true);\n\n    const { id } = drawerApi.getData() as { id?: number | string };\n    isUpdate.value = !!id;\n    // 初始化\n    setupForm(isUpdate.value);\n    if (isUpdate.value && id) {\n      const record = await clientInfo(id);\n      // 不能禁用id为1的记录\n      formApi.updateSchema(getStatusSchema(record.id === 1));\n      await formApi.setValues(record);\n    } else {\n      // 新增模式: 确保状态字段可用\n      formApi.updateSchema(getStatusSchema(false));\n    }\n    await markInitialized();\n\n    drawerApi.drawerLoading(false);\n  },\n});\n\nasync function handleConfirm() {\n  try {\n    drawerApi.lock(true);\n    const { valid } = await formApi.validate();\n    if (!valid) {\n      return;\n    }\n    const data = cloneDeep(await formApi.getValues());\n    await (isUpdate.value ? clientUpdate(data) : clientAdd(data));\n    resetInitialized();\n    emit('reload');\n    drawerApi.close();\n  } catch (error) {\n    console.error(error);\n  } finally {\n    drawerApi.lock(false);\n  }\n}\n\nasync function handleClosed() {\n  await formApi.resetForm();\n  resetInitialized();\n}\n</script>\n\n<template>\n  <BasicDrawer :title=\"title\" class=\"w-[600px]\">\n    <BasicForm>\n      <template #clientSecret=\"slotProps\">\n        <SecretInput v-bind=\"slotProps\" :disabled=\"isUpdate\" />\n      </template>\n    </BasicForm>\n  </BasicDrawer>\n</template>\n\n<style lang=\"scss\" scoped>\n/**\n自定义组件校验失败样式\n*/\n:deep(.form-valid-error .ant-input[name='clientSecret']) {\n  border-color: hsl(var(--destructive));\n  box-shadow: 0 0 0 2px rgb(255 38 5 / 6%);\n}\n</style>\n"
  },
  {
    "path": "apps/web-antd/src/views/system/client/data.tsx",
    "content": "import type { FormSchemaGetter } from '#/adapter/form';\nimport type { VxeGridProps } from '#/adapter/vxe-table';\n\nimport { DictEnum } from '@vben/constants';\nimport { getPopupContainer } from '@vben/utils';\n\nimport { getDictOptions } from '#/utils/dict';\nimport { renderDict, renderDictTags } from '#/utils/render';\n\nexport const querySchema: FormSchemaGetter = () => [\n  {\n    component: 'Input',\n    fieldName: 'clientKey',\n    label: '客户端key',\n  },\n  {\n    component: 'Input',\n    fieldName: 'clientSecret',\n    label: '客户端密钥',\n  },\n  {\n    component: 'Select',\n    componentProps: {\n      options: getDictOptions(DictEnum.SYS_NORMAL_DISABLE),\n    },\n    fieldName: 'status',\n    label: '状态',\n  },\n];\n\nexport const columns: VxeGridProps['columns'] = [\n  { type: 'checkbox', width: 60 },\n  {\n    title: '客户端ID',\n    field: 'clientId',\n    showOverflow: true,\n  },\n  {\n    title: '客户端key',\n    field: 'clientKey',\n  },\n  {\n    title: '客户端密钥',\n    field: 'clientSecret',\n  },\n  {\n    title: '授权类型',\n    field: 'grantTypeList',\n    slots: {\n      default: ({ row }) => {\n        if (!row.grantTypeList) {\n          return '无';\n        }\n        return renderDictTags(\n          row.grantTypeList,\n          getDictOptions(DictEnum.SYS_GRANT_TYPE),\n          true,\n          4,\n        );\n      },\n    },\n  },\n  {\n    title: '设备类型',\n    field: 'deviceType',\n    slots: {\n      default: ({ row }) => {\n        return renderDict(row.deviceType, DictEnum.SYS_DEVICE_TYPE);\n      },\n    },\n  },\n  {\n    title: 'token活跃时间',\n    field: 'activeTimeout',\n    formatter({ row }) {\n      return `${row.activeTimeout}秒`;\n    },\n  },\n  {\n    title: 'token超时时间',\n    field: 'timeout',\n    formatter({ row }) {\n      return `${row.timeout}秒`;\n    },\n  },\n  {\n    title: '状态',\n    field: 'status',\n    slots: {\n      default: 'status',\n    },\n  },\n  {\n    field: 'action',\n    fixed: 'right',\n    slots: { default: 'action' },\n    title: '操作',\n    resizable: false,\n    width: 'auto',\n  },\n];\n\nexport const drawerSchema: FormSchemaGetter = () => [\n  {\n    component: 'Input',\n    dependencies: {\n      show: () => false,\n      triggerFields: [''],\n    },\n    fieldName: 'id',\n    label: 'id',\n  },\n  {\n    component: 'Input',\n    componentProps: {\n      disabled: true,\n    },\n    dependencies: {\n      show: () => false,\n      triggerFields: [''],\n    },\n    fieldName: 'clientId',\n    label: '客户端ID',\n  },\n  {\n    component: 'Input',\n    fieldName: 'clientKey',\n    label: '客户端key',\n    rules: 'required',\n  },\n  {\n    component: 'Input',\n    fieldName: 'clientSecret',\n    label: '客户端密钥',\n    rules: 'required',\n  },\n  {\n    component: 'Select',\n    componentProps: {\n      getPopupContainer,\n      mode: 'multiple',\n      optionFilterProp: 'label',\n      options: getDictOptions(DictEnum.SYS_GRANT_TYPE),\n    },\n    fieldName: 'grantTypeList',\n    label: '授权类型',\n    rules: 'selectRequired',\n  },\n  {\n    component: 'Select',\n    componentProps: {\n      allowClear: false,\n      getPopupContainer,\n      options: getDictOptions(DictEnum.SYS_DEVICE_TYPE),\n    },\n    fieldName: 'deviceType',\n    label: '设备类型',\n    rules: 'selectRequired',\n  },\n  {\n    component: 'InputNumber',\n    componentProps: {\n      addonAfter: '秒',\n      placeholder: '请输入',\n    },\n    defaultValue: 1800,\n    fieldName: 'activeTimeout',\n    formItemClass: 'col-span-2 lg:col-span-1',\n    help: '指定时间无操作则过期(单位：秒), 默认30分钟(1800秒)',\n    label: 'Token活跃超时时间',\n    rules: 'required',\n  },\n  {\n    component: 'InputNumber',\n    componentProps: {\n      addonAfter: '秒',\n    },\n    defaultValue: 604_800,\n    fieldName: 'timeout',\n    formItemClass: 'col-span-2 lg:col-span-1 ',\n    help: '指定时间必定过期(单位：秒)，默认七天(604800秒)',\n    label: 'Token固定超时时间',\n    rules: 'required',\n  },\n  {\n    component: 'RadioGroup',\n    componentProps: {\n      buttonStyle: 'solid',\n      options: getDictOptions(DictEnum.SYS_NORMAL_DISABLE),\n      optionType: 'button',\n    },\n    defaultValue: '0',\n    fieldName: 'status',\n    label: '状态',\n  },\n];\n"
  },
  {
    "path": "apps/web-antd/src/views/system/client/index.vue",
    "content": "<script setup lang=\"ts\">\nimport type { VbenFormProps } from '@vben/common-ui';\n\nimport type { VxeGridProps } from '#/adapter/vxe-table';\nimport type { Client } from '#/api/system/client/model';\n\nimport { useAccess } from '@vben/access';\nimport { Page, useVbenDrawer } from '@vben/common-ui';\nimport { getVxePopupContainer } from '@vben/utils';\n\nimport { Modal, Popconfirm, Space } from 'ant-design-vue';\n\nimport { useVbenVxeGrid, vxeCheckboxChecked } from '#/adapter/vxe-table';\nimport {\n  clientChangeStatus,\n  clientExport,\n  clientList,\n  clientRemove,\n} from '#/api/system/client';\nimport { TableSwitch } from '#/components/table';\nimport { commonDownloadExcel } from '#/utils/file/download';\n\nimport clientDrawer from './client-drawer.vue';\nimport { columns, querySchema } from './data';\n\nconst formOptions: VbenFormProps = {\n  commonConfig: {\n    labelWidth: 80,\n    componentProps: {\n      allowClear: true,\n    },\n  },\n  schema: querySchema(),\n  wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4',\n};\n\nconst gridOptions: VxeGridProps = {\n  checkboxConfig: {\n    // 高亮\n    highlight: true,\n    // 翻页时保留选中状态\n    reserve: true,\n    // 点击行选中\n    // trigger: 'row',\n    checkMethod: ({ row }) => (row as Client)?.id !== 1,\n  },\n  columns,\n  height: 'auto',\n  keepSource: true,\n  pagerConfig: {},\n  proxyConfig: {\n    ajax: {\n      query: async ({ page }, formValues = {}) => {\n        return await clientList({\n          pageNum: page.currentPage,\n          pageSize: page.pageSize,\n          ...formValues,\n        });\n      },\n    },\n  },\n  rowConfig: {\n    keyField: 'id',\n  },\n  id: 'system-client-index',\n  showOverflow: false,\n};\n\nconst [BasicTable, tableApi] = useVbenVxeGrid({\n  formOptions,\n  gridOptions,\n});\n\nconst [ClientDrawer, drawerApi] = useVbenDrawer({\n  connectedComponent: clientDrawer,\n});\n\nfunction handleAdd() {\n  drawerApi.setData({});\n  drawerApi.open();\n}\n\nasync function handleEdit(record: Client) {\n  drawerApi.setData({ id: record.id });\n  drawerApi.open();\n}\n\nasync function handleDelete(row: Client) {\n  await clientRemove([row.id]);\n  await tableApi.query();\n}\n\nfunction handleMultiDelete() {\n  const rows = tableApi.grid.getCheckboxRecords();\n  const ids = rows.map((row: Client) => row.id);\n  Modal.confirm({\n    title: '提示',\n    okType: 'danger',\n    content: `确认删除选中的${ids.length}条记录吗？`,\n    onOk: async () => {\n      await clientRemove(ids);\n      await tableApi.query();\n    },\n  });\n}\n\nfunction handleDownloadExcel() {\n  commonDownloadExcel(clientExport, '客户端数据', tableApi.formApi.form.values);\n}\n\nconst { hasAccessByCodes } = useAccess();\n</script>\n\n<template>\n  <Page :auto-content-height=\"true\">\n    <BasicTable table-title=\"客户端列表\">\n      <template #toolbar-tools>\n        <Space>\n          <a-button\n            v-access:code=\"['system:client:export']\"\n            @click=\"handleDownloadExcel\"\n          >\n            {{ $t('pages.common.export') }}\n          </a-button>\n          <a-button\n            :disabled=\"!vxeCheckboxChecked(tableApi)\"\n            danger\n            type=\"primary\"\n            v-access:code=\"['system:client:remove']\"\n            @click=\"handleMultiDelete\"\n          >\n            {{ $t('pages.common.delete') }}\n          </a-button>\n          <a-button\n            type=\"primary\"\n            v-access:code=\"['system:client:add']\"\n            @click=\"handleAdd\"\n          >\n            {{ $t('pages.common.add') }}\n          </a-button>\n        </Space>\n      </template>\n      <template #status=\"{ row }\">\n        <!-- pc不允许禁用 禁用了直接登录不了 应该设置disabled -->\n        <!-- 登录提示: 认证权限类型已禁用 -->\n        <TableSwitch\n          v-model:value=\"row.status\"\n          :api=\"() => clientChangeStatus(row)\"\n          :disabled=\"row.id === 1 || !hasAccessByCodes(['system:client:edit'])\"\n          @reload=\"tableApi.query()\"\n        />\n      </template>\n      <template #action=\"{ row }\">\n        <Space>\n          <ghost-button\n            v-access:code=\"['system:client:edit']\"\n            @click.stop=\"handleEdit(row)\"\n          >\n            {{ $t('pages.common.edit') }}\n          </ghost-button>\n          <Popconfirm\n            :disabled=\"row.id === 1\"\n            :get-popup-container=\"getVxePopupContainer\"\n            placement=\"left\"\n            title=\"确认删除？\"\n            @confirm=\"handleDelete(row)\"\n          >\n            <ghost-button\n              :disabled=\"row.id === 1\"\n              danger\n              v-access:code=\"['system:client:remove']\"\n              @click.stop=\"\"\n            >\n              {{ $t('pages.common.delete') }}\n            </ghost-button>\n          </Popconfirm>\n        </Space>\n      </template>\n    </BasicTable>\n    <ClientDrawer @reload=\"tableApi.query()\" />\n  </Page>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/system/client/secret-input.vue",
    "content": "<script setup lang=\"ts\">\nimport { IconifyIcon } from '@vben/icons';\nimport { buildUUID } from '@vben/utils';\n\nimport { Input } from 'ant-design-vue';\n\ndefineOptions({ name: 'SecretInput' });\n\nwithDefaults(defineProps<{ disabled?: boolean; placeholder?: string }>(), {\n  disabled: false,\n  placeholder: '请输入密钥或随机生成',\n});\n\nconst value = defineModel<string>('value', {\n  required: false,\n});\n\nfunction refreshSecret() {\n  value.value = buildUUID();\n}\n\n/**\n * 万一要在每次新增时打开Drawer刷新\n * 需要调用实例方法\n */\ndefineExpose({ refreshSecret });\n</script>\n\n<template>\n  <Input v-model:value=\"value\" :disabled=\"disabled\" :placeholder=\"placeholder\">\n    <template v-if=\"!disabled\" #addonAfter>\n      <a-button type=\"primary\" @click=\"refreshSecret\">\n        <div class=\"flex items-center gap-[4px]\">\n          <IconifyIcon icon=\"charm:refresh\" />\n          <span>随机生成</span>\n        </div>\n      </a-button>\n    </template>\n  </Input>\n</template>\n\n<style lang=\"scss\" scoped>\n:deep(.ant-input-group-addon) {\n  padding: 0;\n  border: none;\n}\n\n:deep(.ant-btn-primary) {\n  border-top-left-radius: 0;\n  border-bottom-left-radius: 0;\n}\n</style>\n"
  },
  {
    "path": "apps/web-antd/src/views/system/config/config-modal.vue",
    "content": "<script setup lang=\"ts\">\nimport { computed, ref } from 'vue';\n\nimport { useVbenModal } from '@vben/common-ui';\nimport { $t } from '@vben/locales';\nimport { cloneDeep } from '@vben/utils';\n\nimport { useVbenForm } from '#/adapter/form';\nimport { configAdd, configInfo, configUpdate } from '#/api/system/config';\nimport { defaultFormValueGetter, useBeforeCloseDiff } from '#/utils/popup';\n\nimport { modalSchema } from './data';\n\nconst emit = defineEmits<{ reload: [] }>();\n\nconst isUpdate = ref(false);\nconst title = computed(() => {\n  return isUpdate.value ? $t('pages.common.edit') : $t('pages.common.add');\n});\n\nconst [BasicForm, formApi] = useVbenForm({\n  commonConfig: {\n    labelWidth: 80,\n  },\n  schema: modalSchema(),\n  showDefaultActions: false,\n});\n\nconst { onBeforeClose, markInitialized, resetInitialized } = useBeforeCloseDiff(\n  {\n    initializedGetter: defaultFormValueGetter(formApi),\n    currentGetter: defaultFormValueGetter(formApi),\n  },\n);\n\nconst [BasicModal, modalApi] = useVbenModal({\n  fullscreenButton: false,\n  onBeforeClose,\n  onClosed: handleClosed,\n  onConfirm: handleConfirm,\n  onOpenChange: async (isOpen) => {\n    if (!isOpen) {\n      return null;\n    }\n    modalApi.modalLoading(true);\n\n    const { id } = modalApi.getData() as { id?: number | string };\n    isUpdate.value = !!id;\n\n    if (isUpdate.value && id) {\n      const record = await configInfo(id);\n      await formApi.setValues(record);\n    }\n    await markInitialized();\n\n    modalApi.modalLoading(false);\n  },\n});\n\nasync function handleConfirm() {\n  try {\n    modalApi.lock(true);\n    const { valid } = await formApi.validate();\n    if (!valid) {\n      return;\n    }\n    const data = cloneDeep(await formApi.getValues());\n    await (isUpdate.value ? configUpdate(data) : configAdd(data));\n    resetInitialized();\n    emit('reload');\n    modalApi.close();\n  } catch (error) {\n    console.error(error);\n  } finally {\n    modalApi.lock(false);\n  }\n}\n\nasync function handleClosed() {\n  await formApi.resetForm();\n  resetInitialized();\n}\n</script>\n\n<template>\n  <BasicModal :title=\"title\" class=\"w-[550px]\">\n    <BasicForm />\n  </BasicModal>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/system/config/data.ts",
    "content": "import type { FormSchemaGetter } from '#/adapter/form';\nimport type { VxeGridProps } from '#/adapter/vxe-table';\n\nimport { DictEnum } from '@vben/constants';\nimport { getPopupContainer } from '@vben/utils';\n\nimport { getDictOptions } from '#/utils/dict';\nimport { renderDict } from '#/utils/render';\n\nexport const querySchema: FormSchemaGetter = () => [\n  {\n    component: 'Input',\n    fieldName: 'configName',\n    label: '参数名称',\n  },\n  {\n    component: 'Input',\n    fieldName: 'configKey',\n    label: '参数键名',\n  },\n  {\n    component: 'Select',\n    componentProps: {\n      getPopupContainer,\n      options: getDictOptions(DictEnum.SYS_YES_NO),\n    },\n    fieldName: 'configType',\n    label: '系统内置',\n  },\n  {\n    component: 'RangePicker',\n    fieldName: 'createTime',\n    label: '创建时间',\n  },\n];\n\nexport const columns: VxeGridProps['columns'] = [\n  { type: 'checkbox', width: 60 },\n  {\n    title: '参数名称',\n    field: 'configName',\n  },\n  {\n    title: '参数KEY',\n    field: 'configKey',\n  },\n  {\n    title: '参数Value',\n    field: 'configValue',\n  },\n  {\n    title: '系统内置',\n    field: 'configType',\n    width: 120,\n    slots: {\n      default: ({ row }) => {\n        return renderDict(row.configType, DictEnum.SYS_YES_NO);\n      },\n    },\n  },\n  {\n    title: '备注',\n    field: 'remark',\n  },\n  {\n    title: '创建时间',\n    field: 'createTime',\n  },\n  {\n    field: 'action',\n    fixed: 'right',\n    slots: { default: 'action' },\n    title: '操作',\n    resizable: false,\n    width: 'auto',\n  },\n];\n\nexport const modalSchema: FormSchemaGetter = () => [\n  {\n    component: 'Input',\n    dependencies: {\n      show: () => false,\n      triggerFields: [''],\n    },\n    fieldName: 'configId',\n    label: '参数主键',\n  },\n  {\n    component: 'Input',\n    fieldName: 'configName',\n    label: '参数名称',\n    rules: 'required',\n  },\n  {\n    component: 'Input',\n    fieldName: 'configKey',\n    label: '参数键名',\n    rules: 'required',\n  },\n  {\n    component: 'Textarea',\n    formItemClass: 'items-start',\n    fieldName: 'configValue',\n    label: '参数键值',\n    componentProps: {\n      autoSize: true,\n    },\n    rules: 'required',\n  },\n  {\n    component: 'RadioGroup',\n    componentProps: {\n      buttonStyle: 'solid',\n      options: getDictOptions(DictEnum.SYS_YES_NO),\n      optionType: 'button',\n    },\n    defaultValue: 'N',\n    fieldName: 'configType',\n    label: '是否内置',\n    rules: 'required',\n  },\n  {\n    component: 'Textarea',\n    fieldName: 'remark',\n    formItemClass: 'items-start',\n    label: '备注',\n  },\n];\n"
  },
  {
    "path": "apps/web-antd/src/views/system/config/index.vue",
    "content": "<script setup lang=\"ts\">\nimport type { VbenFormProps } from '@vben/common-ui';\n\nimport type { VxeGridProps } from '#/adapter/vxe-table';\nimport type { SysConfig } from '#/api/system/config/model';\n\nimport { Page, useVbenModal } from '@vben/common-ui';\nimport { getVxePopupContainer } from '@vben/utils';\n\nimport { Modal, Popconfirm, Space } from 'ant-design-vue';\n\nimport { useVbenVxeGrid, vxeCheckboxChecked } from '#/adapter/vxe-table';\nimport {\n  configExport,\n  configList,\n  configRefreshCache,\n  configRemove,\n} from '#/api/system/config';\nimport { commonDownloadExcel } from '#/utils/file/download';\n\nimport configModal from './config-modal.vue';\nimport { columns, querySchema } from './data';\n\nconst formOptions: VbenFormProps = {\n  commonConfig: {\n    labelWidth: 80,\n    componentProps: {\n      allowClear: true,\n    },\n  },\n  schema: querySchema(),\n  wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4',\n  // 日期选择格式化\n  fieldMappingTime: [\n    [\n      'createTime',\n      ['params[beginTime]', 'params[endTime]'],\n      ['YYYY-MM-DD 00:00:00', 'YYYY-MM-DD 23:59:59'],\n    ],\n  ],\n};\n\nconst gridOptions: VxeGridProps = {\n  checkboxConfig: {\n    // 高亮\n    highlight: true,\n    // 翻页时保留选中状态\n    reserve: true,\n  },\n  columns,\n  height: 'auto',\n  keepSource: true,\n  pagerConfig: {},\n  proxyConfig: {\n    ajax: {\n      query: async ({ page }, formValues = {}) => {\n        return await configList({\n          pageNum: page.currentPage,\n          pageSize: page.pageSize,\n          ...formValues,\n        });\n      },\n    },\n  },\n  rowConfig: {\n    keyField: 'configId',\n  },\n  id: 'system-config-index',\n};\n\nconst [BasicTable, tableApi] = useVbenVxeGrid({\n  formOptions,\n  gridOptions,\n});\nconst [ConfigModal, modalApi] = useVbenModal({\n  connectedComponent: configModal,\n});\n\nfunction handleAdd() {\n  modalApi.setData({});\n  modalApi.open();\n}\n\nasync function handleEdit(record: SysConfig) {\n  modalApi.setData({ id: record.configId });\n  modalApi.open();\n}\n\nasync function handleDelete(row: SysConfig) {\n  await configRemove([row.configId]);\n  await tableApi.query();\n}\n\nfunction handleMultiDelete() {\n  const rows = tableApi.grid.getCheckboxRecords();\n  const ids = rows.map((row: SysConfig) => row.configId);\n  Modal.confirm({\n    title: '提示',\n    okType: 'danger',\n    content: `确认删除选中的${ids.length}条记录吗？`,\n    onOk: async () => {\n      await configRemove(ids);\n      await tableApi.query();\n    },\n  });\n}\n\nfunction handleDownloadExcel() {\n  commonDownloadExcel(configExport, '参数配置', tableApi.formApi.form.values, {\n    fieldMappingTime: formOptions.fieldMappingTime,\n  });\n}\n\nasync function handleRefreshCache() {\n  await configRefreshCache();\n  await tableApi.query();\n}\n</script>\n\n<template>\n  <Page :auto-content-height=\"true\">\n    <BasicTable table-title=\"参数列表\">\n      <template #toolbar-tools>\n        <Space>\n          <a-button @click=\"handleRefreshCache\"> 刷新缓存 </a-button>\n          <a-button\n            v-access:code=\"['system:config:export']\"\n            @click=\"handleDownloadExcel\"\n          >\n            {{ $t('pages.common.export') }}\n          </a-button>\n          <a-button\n            :disabled=\"!vxeCheckboxChecked(tableApi)\"\n            danger\n            type=\"primary\"\n            v-access:code=\"['system:config:remove']\"\n            @click=\"handleMultiDelete\"\n          >\n            {{ $t('pages.common.delete') }}\n          </a-button>\n          <a-button\n            type=\"primary\"\n            v-access:code=\"['system:config:add']\"\n            @click=\"handleAdd\"\n          >\n            {{ $t('pages.common.add') }}\n          </a-button>\n        </Space>\n      </template>\n      <template #action=\"{ row }\">\n        <Space>\n          <ghost-button\n            v-access:code=\"['system:config:edit']\"\n            @click.stop=\"handleEdit(row)\"\n          >\n            {{ $t('pages.common.edit') }}\n          </ghost-button>\n          <Popconfirm\n            :get-popup-container=\"getVxePopupContainer\"\n            placement=\"left\"\n            title=\"确认删除？\"\n            @confirm=\"handleDelete(row)\"\n          >\n            <ghost-button\n              danger\n              v-access:code=\"['system:config:remove']\"\n              @click.stop=\"\"\n            >\n              {{ $t('pages.common.delete') }}\n            </ghost-button>\n          </Popconfirm>\n        </Space>\n      </template>\n    </BasicTable>\n    <ConfigModal @reload=\"tableApi.query()\" />\n  </Page>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/system/dept/data.ts",
    "content": "import type { FormSchemaGetter } from '#/adapter/form';\nimport type { VxeGridProps } from '#/adapter/vxe-table';\n\nimport { DictEnum } from '@vben/constants';\nimport { getPopupContainer } from '@vben/utils';\n\nimport { z } from '#/adapter/form';\nimport { getDictOptions } from '#/utils/dict';\nimport { renderDict } from '#/utils/render';\n\nexport const querySchema: FormSchemaGetter = () => [\n  {\n    component: 'Input',\n    fieldName: 'deptName',\n    label: '部门名称',\n  },\n  {\n    component: 'Select',\n    componentProps: {\n      getPopupContainer,\n      options: getDictOptions(DictEnum.SYS_NORMAL_DISABLE),\n    },\n    fieldName: 'status',\n    label: '部门状态',\n  },\n];\n\nexport const columns: VxeGridProps['columns'] = [\n  {\n    field: 'deptName',\n    title: '部门名称',\n    treeNode: true,\n  },\n  {\n    field: 'deptCategory',\n    title: '类别编码',\n  },\n  {\n    field: 'orderNum',\n    title: '排序',\n  },\n  {\n    field: 'status',\n    title: '状态',\n    slots: {\n      default: ({ row }) => {\n        return renderDict(row.status, DictEnum.SYS_NORMAL_DISABLE);\n      },\n    },\n  },\n  {\n    field: 'createTime',\n    title: '创建时间',\n  },\n  {\n    field: 'action',\n    fixed: 'right',\n    slots: { default: 'action' },\n    title: '操作',\n    resizable: false,\n    width: 'auto',\n  },\n];\n\nexport const drawerSchema: FormSchemaGetter = () => [\n  {\n    component: 'Input',\n    dependencies: {\n      show: () => false,\n      triggerFields: [''],\n    },\n    fieldName: 'deptId',\n  },\n  {\n    component: 'TreeSelect',\n    componentProps: {\n      getPopupContainer,\n    },\n    dependencies: {\n      show: (model) => model.parentId !== 0,\n      triggerFields: ['parentId'],\n    },\n    fieldName: 'parentId',\n    label: '上级部门',\n    rules: 'selectRequired',\n  },\n  {\n    component: 'Input',\n    fieldName: 'deptName',\n    label: '部门名称',\n    rules: 'required',\n  },\n  {\n    component: 'InputNumber',\n    fieldName: 'orderNum',\n    label: '显示排序',\n    rules: 'required',\n    defaultValue: 0,\n  },\n  {\n    component: 'Input',\n    fieldName: 'deptCategory',\n    label: '类别编码',\n  },\n  {\n    component: 'Select',\n    componentProps: {\n      // 选中了就只能修改 不能重置为无负责人\n      allowClear: false,\n      getPopupContainer,\n    },\n    fieldName: 'leader',\n    label: '负责人',\n  },\n  {\n    component: 'Input',\n    fieldName: 'phone',\n    label: '联系电话',\n    rules: z\n      .string()\n      .regex(/^1[3,4578]\\d{9}$/, { message: '请输入正确的手机号' })\n      .optional()\n      .or(z.literal('')),\n  },\n  {\n    component: 'Input',\n    fieldName: 'email',\n    label: '邮箱',\n    rules: z\n      .string()\n      .email({ message: '请输入正确的邮箱' })\n      .optional()\n      .or(z.literal('')),\n  },\n  {\n    component: 'RadioGroup',\n    componentProps: {\n      buttonStyle: 'solid',\n      options: getDictOptions(DictEnum.SYS_NORMAL_DISABLE),\n      optionType: 'button',\n    },\n    defaultValue: '0',\n    fieldName: 'status',\n    label: '状态',\n  },\n];\n"
  },
  {
    "path": "apps/web-antd/src/views/system/dept/dept-drawer.vue",
    "content": "<script setup lang=\"ts\">\nimport type { Dept } from '#/api/system/dept/model';\n\nimport { computed, ref } from 'vue';\n\nimport { useVbenDrawer } from '@vben/common-ui';\nimport { $t } from '@vben/locales';\nimport { addFullName, cloneDeep, listToTree } from '@vben/utils';\n\nimport { useVbenForm } from '#/adapter/form';\nimport {\n  deptAdd,\n  deptInfo,\n  deptList,\n  deptNodeList,\n  deptUpdate,\n} from '#/api/system/dept';\nimport { listUserByDeptId } from '#/api/system/user';\nimport { defaultFormValueGetter, useBeforeCloseDiff } from '#/utils/popup';\n\nimport { drawerSchema } from './data';\n\nconst emit = defineEmits<{ reload: [] }>();\n\ninterface DrawerProps {\n  id?: number | string;\n  update: boolean;\n}\n\nconst isUpdate = ref(false);\nconst title = computed(() => {\n  return isUpdate.value ? $t('pages.common.edit') : $t('pages.common.add');\n});\n\nconst [BasicForm, formApi] = useVbenForm({\n  commonConfig: {\n    componentProps: {\n      class: 'w-full',\n    },\n    formItemClass: 'col-span-2',\n    labelWidth: 80,\n  },\n  schema: drawerSchema(),\n  showDefaultActions: false,\n  wrapperClass: 'grid-cols-2',\n});\n\nasync function getDeptTree(deptId?: number | string, exclude = false) {\n  let ret: Dept[] = [];\n  ret = await (!deptId || exclude ? deptList({}) : deptNodeList(deptId));\n  const treeData = listToTree(ret, { id: 'deptId', pid: 'parentId' });\n  // 添加部门名称 如 xx-xx-xx\n  addFullName(treeData, 'deptName', ' / ');\n  return treeData;\n}\n\nasync function initDeptSelect(deptId?: number | string) {\n  // 需要动态更新TreeSelect组件 这里允许为空\n  const treeData = await getDeptTree(deptId, !isUpdate.value);\n  formApi.updateSchema([\n    {\n      componentProps: {\n        fieldNames: { label: 'deptName', value: 'deptId' },\n        showSearch: true,\n        treeData,\n        treeDefaultExpandAll: true,\n        treeLine: { showLeafIcon: false },\n        // 筛选的字段\n        treeNodeFilterProp: 'deptName',\n        // 选中后显示在输入框的值\n        treeNodeLabelProp: 'fullName',\n      },\n      fieldName: 'parentId',\n    },\n  ]);\n}\n\n/**\n * 部门管理员下拉框 更新时才会enable\n * @param deptId\n */\nasync function initDeptUsers(deptId: number | string) {\n  const ret = await listUserByDeptId(deptId);\n  const options = ret.map((user) => ({\n    label: `${user.userName} | ${user.nickName}`,\n    value: user.userId,\n  }));\n  formApi.updateSchema([\n    {\n      componentProps: {\n        disabled: ret.length === 0,\n        options,\n        placeholder: ret.length === 0 ? '该部门暂无用户' : '请选择部门负责人',\n      },\n      fieldName: 'leader',\n    },\n  ]);\n}\n\nasync function setLeaderOptions() {\n  formApi.updateSchema([\n    {\n      componentProps: {\n        disabled: true,\n        options: [],\n        placeholder: '仅在更新时可选部门负责人',\n      },\n      fieldName: 'leader',\n    },\n  ]);\n}\n\nconst { onBeforeClose, markInitialized, resetInitialized } = useBeforeCloseDiff(\n  {\n    initializedGetter: defaultFormValueGetter(formApi),\n    currentGetter: defaultFormValueGetter(formApi),\n  },\n);\n\nconst [BasicDrawer, drawerApi] = useVbenDrawer({\n  onBeforeClose,\n  onClosed: handleClosed,\n  onConfirm: handleConfirm,\n  async onOpenChange(isOpen) {\n    if (!isOpen) {\n      return null;\n    }\n    drawerApi.drawerLoading(true);\n\n    const { id, update } = drawerApi.getData() as DrawerProps;\n    isUpdate.value = update;\n\n    if (id) {\n      await formApi.setFieldValue('parentId', id);\n      if (update) {\n        const record = await deptInfo(id);\n        await formApi.setValues(record);\n      }\n    }\n\n    await (update && id ? initDeptUsers(id) : setLeaderOptions());\n    /** 部门选择 下拉框 */\n    await initDeptSelect(id);\n    await markInitialized();\n\n    drawerApi.drawerLoading(false);\n  },\n});\n\nasync function handleConfirm() {\n  try {\n    drawerApi.lock(true);\n    const { valid } = await formApi.validate();\n    if (!valid) {\n      return;\n    }\n    const data = cloneDeep(await formApi.getValues());\n    await (isUpdate.value ? deptUpdate(data) : deptAdd(data));\n    resetInitialized();\n    emit('reload');\n    drawerApi.close();\n  } catch (error) {\n    console.error(error);\n  } finally {\n    drawerApi.lock(false);\n  }\n}\n\nasync function handleClosed() {\n  await formApi.resetForm();\n  resetInitialized();\n}\n</script>\n\n<template>\n  <BasicDrawer :title=\"title\" class=\"w-[600px]\">\n    <BasicForm />\n  </BasicDrawer>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/system/dept/index.vue",
    "content": "<script setup lang=\"ts\">\nimport type { VbenFormProps } from '@vben/common-ui';\n\nimport type { VxeGridProps } from '#/adapter/vxe-table';\nimport type { Dept } from '#/api/system/dept/model';\n\nimport { nextTick } from 'vue';\n\nimport { Page, useVbenDrawer } from '@vben/common-ui';\nimport { eachTree, getVxePopupContainer } from '@vben/utils';\n\nimport { Popconfirm, Space } from 'ant-design-vue';\n\nimport { useVbenVxeGrid } from '#/adapter/vxe-table';\nimport { deptList, deptRemove } from '#/api/system/dept';\n\nimport { columns, querySchema } from './data';\nimport deptDrawer from './dept-drawer.vue';\n\nconst formOptions: VbenFormProps = {\n  commonConfig: {\n    labelWidth: 80,\n    componentProps: {\n      allowClear: true,\n    },\n  },\n  schema: querySchema(),\n  wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4',\n};\n\nconst gridOptions: VxeGridProps = {\n  columns,\n  height: 'auto',\n  keepSource: true,\n  pagerConfig: {\n    enabled: false,\n  },\n  proxyConfig: {\n    ajax: {\n      query: async (_, formValues = {}) => {\n        const resp = await deptList({\n          ...formValues,\n        });\n        return { rows: resp };\n      },\n      // 默认请求接口后展开全部 不需要可以删除这段\n      querySuccess: () => {\n        // 默认展开 需要加上标记\n        eachTree(tableApi.grid.getData(), (item) => (item.expand = true));\n        nextTick(() => {\n          setExpandOrCollapse(true);\n        });\n      },\n    },\n  },\n  /**\n   * 虚拟滚动  默认关闭\n   */\n  scrollY: {\n    enabled: false,\n    gt: 0,\n  },\n  rowConfig: {\n    keyField: 'deptId',\n  },\n  treeConfig: {\n    parentField: 'parentId',\n    rowField: 'deptId',\n    transform: true,\n  },\n  id: 'system-dept-index',\n};\n\nconst [BasicTable, tableApi] = useVbenVxeGrid({\n  formOptions,\n  gridOptions,\n  gridEvents: {\n    cellDblclick: (e) => {\n      const { row = {} } = e;\n      if (!row?.children) {\n        return;\n      }\n      const isExpanded = row?.expand;\n      tableApi.grid.setTreeExpand(row, !isExpanded);\n      row.expand = !isExpanded;\n    },\n    // 需要监听使用箭头展开的情况 否则展开/折叠的数据不一致\n    toggleTreeExpand: (e) => {\n      const { row = {}, expanded } = e;\n      row.expand = expanded;\n    },\n  },\n});\nconst [DeptDrawer, drawerApi] = useVbenDrawer({\n  connectedComponent: deptDrawer,\n});\n\nfunction handleAdd() {\n  drawerApi.setData({ update: false });\n  drawerApi.open();\n}\n\nfunction handleSubAdd(row: Dept) {\n  const { deptId } = row;\n  drawerApi.setData({ id: deptId, update: false });\n  drawerApi.open();\n}\n\nasync function handleEdit(record: Dept) {\n  drawerApi.setData({ id: record.deptId, update: true });\n  drawerApi.open();\n}\n\nasync function handleDelete(row: Dept) {\n  await deptRemove(row.deptId);\n  await tableApi.query();\n}\n\n/**\n * 全部展开/折叠\n * @param expand 是否展开\n */\nfunction setExpandOrCollapse(expand: boolean) {\n  eachTree(tableApi.grid.getData(), (item) => (item.expand = expand));\n  tableApi.grid?.setAllTreeExpand(expand);\n}\n</script>\n\n<template>\n  <Page :auto-content-height=\"true\">\n    <BasicTable table-title=\"部门列表\" table-title-help=\"双击展开/收起子菜单\">\n      <template #toolbar-tools>\n        <Space>\n          <a-button @click=\"setExpandOrCollapse(false)\">\n            {{ $t('pages.common.collapse') }}\n          </a-button>\n          <a-button @click=\"setExpandOrCollapse(true)\">\n            {{ $t('pages.common.expand') }}\n          </a-button>\n          <a-button\n            type=\"primary\"\n            v-access:code=\"['system:dept:add']\"\n            @click=\"handleAdd\"\n          >\n            {{ $t('pages.common.add') }}\n          </a-button>\n        </Space>\n      </template>\n      <template #action=\"{ row }\">\n        <Space>\n          <ghost-button\n            v-access:code=\"['system:dept:edit']\"\n            @click=\"handleEdit(row)\"\n          >\n            {{ $t('pages.common.edit') }}\n          </ghost-button>\n          <ghost-button\n            class=\"btn-success\"\n            v-access:code=\"['system:dept:add']\"\n            @click=\"handleSubAdd(row)\"\n          >\n            {{ $t('pages.common.add') }}\n          </ghost-button>\n          <Popconfirm\n            :get-popup-container=\"getVxePopupContainer\"\n            placement=\"left\"\n            title=\"确认删除？\"\n            @confirm=\"handleDelete(row)\"\n          >\n            <ghost-button\n              danger\n              v-access:code=\"['system:dept:remove']\"\n              @click.stop=\"\"\n            >\n              {{ $t('pages.common.delete') }}\n            </ghost-button>\n          </Popconfirm>\n        </Space>\n      </template>\n    </BasicTable>\n    <DeptDrawer @reload=\"tableApi.query()\" />\n  </Page>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/system/dict/data/data.ts",
    "content": "import type { FormSchemaGetter } from '#/adapter/form';\nimport type { VxeGridProps } from '#/adapter/vxe-table';\nimport type { DictData } from '#/api/system/dict/dict-data-model';\n\nimport { renderDictTag } from '#/utils/render';\n\nexport const querySchema: FormSchemaGetter = () => [\n  {\n    component: 'Input',\n    fieldName: 'dictLabel',\n    label: '字典标签',\n  },\n];\n\nexport const columns: VxeGridProps['columns'] = [\n  { type: 'checkbox', width: 60 },\n  {\n    title: '字典标签',\n    field: 'cssClass',\n    slots: {\n      default: ({ row }) => {\n        const { dictValue } = row as DictData;\n        return renderDictTag(dictValue, [row]);\n      },\n    },\n  },\n  {\n    title: '字典键值',\n    field: 'dictValue',\n  },\n  {\n    title: '字典排序',\n    field: 'dictSort',\n  },\n  {\n    title: '备注',\n    field: 'remark',\n  },\n  {\n    title: '创建时间',\n    field: 'createTime',\n  },\n  {\n    field: 'action',\n    fixed: 'right',\n    slots: { default: 'action' },\n    title: '操作',\n    resizable: false,\n    width: 'auto',\n  },\n];\n\nexport const drawerSchema: FormSchemaGetter = () => [\n  {\n    component: 'Input',\n    dependencies: {\n      show: () => false,\n      triggerFields: [''],\n    },\n    fieldName: 'dictCode',\n  },\n  {\n    component: 'Input',\n    componentProps: {\n      disabled: true,\n    },\n    fieldName: 'dictType',\n    label: '字典类型',\n  },\n  {\n    component: 'Input',\n    fieldName: 'listClass',\n    label: '标签样式',\n  },\n  {\n    component: 'Input',\n    fieldName: 'dictLabel',\n    label: '数据标签',\n    rules: 'required',\n  },\n  {\n    component: 'Input',\n    fieldName: 'dictValue',\n    label: '数据键值',\n    rules: 'required',\n  },\n  {\n    component: 'Textarea',\n    componentProps: {\n      placeholder: '可使用tailwind类名 如bg-blue w-full h-full等',\n    },\n    fieldName: 'cssClass',\n    formItemClass: 'items-start',\n    help: '标签的css样式, 可添加已经编译的css类名',\n    label: 'css类名',\n  },\n  {\n    component: 'InputNumber',\n    fieldName: 'dictSort',\n    label: '显示排序',\n    rules: 'required',\n    defaultValue: 0,\n  },\n  {\n    component: 'Textarea',\n    fieldName: 'remark',\n    formItemClass: 'items-start',\n    label: '备注',\n  },\n];\n"
  },
  {
    "path": "apps/web-antd/src/views/system/dict/data/dict-data-drawer.vue",
    "content": "<script setup lang=\"ts\">\nimport { computed, ref } from 'vue';\n\nimport { useVbenDrawer } from '@vben/common-ui';\nimport { $t } from '@vben/locales';\nimport { cloneDeep } from '@vben/utils';\n\nimport { useVbenForm } from '#/adapter/form';\nimport {\n  dictDataAdd,\n  dictDataUpdate,\n  dictDetailInfo,\n} from '#/api/system/dict/dict-data';\nimport { tagTypes } from '#/components/dict';\nimport { defaultFormValueGetter, useBeforeCloseDiff } from '#/utils/popup';\n\nimport { drawerSchema } from './data';\nimport TagStylePicker from './tag-style-picker.vue';\n\nconst emit = defineEmits<{ reload: [] }>();\n\ninterface DrawerProps {\n  dictCode?: number | string;\n  dictType: string;\n}\n\nconst isUpdate = ref(false);\nconst title = computed(() => {\n  return isUpdate.value ? $t('pages.common.edit') : $t('pages.common.add');\n});\n\nconst [BasicForm, formApi] = useVbenForm({\n  commonConfig: {\n    componentProps: {\n      class: 'w-full',\n    },\n    formItemClass: 'col-span-2',\n    labelWidth: 80,\n  },\n  schema: drawerSchema(),\n  showDefaultActions: false,\n  wrapperClass: 'grid-cols-2',\n});\n\n/**\n * 标签样式选择器\n * default: 预设标签样式\n * custom: 自定义标签样式\n */\nconst selectType = ref('default');\n/**\n * 根据标签样式判断是自定义还是默认\n * @param listClass 标签样式\n */\nfunction setupSelectType(listClass: string) {\n  // 判断是自定义还是预设\n  const isDefault = Reflect.has(tagTypes, listClass);\n  selectType.value = isDefault ? 'default' : 'custom';\n}\n\nconst { onBeforeClose, markInitialized, resetInitialized } = useBeforeCloseDiff(\n  {\n    initializedGetter: defaultFormValueGetter(formApi),\n    currentGetter: defaultFormValueGetter(formApi),\n  },\n);\n\nconst [BasicDrawer, drawerApi] = useVbenDrawer({\n  onBeforeClose,\n  onClosed: handleClosed,\n  onConfirm: handleConfirm,\n  async onOpenChange(isOpen) {\n    if (!isOpen) {\n      return null;\n    }\n    drawerApi.drawerLoading(true);\n\n    const { dictCode, dictType } = drawerApi.getData() as DrawerProps;\n    isUpdate.value = !!dictCode;\n    await formApi.setFieldValue('dictType', dictType);\n\n    if (dictCode && isUpdate.value) {\n      const record = await dictDetailInfo(dictCode);\n      setupSelectType(record.listClass);\n      await formApi.setValues(record);\n    }\n    await markInitialized();\n\n    drawerApi.drawerLoading(false);\n  },\n});\n\nasync function handleConfirm() {\n  try {\n    drawerApi.lock(true);\n    const { valid } = await formApi.validate();\n    if (!valid) {\n      return;\n    }\n    const data = cloneDeep(await formApi.getValues());\n    // 需要置空的情况 undefined不会提交给后端 需要改为空字符串\n    if (!data.listClass) {\n      data.listClass = '';\n    }\n    await (isUpdate.value ? dictDataUpdate(data) : dictDataAdd(data));\n    resetInitialized();\n    emit('reload');\n    drawerApi.close();\n  } catch (error) {\n    console.error(error);\n  } finally {\n    drawerApi.lock(false);\n  }\n}\n\nasync function handleClosed() {\n  await formApi.resetForm();\n  selectType.value = 'default';\n  resetInitialized();\n}\n\n/**\n * 取消标签选中 必须设置为undefined才行\n */\nasync function handleDeSelect() {\n  await formApi.setFieldValue('listClass', undefined);\n}\n</script>\n\n<template>\n  <BasicDrawer :title=\"title\" class=\"w-[600px]\">\n    <BasicForm>\n      <template #listClass=\"slotProps\">\n        <TagStylePicker\n          v-bind=\"slotProps\"\n          v-model:select-type=\"selectType\"\n          @deselect=\"handleDeSelect\"\n        />\n      </template>\n    </BasicForm>\n  </BasicDrawer>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/system/dict/data/index.vue",
    "content": "<script setup lang=\"ts\">\nimport type { VbenFormProps } from '@vben/common-ui';\n\nimport type { VxeGridProps } from '#/adapter/vxe-table';\nimport type { PageQuery } from '#/api/common';\nimport type { DictData } from '#/api/system/dict/dict-data-model';\n\nimport { ref } from 'vue';\n\nimport { useVbenDrawer } from '@vben/common-ui';\nimport { getVxePopupContainer } from '@vben/utils';\n\nimport { Modal, Popconfirm, Space } from 'ant-design-vue';\n\nimport { useVbenVxeGrid, vxeCheckboxChecked } from '#/adapter/vxe-table';\nimport {\n  dictDataExport,\n  dictDataList,\n  dictDataRemove,\n} from '#/api/system/dict/dict-data';\nimport { commonDownloadExcel } from '#/utils/file/download';\n\nimport { emitter } from '../mitt';\nimport { columns, querySchema } from './data';\nimport dictDataDrawer from './dict-data-drawer.vue';\n\nconst dictType = ref('');\n\nconst formOptions: VbenFormProps = {\n  commonConfig: {\n    labelWidth: 80,\n    componentProps: {\n      allowClear: true,\n    },\n  },\n  schema: querySchema(),\n  wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3',\n};\n\nconst gridOptions: VxeGridProps = {\n  checkboxConfig: {\n    // 高亮\n    highlight: true,\n    // 翻页时保留选中状态\n    reserve: true,\n    // 点击行选中\n    // trigger: 'row',\n  },\n  columns,\n  height: 'auto',\n  keepSource: true,\n  pagerConfig: {},\n  proxyConfig: {\n    ajax: {\n      query: async ({ page }, formValues = {}) => {\n        const params: PageQuery = {\n          pageNum: page.currentPage,\n          pageSize: page.pageSize,\n          ...formValues,\n        };\n        if (dictType.value) {\n          params.dictType = dictType.value;\n        }\n\n        return await dictDataList(params);\n      },\n    },\n  },\n  rowConfig: {\n    keyField: 'dictCode',\n  },\n  id: 'system-dict-data-index',\n};\n\nconst [BasicTable, tableApi] = useVbenVxeGrid({\n  formOptions,\n  gridOptions,\n});\n\nconst [DictDataDrawer, drawerApi] = useVbenDrawer({\n  connectedComponent: dictDataDrawer,\n});\n\nfunction handleAdd() {\n  drawerApi.setData({ dictType: dictType.value });\n  drawerApi.open();\n}\n\nasync function handleEdit(record: DictData) {\n  drawerApi.setData({\n    dictType: dictType.value,\n    dictCode: record.dictCode,\n  });\n  drawerApi.open();\n}\n\nasync function handleDelete(row: DictData) {\n  await dictDataRemove([row.dictCode]);\n  await tableApi.query();\n}\n\nfunction handleMultiDelete() {\n  const rows = tableApi.grid.getCheckboxRecords();\n  const ids = rows.map((row: DictData) => row.dictCode);\n  Modal.confirm({\n    title: '提示',\n    okType: 'danger',\n    content: `确认删除选中的${ids.length}条记录吗？`,\n    onOk: async () => {\n      await dictDataRemove(ids);\n      await tableApi.query();\n    },\n  });\n}\n\nfunction handleDownloadExcel() {\n  commonDownloadExcel(dictDataExport, '字典数据', tableApi.formApi.form.values);\n}\n\nemitter.on('rowClick', async (value) => {\n  dictType.value = value;\n  await tableApi.query();\n});\n</script>\n\n<template>\n  <div>\n    <BasicTable id=\"dict-data\" table-title=\"字典数据列表\">\n      <template #toolbar-tools>\n        <Space>\n          <a-button\n            v-access:code=\"['system:dict:export']\"\n            @click=\"handleDownloadExcel\"\n          >\n            {{ $t('pages.common.export') }}\n          </a-button>\n          <a-button\n            :disabled=\"!vxeCheckboxChecked(tableApi)\"\n            danger\n            type=\"primary\"\n            v-access:code=\"['system:dict:remove']\"\n            @click=\"handleMultiDelete\"\n          >\n            {{ $t('pages.common.delete') }}\n          </a-button>\n          <a-button\n            :disabled=\"dictType === ''\"\n            type=\"primary\"\n            v-access:code=\"['system:dict:add']\"\n            @click=\"handleAdd\"\n          >\n            {{ $t('pages.common.add') }}\n          </a-button>\n        </Space>\n      </template>\n      <template #action=\"{ row }\">\n        <Space>\n          <ghost-button\n            v-access:code=\"['system:dict:edit']\"\n            @click=\"handleEdit(row)\"\n          >\n            {{ $t('pages.common.edit') }}\n          </ghost-button>\n          <Popconfirm\n            :get-popup-container=\"\n              (node) => getVxePopupContainer(node, 'dict-data')\n            \"\n            placement=\"left\"\n            title=\"确认删除？\"\n            @confirm=\"handleDelete(row)\"\n          >\n            <ghost-button\n              danger\n              v-access:code=\"['system:dict:remove']\"\n              @click.stop=\"\"\n            >\n              {{ $t('pages.common.delete') }}\n            </ghost-button>\n          </Popconfirm>\n        </Space>\n      </template>\n    </BasicTable>\n    <DictDataDrawer @reload=\"tableApi.query()\" />\n  </div>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/system/dict/data/tag-style-picker.vue",
    "content": "<script setup lang=\"ts\">\nimport type { RadioChangeEvent } from 'ant-design-vue';\n\nimport { computed } from 'vue';\n\nimport { usePreferences } from '@vben/preferences';\n\nimport { RadioGroup, Select } from 'ant-design-vue';\nimport { ColorPicker } from 'vue3-colorpicker';\n\nimport { tagSelectOptions } from '#/components/dict';\n\nimport 'vue3-colorpicker/style.css';\n\n/**\n * 需要禁止透传\n * 不禁止会有奇怪的bug 会绑定到selectType上\n * TODO: 未知原因 有待研究\n */\ndefineOptions({ inheritAttrs: false });\n\ndefineEmits<{ deselect: [] }>();\n\nconst options = [\n  { label: '默认颜色', value: 'default' },\n  { label: '自定义颜色', value: 'custom' },\n] as const;\n\n/**\n * 主要是加了const报错\n */\nconst computedOptions = computed(\n  () => options as unknown as { label: string; value: string }[],\n);\n\ntype SelectType = (typeof options)[number]['value'];\n\nconst selectType = defineModel<SelectType>('selectType', {\n  default: 'default',\n});\n\n/**\n * color必须为hex颜色或者undefined\n */\nconst color = defineModel<string | undefined>('value', {\n  default: undefined,\n});\n\nfunction handleSelectTypeChange(e: RadioChangeEvent) {\n  // 必须给默认hex颜色 不能为空字符串\n  color.value = e.target.value === 'custom' ? '#1677ff' : undefined;\n}\n\nconst { isDark } = usePreferences();\nconst theme = computed(() => {\n  return isDark.value ? 'black' : 'white';\n});\n</script>\n\n<template>\n  <div class=\"flex flex-1 items-center gap-[6px]\">\n    <RadioGroup\n      v-model:value=\"selectType\"\n      :options=\"computedOptions\"\n      button-style=\"solid\"\n      option-type=\"button\"\n      @change=\"handleSelectTypeChange\"\n    />\n    <Select\n      v-if=\"selectType === 'default'\"\n      v-model:value=\"color\"\n      :allow-clear=\"true\"\n      :options=\"tagSelectOptions()\"\n      class=\"flex-1\"\n      placeholder=\"请选择标签样式\"\n      @deselect=\"$emit('deselect')\"\n    />\n    <ColorPicker\n      v-if=\"selectType === 'custom'\"\n      disable-alpha\n      format=\"hex\"\n      v-model:pure-color=\"color\"\n      :theme=\"theme\"\n    />\n  </div>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/system/dict/data.vue",
    "content": "<template>\n  <div>\n    ele版本会使用这个文件 只是为了不报错`未找到对应组件`才新建的这个文件\n    无实际意义\n  </div>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/system/dict/index.vue",
    "content": "<script setup lang=\"ts\">\nimport { onUnmounted } from 'vue';\n\nimport { Page } from '@vben/common-ui';\n\nimport DictDataPanel from './data/index.vue';\nimport { emitter } from './mitt';\nimport DictTypePanel from './type/index.vue';\n\nonUnmounted(() => emitter.off('rowClick'));\n</script>\n\n<template>\n  <Page\n    :auto-content-height=\"true\"\n    content-class=\"flex flex-col lg:flex-row gap-4\"\n  >\n    <DictTypePanel class=\"flex-1 overflow-hidden\" />\n    <DictDataPanel class=\"flex-1 overflow-hidden\" />\n  </Page>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/system/dict/mitt.ts",
    "content": "import { mitt } from '@vben/utils';\n\n/**\n * dictType: string\n */\ntype Events = {\n  rowClick: string;\n};\n\nexport const emitter = mitt<Events>();\n"
  },
  {
    "path": "apps/web-antd/src/views/system/dict/type/data.ts",
    "content": "import type { FormSchemaGetter } from '#/adapter/form';\nimport type { VxeGridProps } from '#/adapter/vxe-table';\n\nimport { z } from '#/adapter/form';\n\nexport const querySchema: FormSchemaGetter = () => [\n  {\n    component: 'Input',\n    fieldName: 'dictName',\n    label: '字典名称',\n  },\n  {\n    component: 'Input',\n    fieldName: 'dictType',\n    label: '字典类型',\n  },\n];\n\nexport const columns: VxeGridProps['columns'] = [\n  { type: 'checkbox', width: 60 },\n  {\n    title: '字典名称',\n    field: 'dictName',\n  },\n  {\n    title: '字典类型',\n    field: 'dictType',\n  },\n  {\n    title: '备注',\n    field: 'remark',\n  },\n  {\n    title: '创建时间',\n    field: 'createTime',\n  },\n  {\n    field: 'action',\n    fixed: 'right',\n    slots: { default: 'action' },\n    title: '操作',\n    resizable: false,\n    width: 'auto',\n  },\n];\n\nexport const modalSchema: FormSchemaGetter = () => [\n  {\n    component: 'Input',\n    dependencies: {\n      show: () => false,\n      triggerFields: [''],\n    },\n    fieldName: 'dictId',\n    label: 'dictId',\n  },\n  {\n    component: 'Input',\n    fieldName: 'dictName',\n    label: '字典名称',\n    rules: 'required',\n  },\n  {\n    component: 'Input',\n    fieldName: 'dictType',\n    help: '使用英文/下划线命名, 如:sys_normal_disable',\n    label: '字典类型',\n    rules: z\n      .string()\n      .regex(/^[a-z_]+$/i, { message: '字典类型只能使用英文/下划线命名' }),\n  },\n  {\n    component: 'Textarea',\n    fieldName: 'remark',\n    label: '备注',\n  },\n];\n"
  },
  {
    "path": "apps/web-antd/src/views/system/dict/type/dict-type-modal.vue",
    "content": "<script setup lang=\"ts\">\nimport { computed, ref } from 'vue';\n\nimport { useVbenModal } from '@vben/common-ui';\nimport { $t } from '@vben/locales';\nimport { cloneDeep } from '@vben/utils';\n\nimport { useVbenForm } from '#/adapter/form';\nimport {\n  dictTypeAdd,\n  dictTypeInfo,\n  dictTypeUpdate,\n} from '#/api/system/dict/dict-type';\nimport { defaultFormValueGetter, useBeforeCloseDiff } from '#/utils/popup';\n\nimport { modalSchema } from './data';\n\nconst emit = defineEmits<{ reload: [] }>();\n\nconst isUpdate = ref(false);\nconst title = computed(() => {\n  return isUpdate.value ? $t('pages.common.edit') : $t('pages.common.add');\n});\n\nconst [BasicForm, formApi] = useVbenForm({\n  layout: 'vertical',\n  commonConfig: {\n    labelWidth: 100,\n  },\n  schema: modalSchema(),\n  showDefaultActions: false,\n});\n\nconst { onBeforeClose, markInitialized, resetInitialized } = useBeforeCloseDiff(\n  {\n    initializedGetter: defaultFormValueGetter(formApi),\n    currentGetter: defaultFormValueGetter(formApi),\n  },\n);\n\nconst [BasicModal, modalApi] = useVbenModal({\n  fullscreenButton: false,\n  onBeforeClose,\n  onClosed: handleClosed,\n  onConfirm: handleConfirm,\n  onOpenChange: async (isOpen) => {\n    if (!isOpen) {\n      return null;\n    }\n    modalApi.modalLoading(true);\n\n    const { id } = modalApi.getData() as { id?: number | string };\n    isUpdate.value = !!id;\n    if (isUpdate.value && id) {\n      const record = await dictTypeInfo(id);\n      await formApi.setValues(record);\n    }\n    await markInitialized();\n\n    modalApi.modalLoading(false);\n  },\n});\n\nasync function handleConfirm() {\n  try {\n    modalApi.lock(true);\n    const { valid } = await formApi.validate();\n    if (!valid) {\n      return;\n    }\n    const data = cloneDeep(await formApi.getValues());\n    await (isUpdate.value ? dictTypeUpdate(data) : dictTypeAdd(data));\n    resetInitialized();\n    emit('reload');\n    modalApi.close();\n  } catch (error) {\n    console.error(error);\n  } finally {\n    modalApi.lock(false);\n  }\n}\n\nasync function handleClosed() {\n  await formApi.resetForm();\n  resetInitialized();\n}\n</script>\n\n<template>\n  <BasicModal :title=\"title\">\n    <BasicForm />\n  </BasicModal>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/system/dict/type/index-refactor.vue",
    "content": "<!-- 使用vxe实现成本最小 且自带虚拟滚动  -->\n<script setup lang=\"ts\">\nimport type { VxeGridProps } from '#/adapter/vxe-table';\nimport type { DictType } from '#/api/system/dict/dict-type-model';\n\nimport { h, ref, shallowRef, watch } from 'vue';\n\nimport { useVbenModal } from '@vben/common-ui';\nimport { cn } from '@vben/utils';\n\nimport {\n  DeleteOutlined,\n  EditOutlined,\n  ExportOutlined,\n  PlusOutlined,\n  SyncOutlined,\n} from '@ant-design/icons-vue';\nimport {\n  Alert,\n  Input,\n  Modal,\n  Popconfirm,\n  Space,\n  Tooltip,\n} from 'ant-design-vue';\n\nimport { useVbenVxeGrid } from '#/adapter/vxe-table';\nimport {\n  dictTypeExport,\n  dictTypeList,\n  dictTypeRemove,\n  refreshDictTypeCache,\n} from '#/api/system/dict/dict-type';\nimport { commonDownloadExcel } from '#/utils/file/download';\n\nimport { emitter } from '../mitt';\nimport dictTypeModal from './dict-type-modal.vue';\n\nconst tableAllData = shallowRef<DictType[]>([]);\nconst gridOptions: VxeGridProps = {\n  columns: [\n    {\n      title: 'name',\n      field: 'render',\n      slots: { default: 'render' },\n    },\n  ],\n  height: 'auto',\n  keepSource: true,\n  pagerConfig: {\n    enabled: false,\n  },\n  proxyConfig: {\n    ajax: {\n      query: async () => {\n        const resp = await dictTypeList();\n\n        total.value = resp.total;\n        tableAllData.value = resp.rows;\n        return resp;\n      },\n    },\n  },\n  rowConfig: {\n    keyField: 'dictId',\n    // 高亮当前行\n    isCurrent: true,\n  },\n  cellConfig: {\n    height: 60,\n  },\n  showHeader: false,\n  toolbarConfig: {\n    enabled: false,\n  },\n  // 开启虚拟滚动\n  scrollY: {\n    enabled: false,\n    gt: 0,\n  },\n  rowClassName: 'cursor-pointer',\n  id: 'system-dict-data-index',\n};\n\nconst [BasicTable, tableApi] = useVbenVxeGrid({\n  gridOptions,\n  gridEvents: {\n    cellClick: ({ row }) => {\n      handleRowClick(row);\n    },\n  },\n});\n\nconst [DictTypeModal, modalApi] = useVbenModal({\n  connectedComponent: dictTypeModal,\n});\n\nfunction handleAdd() {\n  modalApi.setData({});\n  modalApi.open();\n}\n\nasync function handleEdit(record: DictType) {\n  modalApi.setData({ id: record.dictId });\n  modalApi.open();\n}\n\nasync function handleDelete(row: DictType) {\n  await dictTypeRemove([row.dictId]);\n  await tableApi.query();\n}\n\nasync function handleReset() {\n  currentRowId.value = '';\n  searchValue.value = '';\n  await tableApi.query();\n}\n\nfunction handleDownloadExcel() {\n  commonDownloadExcel(dictTypeExport, '字典类型数据');\n}\n\nfunction handleRefreshCache() {\n  Modal.confirm({\n    title: '提示',\n    content: '确认刷新字典类型缓存吗？',\n    okButtonProps: {\n      danger: true,\n    },\n    onOk: async () => {\n      await refreshDictTypeCache();\n      await tableApi.query();\n    },\n  });\n}\n\nconst lastDictType = ref<string>('');\nconst currentRowId = ref<null | number | string>(null);\nfunction handleRowClick(row: DictType) {\n  if (lastDictType.value === row.dictType) {\n    return;\n  }\n  currentRowId.value = row.dictId;\n  emitter.emit('rowClick', row.dictType);\n}\n\nconst searchValue = ref('');\nconst total = ref(0);\nwatch(searchValue, (value) => {\n  if (!tableApi) {\n    return;\n  }\n  if (value) {\n    const names = tableAllData.value.filter((item) =>\n      item.dictName.includes(searchValue.value),\n    );\n    const types = tableAllData.value.filter((item) =>\n      item.dictType.includes(searchValue.value),\n    );\n    const filtered = [...new Set([...names, ...types])];\n    total.value = filtered.length;\n    tableApi.grid.loadData(filtered);\n  } else {\n    total.value = tableAllData.value.length;\n    tableApi.grid.loadData(tableAllData.value);\n  }\n});\n</script>\n\n<template>\n  <div\n    :class=\"\n      cn(\n        'bg-background flex max-h-[100vh] w-[360px] flex-col overflow-y-hidden',\n        'rounded-lg',\n        'dict-type-card',\n      )\n    \"\n  >\n    <div :class=\"cn('flex items-center justify-between', 'border-b px-4 py-2')\">\n      <span class=\"font-semibold\">字典项列表</span>\n      <Space>\n        <Tooltip title=\"刷新缓存\">\n          <a-button\n            v-access:code=\"['system:dict:edit']\"\n            :icon=\"h(SyncOutlined)\"\n            @click=\"handleRefreshCache\"\n          />\n        </Tooltip>\n        <Tooltip :title=\"$t('pages.common.export')\">\n          <a-button\n            v-access:code=\"['system:dict:export']\"\n            :icon=\"h(ExportOutlined)\"\n            @click=\"handleDownloadExcel\"\n          />\n        </Tooltip>\n        <Tooltip :title=\"$t('pages.common.add')\">\n          <a-button\n            v-access:code=\"['system:dict:add']\"\n            :icon=\"h(PlusOutlined)\"\n            @click=\"handleAdd\"\n          />\n        </Tooltip>\n      </Space>\n    </div>\n    <div class=\"flex flex-1 flex-col overflow-y-hidden p-4\">\n      <Alert\n        class=\"mb-4\"\n        show-icon\n        message=\"如果你的数据量大 自行开启虚拟滚动\"\n      />\n      <Input\n        placeholder=\"搜索字典项名称/类型\"\n        v-model:value=\"searchValue\"\n        allow-clear\n      >\n        <template #addonAfter>\n          <Tooltip title=\"重置/刷新\">\n            <SyncOutlined\n              v-access:code=\"['system:dict:edit']\"\n              @click=\"handleReset\"\n            />\n          </Tooltip>\n        </template>\n      </Input>\n      <BasicTable class=\"flex-1 overflow-hidden\">\n        <template #render=\"{ row: item }\">\n          <div :class=\"cn('flex items-center justify-between px-2 py-2')\">\n            <div class=\"flex flex-col items-baseline overflow-hidden\">\n              <span class=\"font-medium\">{{ item.dictName }}</span>\n              <div\n                class=\"max-w-full overflow-hidden text-ellipsis whitespace-nowrap\"\n              >\n                {{ item.dictType }}\n              </div>\n            </div>\n            <div class=\"flex items-center gap-3 text-[17px]\">\n              <EditOutlined\n                class=\"text-primary\"\n                v-access:code=\"['system:dict:edit']\"\n                @click.stop=\"handleEdit(item)\"\n              />\n              <Popconfirm\n                placement=\"left\"\n                :title=\"`确认删除 [${item.dictName}]?`\"\n                @confirm=\"handleDelete(item)\"\n              >\n                <DeleteOutlined\n                  v-access:code=\"['system:dict:remove']\"\n                  class=\"text-destructive\"\n                  @click.stop=\"\"\n                />\n              </Popconfirm>\n            </div>\n          </div>\n        </template>\n      </BasicTable>\n    </div>\n    <div class=\"border-t px-4 py-3\">共 {{ total }} 条数据</div>\n    <DictTypeModal @reload=\"tableApi.query()\" />\n  </div>\n</template>\n\n<style lang=\"scss\">\n.dict-type-card {\n  .vxe-grid {\n    padding: 12px 0 0;\n\n    .vxe-body--row {\n      &.row--current {\n        // 选中行背景色\n        background-color: hsl(var(--accent-hover)) !important;\n      }\n    }\n  }\n\n  .ant-alert {\n    padding: 6px 12px;\n  }\n}\n</style>\n"
  },
  {
    "path": "apps/web-antd/src/views/system/dict/type/index.vue",
    "content": "<script setup lang=\"ts\">\nimport type { VbenFormProps } from '@vben/common-ui';\n\nimport type { VxeGridProps } from '#/adapter/vxe-table';\nimport type { DictType } from '#/api/system/dict/dict-type-model';\n\nimport { ref } from 'vue';\n\nimport { useVbenModal } from '@vben/common-ui';\nimport { getVxePopupContainer } from '@vben/utils';\n\nimport { Modal, Popconfirm, Space } from 'ant-design-vue';\n\nimport { useVbenVxeGrid, vxeCheckboxChecked } from '#/adapter/vxe-table';\nimport {\n  dictTypeExport,\n  dictTypeList,\n  dictTypeRemove,\n  refreshDictTypeCache,\n} from '#/api/system/dict/dict-type';\nimport { commonDownloadExcel } from '#/utils/file/download';\n\nimport { emitter } from '../mitt';\nimport { columns, querySchema } from './data';\nimport dictTypeModal from './dict-type-modal.vue';\n\nconst formOptions: VbenFormProps = {\n  commonConfig: {\n    labelWidth: 70,\n    componentProps: {\n      allowClear: true,\n    },\n  },\n  schema: querySchema(),\n  wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3',\n};\n\nconst gridOptions: VxeGridProps = {\n  checkboxConfig: {\n    // 高亮\n    highlight: true,\n    // 翻页时保留选中状态\n    reserve: true,\n    // 点击行选中\n    // trigger: 'row',\n  },\n  columns,\n  height: 'auto',\n  keepSource: true,\n  pagerConfig: {},\n  proxyConfig: {\n    ajax: {\n      query: async ({ page }, formValues = {}) => {\n        return await dictTypeList({\n          pageNum: page.currentPage,\n          pageSize: page.pageSize,\n          ...formValues,\n        });\n      },\n    },\n  },\n  rowConfig: {\n    keyField: 'dictId',\n    // 高亮当前行\n    isCurrent: true,\n  },\n  id: 'system-dict-type-index',\n  rowClassName: 'hover:cursor-pointer',\n};\n\nconst lastDictType = ref('');\n\nconst [BasicTable, tableApi] = useVbenVxeGrid({\n  formOptions,\n  gridOptions,\n  gridEvents: {\n    cellClick: (e) => {\n      const { row } = e;\n      if (lastDictType.value === row.dictType) {\n        return;\n      }\n      emitter.emit('rowClick', row.dictType);\n      lastDictType.value = row.dictType;\n    },\n  },\n});\nconst [DictTypeModal, modalApi] = useVbenModal({\n  connectedComponent: dictTypeModal,\n});\n\nfunction handleAdd() {\n  modalApi.setData({});\n  modalApi.open();\n}\n\nasync function handleEdit(record: DictType) {\n  modalApi.setData({ id: record.dictId });\n  modalApi.open();\n}\n\nasync function handleDelete(row: DictType) {\n  await dictTypeRemove([row.dictId]);\n  await tableApi.query();\n}\n\nfunction handleMultiDelete() {\n  const rows = tableApi.grid.getCheckboxRecords();\n  const ids = rows.map((row: DictType) => row.dictId);\n  Modal.confirm({\n    title: '提示',\n    okType: 'danger',\n    content: `确认删除选中的${ids.length}条记录吗？`,\n    onOk: async () => {\n      await dictTypeRemove(ids);\n      await tableApi.query();\n    },\n  });\n}\n\nasync function handleRefreshCache() {\n  await refreshDictTypeCache();\n  await tableApi.query();\n}\n\nfunction handleDownloadExcel() {\n  commonDownloadExcel(\n    dictTypeExport,\n    '字典类型数据',\n    tableApi.formApi.form.values,\n  );\n}\n</script>\n\n<template>\n  <div>\n    <BasicTable id=\"dict-type\" table-title=\"字典类型列表\">\n      <template #toolbar-tools>\n        <Space>\n          <a-button\n            v-access:code=\"['system:dict:edit']\"\n            @click=\"handleRefreshCache\"\n          >\n            刷新缓存\n          </a-button>\n          <a-button\n            v-access:code=\"['system:dict:export']\"\n            @click=\"handleDownloadExcel\"\n          >\n            {{ $t('pages.common.export') }}\n          </a-button>\n          <a-button\n            :disabled=\"!vxeCheckboxChecked(tableApi)\"\n            danger\n            type=\"primary\"\n            v-access:code=\"['system:dict:remove']\"\n            @click=\"handleMultiDelete\"\n          >\n            {{ $t('pages.common.delete') }}\n          </a-button>\n          <a-button\n            type=\"primary\"\n            v-access:code=\"['system:dict:add']\"\n            @click=\"handleAdd\"\n          >\n            {{ $t('pages.common.add') }}\n          </a-button>\n        </Space>\n      </template>\n      <template #action=\"{ row }\">\n        <Space>\n          <ghost-button\n            v-access:code=\"['system:dict:edit']\"\n            @click.stop=\"handleEdit(row)\"\n          >\n            {{ $t('pages.common.edit') }}\n          </ghost-button>\n          <Popconfirm\n            :get-popup-container=\"\n              (node) => getVxePopupContainer(node, 'dict-type')\n            \"\n            placement=\"left\"\n            title=\"确认删除？\"\n            @confirm=\"handleDelete(row)\"\n          >\n            <ghost-button\n              danger\n              v-access:code=\"['system:dict:remove']\"\n              @click.stop=\"\"\n            >\n              {{ $t('pages.common.delete') }}\n            </ghost-button>\n          </Popconfirm>\n        </Space>\n      </template>\n    </BasicTable>\n    <DictTypeModal @reload=\"tableApi.query()\" />\n  </div>\n</template>\n\n<style lang=\"scss\">\ndiv#dict-type {\n  .vxe-body--row {\n    &.row--current {\n      // 选中行bold\n      @apply font-semibold;\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "apps/web-antd/src/views/system/menu/data.tsx",
    "content": "import type { FormSchemaGetter } from '#/adapter/form';\nimport type { VxeGridProps } from '#/adapter/vxe-table';\n\nimport { h } from 'vue';\n\nimport { DictEnum } from '@vben/constants';\nimport { FolderIcon, MenuIcon, OkButtonIcon, VbenIcon } from '@vben/icons';\nimport { $t } from '@vben/locales';\nimport { getPopupContainer } from '@vben/utils';\n\nimport { z } from '#/adapter/form';\nimport { getDictOptions } from '#/utils/dict';\nimport { renderDict } from '#/utils/render';\n\nexport const querySchema: FormSchemaGetter = () => [\n  {\n    component: 'Input',\n    fieldName: 'menuName',\n    label: '菜单名称 ',\n  },\n  {\n    component: 'Select',\n    componentProps: {\n      getPopupContainer,\n      options: getDictOptions(DictEnum.SYS_NORMAL_DISABLE),\n    },\n    fieldName: 'status',\n    label: '菜单状态 ',\n  },\n  {\n    component: 'Select',\n    componentProps: {\n      getPopupContainer,\n      options: getDictOptions(DictEnum.SYS_SHOW_HIDE),\n    },\n    fieldName: 'visible',\n    label: '显示状态',\n  },\n];\n\n// 菜单类型（M目录 C菜单 F按钮）\nexport const menuTypeOptions = [\n  { label: '目录', value: 'M' },\n  { label: '菜单', value: 'C' },\n  { label: '按钮', value: 'F' },\n];\n\nexport const yesNoOptions = [\n  { label: '是', value: '0' },\n  { label: '否', value: '1' },\n];\n\n// （M目录 C菜单 F按钮）\nconst menuTypes = {\n  C: { icon: MenuIcon, value: '菜单' },\n  F: { icon: OkButtonIcon, value: '按钮' },\n  M: { icon: FolderIcon, value: '目录' },\n};\nexport const columns: VxeGridProps['columns'] = [\n  {\n    title: '菜单名称',\n    field: 'menuName',\n    treeNode: true,\n    width: 200,\n    // 层级更明显显示\n    align: 'left',\n    slots: {\n      // 需要i18n支持 否则返回原始值\n      default: ({ row }) => $t(row.menuName),\n    },\n  },\n  {\n    title: '图标',\n    field: 'icon',\n    width: 80,\n    slots: {\n      default: ({ row }) => {\n        if (row?.icon === '#') {\n          return '';\n        }\n        return (\n          <span class={'flex justify-center'}>\n            <VbenIcon icon={row.icon} />\n          </span>\n        );\n      },\n    },\n  },\n  {\n    title: '排序',\n    field: 'orderNum',\n    width: 120,\n  },\n  {\n    title: '组件类型',\n    field: 'menuType',\n    width: 150,\n    slots: {\n      default: ({ row }) => {\n        const current = menuTypes[row.menuType as 'C' | 'F' | 'M'];\n        if (!current) {\n          return '未知';\n        }\n        return (\n          <span class=\"flex items-center justify-center gap-1\">\n            {h(current.icon, { class: 'size-[18px]' })}\n            <span>{current.value}</span>\n          </span>\n        );\n      },\n    },\n  },\n  {\n    title: '权限标识',\n    field: 'perms',\n  },\n  {\n    title: '组件路径',\n    field: 'component',\n  },\n  {\n    title: '状态',\n    field: 'status',\n    width: 100,\n    slots: {\n      default: ({ row }) => {\n        return renderDict(row.status, DictEnum.SYS_NORMAL_DISABLE);\n      },\n    },\n  },\n  {\n    title: '显示',\n    field: 'visible',\n    width: 100,\n    slots: {\n      default: ({ row }) => {\n        return renderDict(row.visible, DictEnum.SYS_SHOW_HIDE);\n      },\n    },\n  },\n  {\n    title: '创建时间',\n    field: 'createTime',\n  },\n  {\n    field: 'action',\n    fixed: 'right',\n    slots: { default: 'action' },\n    title: '操作',\n    resizable: false,\n    width: 'auto',\n  },\n];\n\nexport const drawerSchema: FormSchemaGetter = () => [\n  {\n    component: 'Input',\n    dependencies: {\n      show: () => false,\n      triggerFields: [''],\n    },\n    fieldName: 'menuId',\n  },\n  {\n    component: 'TreeSelect',\n    defaultValue: 0,\n    fieldName: 'parentId',\n    label: '上级菜单',\n    rules: 'selectRequired',\n  },\n  {\n    component: 'RadioGroup',\n    componentProps: {\n      buttonStyle: 'solid',\n      options: menuTypeOptions,\n      optionType: 'button',\n    },\n    defaultValue: 'M',\n    dependencies: {\n      componentProps: (_, api) => {\n        // 切换时清空校验\n        // 直接抄的源码 没有清空校验的方法\n        Object.keys(api.errors.value).forEach((key) => {\n          api.setFieldError(key, undefined);\n        });\n        return {};\n      },\n      triggerFields: ['menuType'],\n    },\n    fieldName: 'menuType',\n    label: '菜单类型',\n  },\n  {\n    component: 'Input',\n    dependencies: {\n      // 类型不为按钮时显示\n      show: (values) => values.menuType !== 'F',\n      triggerFields: ['menuType'],\n    },\n    renderComponentContent: (model) => ({\n      addonBefore: () => <VbenIcon icon={model.icon} />,\n      addonAfter: () => (\n        <a href=\"https://icon-sets.iconify.design/\" target=\"_blank\">\n          搜索图标\n        </a>\n      ),\n    }),\n    fieldName: 'icon',\n    help: '点击搜索图标跳转到iconify & 粘贴',\n    label: '菜单图标',\n  },\n  {\n    component: 'Input',\n    fieldName: 'menuName',\n    label: '菜单名称',\n    help: '支持i18n写法, 如: menu.system.user',\n    rules: 'required',\n  },\n  {\n    component: 'InputNumber',\n    fieldName: 'orderNum',\n    help: '排序, 数字越小越靠前',\n    label: '显示排序',\n    defaultValue: 0,\n    rules: 'required',\n  },\n  {\n    component: 'Input',\n    componentProps: (model) => {\n      const placeholder =\n        model.isFrame === '0'\n          ? '填写链接地址http(s)://  使用新页面打开'\n          : '填写`路由地址`或者`链接地址`  链接默认使用内部iframe内嵌打开';\n      return {\n        placeholder,\n      };\n    },\n    dependencies: {\n      rules: (model) => {\n        if (model.isFrame !== '0') {\n          return z\n            .string({ message: '请输入路由地址' })\n            .min(1, '请输入路由地址')\n            .refine((val) => !val.startsWith('/'), {\n              message: '路由地址不需要带/',\n            });\n        }\n        // 为链接\n        return z\n          .string({ message: '请输入链接地址' })\n          .regex(/^https?:\\/\\//, { message: '请输入正确的链接地址' });\n      },\n      // 类型不为按钮时显示\n      show: (values) => values?.menuType !== 'F',\n      triggerFields: ['isFrame', 'menuType'],\n    },\n    fieldName: 'path',\n    help: `路由地址不带/, 如: menu, user\\n 链接为http(s)://开头\\n 链接默认使用内部iframe打开, 可通过{是否外链}控制打开方式`,\n    label: '路由地址',\n  },\n  {\n    component: 'Input',\n    componentProps: (model) => {\n      return {\n        // 为链接时组件disabled\n        disabled: model.isFrame === '0',\n      };\n    },\n    defaultValue: '',\n    dependencies: {\n      rules: (model) => {\n        // 非链接时为必填项\n        if (model.path && !/^https?:\\/\\//.test(model.path)) {\n          return z\n            .string()\n            .min(1, { message: '非链接时必填组件路径' })\n            .refine((val) => !val.startsWith('/') && !val.endsWith('/'), {\n              message: '组件路径开头/末尾不需要带/',\n            });\n        }\n        // 为链接时非必填\n        return z.string().optional();\n      },\n      // 类型为菜单时显示\n      show: (values) => values.menuType === 'C',\n      triggerFields: ['menuType', 'path'],\n    },\n    fieldName: 'component',\n    help: '填写./src/views下的组件路径, 如system/menu/index',\n    label: '组件路径',\n  },\n  {\n    component: 'RadioGroup',\n    componentProps: {\n      buttonStyle: 'solid',\n      options: yesNoOptions,\n      optionType: 'button',\n    },\n    defaultValue: '1',\n    dependencies: {\n      // 类型不为按钮时显示\n      show: (values) => values.menuType !== 'F',\n      triggerFields: ['menuType'],\n    },\n    fieldName: 'isFrame',\n    help: '外链为http(s)://开头\\n 选择否时, 使用iframe从内部打开页面, 否则新窗口打开',\n    label: '是否外链',\n  },\n  {\n    component: 'RadioGroup',\n    componentProps: {\n      buttonStyle: 'solid',\n      options: getDictOptions(DictEnum.SYS_SHOW_HIDE),\n      optionType: 'button',\n    },\n    defaultValue: '0',\n    dependencies: {\n      // 类型不为按钮时显示\n      show: (values) => values.menuType !== 'F',\n      triggerFields: ['menuType'],\n    },\n    fieldName: 'visible',\n    help: '隐藏后不会出现在菜单栏, 但仍然可以访问',\n    label: '是否显示',\n  },\n  {\n    component: 'RadioGroup',\n    componentProps: {\n      buttonStyle: 'solid',\n      options: getDictOptions(DictEnum.SYS_NORMAL_DISABLE),\n      optionType: 'button',\n    },\n    defaultValue: '0',\n    dependencies: {\n      // 类型不为按钮时显示\n      show: (values) => values.menuType !== 'F',\n      triggerFields: ['menuType'],\n    },\n    fieldName: 'status',\n    help: '停用后不会出现在菜单栏, 也无法访问',\n    label: '菜单状态',\n  },\n  {\n    component: 'Input',\n    dependencies: {\n      // 类型为菜单/按钮时显示\n      show: (values) => values.menuType !== 'M',\n      triggerFields: ['menuType'],\n    },\n    fieldName: 'perms',\n    help: `控制器中定义的权限字符\\n 如: @SaCheckPermission(\"system:user:import\")`,\n    label: '权限标识',\n  },\n  {\n    component: 'Input',\n    componentProps: (model) => ({\n      // 为链接时组件disabled\n      disabled: model.isFrame === '0',\n      placeholder: '必须为json字符串格式',\n    }),\n    dependencies: {\n      // 类型为菜单时显示\n      show: (values) => values.menuType === 'C',\n      triggerFields: ['menuType'],\n    },\n    fieldName: 'queryParam',\n    help: 'vue-router中的query属性\\n 如{\"name\": \"xxx\", \"age\": 16}',\n    label: '路由参数',\n  },\n  {\n    component: 'RadioGroup',\n    componentProps: {\n      buttonStyle: 'solid',\n      options: yesNoOptions,\n      optionType: 'button',\n    },\n    defaultValue: '0',\n    dependencies: {\n      // 类型为菜单时显示\n      show: (values) => values.menuType === 'C',\n      triggerFields: ['menuType'],\n    },\n    fieldName: 'isCache',\n    help: '路由的keepAlive属性',\n    label: '是否缓存',\n  },\n  {\n    component: 'Input',\n    fieldName: 'remark',\n    label: '拓展字段',\n    formItemClass: 'items-baseline',\n  },\n];\n"
  },
  {
    "path": "apps/web-antd/src/views/system/menu/index.vue",
    "content": "<script setup lang=\"ts\">\nimport type { VbenFormProps } from '@vben/common-ui';\n\nimport type { VxeGridProps } from '#/adapter/vxe-table';\nimport type { Menu } from '#/api/system/menu/model';\n\nimport { computed, ref } from 'vue';\n\nimport { useAccess } from '@vben/access';\nimport { Fallback, Page, useVbenDrawer } from '@vben/common-ui';\nimport { $t } from '@vben/locales';\nimport {\n  eachTree,\n  getVxePopupContainer,\n  listToTree,\n  treeToList,\n} from '@vben/utils';\n\nimport { Popconfirm, Space, Switch, Tooltip } from 'ant-design-vue';\n\nimport { useVbenVxeGrid } from '#/adapter/vxe-table';\nimport { menuCascadeRemove, menuList, menuRemove } from '#/api/system/menu';\n\nimport { columns, querySchema } from './data';\nimport menuDrawer from './menu-drawer.vue';\n\n/**\n * 不要问为什么有两个根节点 v-if会控制只会渲染一个\n */\n\nconst formOptions: VbenFormProps = {\n  commonConfig: {\n    labelWidth: 80,\n    componentProps: {\n      allowClear: true,\n    },\n  },\n  schema: querySchema(),\n  wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4',\n};\n\nconst gridOptions: VxeGridProps = {\n  columns,\n  height: 'auto',\n  keepSource: true,\n  pagerConfig: {\n    enabled: false,\n  },\n  proxyConfig: {\n    ajax: {\n      query: async (_, formValues = {}) => {\n        const resp = await menuList({\n          ...formValues,\n        });\n        // 手动转为树结构\n        const treeData = listToTree(resp, { id: 'menuId', pid: 'parentId' });\n        // 添加hasChildren字段\n        eachTree(treeData, (item) => {\n          item.hasChildren = !!(item.children && item.children.length > 0);\n        });\n        console.log(treeData);\n\n        return { rows: treeData };\n      },\n    },\n  },\n  rowConfig: {\n    keyField: 'menuId',\n    // 高亮点击行\n    isCurrent: true,\n  },\n  /**\n   * 开启虚拟滚动\n   * 数据量小可以选择关闭\n   * 如果遇到样式问题(空白、错位 滚动等)可以选择关闭虚拟滚动\n   *\n   * 由于已经重构为懒加载 不需要虚拟滚动(如果你觉得卡顿 依旧可以选择开启)\n   */\n  // scrollY: {\n  //   enabled: true,\n  //   gt: 0,\n  // },\n  treeConfig: {\n    parentField: 'parentId',\n    rowField: 'menuId',\n    // 使用懒加载需要自行构造hasChild字段 不需要自动转换为树结构\n    transform: false,\n    // 刷新接口后 记录展开行的情况\n    reserve: true,\n    // 是否存在子节点的字段\n    hasChildField: 'hasChildren',\n    // 开启展开 懒加载\n    lazy: true,\n    // 懒加载方法 直接返回children\n    loadMethod: ({ row }) => row.children ?? [],\n  },\n  id: 'system-menu-index',\n};\n\nconst [BasicTable, tableApi] = useVbenVxeGrid({\n  formOptions,\n  gridOptions,\n  gridEvents: {\n    cellDblclick: (e) => {\n      const { row = {} } = e;\n      if (!row?.children) {\n        return;\n      }\n      const isExpanded = row?.expand;\n      tableApi.grid.setTreeExpand(row, !isExpanded);\n      row.expand = !isExpanded;\n    },\n    // 需要监听使用箭头展开的情况 否则展开/折叠的数据不一致\n    toggleTreeExpand: (e) => {\n      const { row = {}, expanded } = e;\n      row.expand = expanded;\n    },\n  },\n});\nconst [MenuDrawer, drawerApi] = useVbenDrawer({\n  connectedComponent: menuDrawer,\n});\n\nfunction handleAdd() {\n  drawerApi.setData({});\n  drawerApi.open();\n}\n\nfunction handleSubAdd(row: Menu) {\n  const { menuId } = row;\n  drawerApi.setData({ id: menuId, update: false });\n  drawerApi.open();\n}\n\nasync function handleEdit(record: Menu) {\n  drawerApi.setData({ id: record.menuId, update: true });\n  drawerApi.open();\n}\n\n/**\n * 是否级联删除\n */\nconst cascadingDeletion = ref(false);\nasync function handleDelete(row: Menu) {\n  if (cascadingDeletion.value) {\n    // 级联删除\n    const menuAndChildren: Menu[] = treeToList([row], { id: 'menuId' });\n    await menuCascadeRemove(menuAndChildren.map((item) => item.menuId));\n  } else {\n    // 单删除\n    await menuRemove([row.menuId]);\n  }\n  await tableApi.query();\n}\n\nfunction removeConfirmTitle(row: Menu) {\n  const menuName = $t(row.menuName);\n  if (!cascadingDeletion.value) {\n    return `是否确认删除 [${menuName}] ?`;\n  }\n  const menuAndChildren = treeToList([row], { id: 'menuId' });\n  if (menuAndChildren.length === 1) {\n    return `是否确认删除 [${menuName}] ?`;\n  }\n  return `是否确认删除 [${menuName}] 及 [${menuAndChildren.length - 1}]个子项目 ?`;\n}\n\n/**\n * 编辑/添加成功后刷新表格\n */\nasync function afterEditOrAdd() {\n  tableApi.query();\n}\n\n/**\n * 全部展开/折叠\n * @param expand 是否展开\n */\nfunction setExpandOrCollapse(expand: boolean) {\n  eachTree(tableApi.grid.getData(), (item) => (item.expand = expand));\n  tableApi.grid?.setAllTreeExpand(expand);\n}\n\n/**\n * 与后台逻辑相同\n * 只有租户管理和超级管理能访问菜单管理\n * 注意: 只有超管才能对菜单进行`增删改`操作\n * 注意: 只有超管才能对菜单进行`增删改`操作\n * 注意: 只有超管才能对菜单进行`增删改`操作\n */\nconst { hasAccessByRoles } = useAccess();\nconst isAdmin = computed(() => {\n  return hasAccessByRoles(['admin', 'superadmin']);\n});\n</script>\n\n<template>\n  <Page v-if=\"isAdmin\" :auto-content-height=\"true\">\n    <BasicTable\n      id=\"system-menu-table\"\n      table-title=\"菜单列表\"\n      table-title-help=\"双击展开/收起子菜单\"\n    >\n      <template #toolbar-tools>\n        <Space>\n          <Tooltip title=\"删除菜单以及子菜单\">\n            <div\n              v-access:role=\"['superadmin']\"\n              v-access:code=\"['system:menu:remove']\"\n              class=\"mr-2 flex items-center\"\n            >\n              <span class=\"mr-2 text-sm text-[#666666]\">级联删除</span>\n              <Switch v-model:checked=\"cascadingDeletion\" />\n            </div>\n          </Tooltip>\n\n          <a-button @click=\"setExpandOrCollapse(false)\">\n            {{ $t('pages.common.collapse') }}\n          </a-button>\n\n          <a-button\n            type=\"primary\"\n            v-access:code=\"['system:menu:add']\"\n            v-access:role=\"['superadmin']\"\n            @click=\"handleAdd\"\n          >\n            {{ $t('pages.common.add') }}\n          </a-button>\n        </Space>\n      </template>\n      <template #action=\"{ row }\">\n        <Space>\n          <ghost-button\n            v-access:code=\"['system:menu:edit']\"\n            v-access:role=\"['superadmin']\"\n            @click=\"handleEdit(row)\"\n          >\n            {{ $t('pages.common.edit') }}\n          </ghost-button>\n          <!-- '按钮类型'无法再添加子菜单 -->\n          <ghost-button\n            v-if=\"row.menuType !== 'F'\"\n            class=\"btn-success\"\n            v-access:code=\"['system:menu:add']\"\n            v-access:role=\"['superadmin']\"\n            @click=\"handleSubAdd(row)\"\n          >\n            {{ $t('pages.common.add') }}\n          </ghost-button>\n          <Popconfirm\n            :get-popup-container=\"getVxePopupContainer\"\n            placement=\"left\"\n            :title=\"removeConfirmTitle(row)\"\n            @confirm=\"handleDelete(row)\"\n          >\n            <ghost-button\n              danger\n              v-access:code=\"['system:menu:remove']\"\n              v-access:role=\"['superadmin']\"\n              @click.stop=\"\"\n            >\n              {{ $t('pages.common.delete') }}\n            </ghost-button>\n          </Popconfirm>\n        </Space>\n      </template>\n    </BasicTable>\n    <MenuDrawer @reload=\"afterEditOrAdd\" />\n  </Page>\n  <Fallback v-else description=\"您没有菜单管理的访问权限\" status=\"403\" />\n</template>\n\n<style lang=\"scss\">\n#system-menu-table > .vxe-grid {\n  --vxe-ui-table-row-current-background-color: hsl(var(--primary-100));\n\n  html.dark & {\n    --vxe-ui-table-row-current-background-color: hsl(var(--primary-800));\n  }\n}\n</style>\n"
  },
  {
    "path": "apps/web-antd/src/views/system/menu/menu-drawer.vue",
    "content": "<script setup lang=\"ts\">\nimport { computed, ref } from 'vue';\n\nimport { useVbenDrawer } from '@vben/common-ui';\nimport { $t } from '@vben/locales';\nimport {\n  addFullName,\n  cloneDeep,\n  getPopupContainer,\n  listToTree,\n} from '@vben/utils';\n\nimport { Input, Skeleton } from 'ant-design-vue';\n\nimport { useVbenForm } from '#/adapter/form';\nimport { menuAdd, menuInfo, menuList, menuUpdate } from '#/api/system/menu';\nimport { defaultFormValueGetter, useBeforeCloseDiff } from '#/utils/popup';\n\nimport { drawerSchema } from './data';\n\ninterface ModalProps {\n  id?: number | string;\n  update: boolean;\n}\n\nconst emit = defineEmits<{ reload: [] }>();\n\nconst isUpdate = ref(false);\nconst title = computed(() => {\n  return isUpdate.value ? $t('pages.common.edit') : $t('pages.common.add');\n});\nconst loading = ref(false);\n\nconst [BasicForm, formApi] = useVbenForm({\n  commonConfig: {\n    componentProps: {\n      class: 'w-full',\n    },\n    formItemClass: 'col-span-2',\n    labelWidth: 90,\n  },\n  schema: drawerSchema(),\n  showDefaultActions: false,\n  wrapperClass: 'grid-cols-2',\n});\n\nasync function setupMenuSelect() {\n  // menu\n  const menuArray = await menuList();\n  // support i18n\n  menuArray.forEach((item) => {\n    item.menuName = $t(item.menuName);\n  });\n  // const folderArray = menuArray.filter((item) => item.menuType === 'M');\n  /**\n   * 这里需要过滤掉按钮类型\n   * 不允许在按钮下添加数据\n   */\n  const filteredList = menuArray.filter((item) => item.menuType !== 'F');\n  const menuTree = listToTree(filteredList, { id: 'menuId', pid: 'parentId' });\n  const fullMenuTree = [\n    {\n      menuId: 0,\n      menuName: $t('menu.root'),\n      children: menuTree,\n    },\n  ];\n  addFullName(fullMenuTree, 'menuName', ' / ');\n\n  formApi.updateSchema([\n    {\n      componentProps: {\n        fieldNames: {\n          label: 'menuName',\n          value: 'menuId',\n        },\n        getPopupContainer,\n        // 设置弹窗滚动高度 默认256\n        listHeight: 300,\n        showSearch: true,\n        treeData: fullMenuTree,\n        treeDefaultExpandAll: false,\n        // 默认展开的树节点\n        treeDefaultExpandedKeys: [0],\n        treeLine: { showLeafIcon: false },\n        // 筛选的字段\n        treeNodeFilterProp: 'menuName',\n        treeNodeLabelProp: 'fullName',\n      },\n      fieldName: 'parentId',\n    },\n  ]);\n}\n\nconst { onBeforeClose, markInitialized, resetInitialized } = useBeforeCloseDiff(\n  {\n    initializedGetter: defaultFormValueGetter(formApi),\n    currentGetter: defaultFormValueGetter(formApi),\n  },\n);\n\nconst [BasicDrawer, drawerApi] = useVbenDrawer({\n  onBeforeClose,\n  onClosed: handleClosed,\n  onConfirm: handleConfirm,\n  async onOpenChange(isOpen) {\n    if (!isOpen) {\n      return null;\n    }\n    drawerApi.drawerLoading(true);\n    loading.value = true;\n\n    const { id, update } = drawerApi.getData() as ModalProps;\n    isUpdate.value = update;\n\n    if (id) {\n      await formApi.setFieldValue('parentId', id);\n      // 创建元组(不是数组 元素位置固定)\n      const promise = [\n        update ? menuInfo(id) : null,\n        setupMenuSelect(),\n      ] as const;\n      // 并行获取菜单树选择和菜单信息\n      const [record] = await Promise.all(promise);\n      if (record) {\n        await formApi.setValues(record);\n      }\n    } else {\n      // 加载菜单树选择\n      await setupMenuSelect();\n    }\n    await markInitialized();\n\n    drawerApi.drawerLoading(false);\n    loading.value = false;\n  },\n});\n\nasync function handleConfirm() {\n  try {\n    drawerApi.lock(true);\n    const { valid } = await formApi.validate();\n    if (!valid) {\n      return;\n    }\n    const data = cloneDeep(await formApi.getValues());\n    await (isUpdate.value ? menuUpdate(data) : menuAdd(data));\n    resetInitialized();\n    emit('reload');\n    drawerApi.close();\n  } catch (error) {\n    console.error(error);\n  } finally {\n    drawerApi.lock(false);\n  }\n}\n\nasync function handleClosed() {\n  await formApi.resetForm();\n  resetInitialized();\n}\n</script>\n\n<template>\n  <BasicDrawer :title=\"title\" class=\"w-[600px]\">\n    <Skeleton active v-if=\"loading\" />\n    <BasicForm v-show=\"!loading\">\n      <template #remark=\"slotProps\">\n        <div class=\"flex flex-col gap-2\">\n          <Input v-bind=\"slotProps\" />\n          <span class=\"text-[14px] leading-[1.5] text-black/45\">\n            在ele作为activePath使用 但是非json格式 v5无法使用\n            建议自行在apps/web-antd/src/router/access.ts更改\n          </span>\n        </div>\n      </template>\n    </BasicForm>\n  </BasicDrawer>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/system/notice/data.ts",
    "content": "import type { FormSchemaGetter } from '#/adapter/form';\nimport type { VxeGridProps } from '#/adapter/vxe-table';\n\nimport { DictEnum } from '@vben/constants';\nimport { getPopupContainer } from '@vben/utils';\n\nimport { getDictOptions } from '#/utils/dict';\nimport { renderDict } from '#/utils/render';\n\nexport const querySchema: FormSchemaGetter = () => [\n  {\n    component: 'Input',\n    fieldName: 'noticeTitle',\n    label: '公告标题',\n  },\n  {\n    component: 'Input',\n    fieldName: 'createByName',\n    label: '创建人',\n  },\n  {\n    component: 'Select',\n    componentProps: {\n      getPopupContainer,\n      options: getDictOptions(DictEnum.SYS_NOTICE_TYPE),\n    },\n    fieldName: 'noticeType',\n    label: '公告类型',\n  },\n];\n\nexport const columns: VxeGridProps['columns'] = [\n  { type: 'checkbox', width: 60 },\n  {\n    title: '公告标题',\n    field: 'noticeTitle',\n  },\n  {\n    title: '公告类型',\n    field: 'noticeType',\n    width: 120,\n    slots: {\n      default: ({ row }) => {\n        return renderDict(row.noticeType, DictEnum.SYS_NOTICE_TYPE);\n      },\n    },\n  },\n  {\n    title: '状态',\n    field: 'status',\n    width: 120,\n    slots: {\n      default: ({ row }) => {\n        return renderDict(row.status, DictEnum.SYS_NOTICE_STATUS);\n      },\n    },\n  },\n  {\n    title: '创建人',\n    field: 'createByName',\n    width: 150,\n  },\n  {\n    title: '创建时间',\n    field: 'createTime',\n  },\n  {\n    field: 'action',\n    fixed: 'right',\n    slots: { default: 'action' },\n    title: '操作',\n    resizable: false,\n    width: 'auto',\n  },\n];\n\nexport const modalSchema: FormSchemaGetter = () => [\n  {\n    component: 'Input',\n    dependencies: {\n      show: () => false,\n      triggerFields: [''],\n    },\n    fieldName: 'noticeId',\n    label: '主键',\n  },\n  {\n    component: 'Input',\n    fieldName: 'noticeTitle',\n    label: '公告标题',\n    rules: 'required',\n  },\n  {\n    component: 'RadioGroup',\n    componentProps: {\n      buttonStyle: 'solid',\n      options: getDictOptions(DictEnum.SYS_NOTICE_STATUS),\n      optionType: 'button',\n    },\n    defaultValue: '0',\n    fieldName: 'status',\n    label: '公告状态',\n    rules: 'required',\n    formItemClass: 'col-span-1',\n  },\n  {\n    component: 'RadioGroup',\n    componentProps: {\n      buttonStyle: 'solid',\n      options: getDictOptions(DictEnum.SYS_NOTICE_TYPE),\n      optionType: 'button',\n    },\n    defaultValue: '1',\n    fieldName: 'noticeType',\n    label: '公告类型',\n    rules: 'required',\n    formItemClass: 'col-span-1',\n  },\n  {\n    component: 'RichTextarea',\n    componentProps: {\n      width: '100%',\n    },\n    fieldName: 'noticeContent',\n    label: '公告内容',\n    rules: 'required',\n  },\n];\n"
  },
  {
    "path": "apps/web-antd/src/views/system/notice/index.vue",
    "content": "<script setup lang=\"ts\">\nimport type { VbenFormProps } from '@vben/common-ui';\n\nimport type { VxeGridProps } from '#/adapter/vxe-table';\nimport type { Notice } from '#/api/system/notice/model';\n\nimport { Page, useVbenModal } from '@vben/common-ui';\nimport { getVxePopupContainer } from '@vben/utils';\n\nimport { Modal, Popconfirm, Space } from 'ant-design-vue';\n\nimport { useVbenVxeGrid, vxeCheckboxChecked } from '#/adapter/vxe-table';\nimport { noticeList, noticeRemove } from '#/api/system/notice';\n\nimport { columns, querySchema } from './data';\nimport noticeModal from './notice-modal.vue';\n\nconst formOptions: VbenFormProps = {\n  commonConfig: {\n    labelWidth: 80,\n    componentProps: {\n      allowClear: true,\n    },\n  },\n  schema: querySchema(),\n  wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4',\n};\n\nconst gridOptions: VxeGridProps = {\n  checkboxConfig: {\n    // 高亮\n    highlight: true,\n    // 翻页时保留选中状态\n    reserve: true,\n    // 点击行选中\n    // trigger: 'row',\n  },\n  columns,\n  height: 'auto',\n  keepSource: true,\n  pagerConfig: {},\n  proxyConfig: {\n    ajax: {\n      query: async ({ page }, formValues = {}) => {\n        return await noticeList({\n          pageNum: page.currentPage,\n          pageSize: page.pageSize,\n          ...formValues,\n        });\n      },\n    },\n  },\n  rowConfig: {\n    keyField: 'noticeId',\n  },\n  id: 'system-notice-index',\n};\n\nconst [BasicTable, tableApi] = useVbenVxeGrid({\n  formOptions,\n  gridOptions,\n});\n\nconst [NoticeModal, modalApi] = useVbenModal({\n  connectedComponent: noticeModal,\n});\n\nfunction handleAdd() {\n  modalApi.setData({});\n  modalApi.open();\n}\n\nasync function handleEdit(record: Notice) {\n  modalApi.setData({ id: record.noticeId });\n  modalApi.open();\n}\n\nasync function handleDelete(row: Notice) {\n  await noticeRemove([row.noticeId]);\n  await tableApi.query();\n}\n\nfunction handleMultiDelete() {\n  const rows = tableApi.grid.getCheckboxRecords();\n  const ids = rows.map((row: Notice) => row.noticeId);\n  Modal.confirm({\n    title: '提示',\n    okType: 'danger',\n    content: `确认删除选中的${ids.length}条记录吗？`,\n    onOk: async () => {\n      await noticeRemove(ids);\n      await tableApi.query();\n    },\n  });\n}\n</script>\n\n<template>\n  <Page :auto-content-height=\"true\">\n    <BasicTable table-title=\"通知公告列表\">\n      <template #toolbar-tools>\n        <Space>\n          <a-button\n            :disabled=\"!vxeCheckboxChecked(tableApi)\"\n            danger\n            type=\"primary\"\n            v-access:code=\"['system:notice:remove']\"\n            @click=\"handleMultiDelete\"\n          >\n            {{ $t('pages.common.delete') }}\n          </a-button>\n          <a-button\n            type=\"primary\"\n            v-access:code=\"['system:notice:add']\"\n            @click=\"handleAdd\"\n          >\n            {{ $t('pages.common.add') }}\n          </a-button>\n        </Space>\n      </template>\n      <template #action=\"{ row }\">\n        <Space>\n          <ghost-button\n            v-access:code=\"['system:notice:edit']\"\n            @click=\"handleEdit(row)\"\n          >\n            {{ $t('pages.common.edit') }}\n          </ghost-button>\n          <Popconfirm\n            :get-popup-container=\"getVxePopupContainer\"\n            placement=\"left\"\n            title=\"确认删除？\"\n            @confirm=\"handleDelete(row)\"\n          >\n            <ghost-button\n              danger\n              v-access:code=\"['system:notice:remove']\"\n              @click.stop=\"\"\n            >\n              {{ $t('pages.common.delete') }}\n            </ghost-button>\n          </Popconfirm>\n        </Space>\n      </template>\n    </BasicTable>\n    <NoticeModal @reload=\"tableApi.query()\" />\n  </Page>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/system/notice/notice-modal.vue",
    "content": "<!--\n2025年03月08日重构为原生表单(反向重构??)\n该文件作为例子 使用原生表单而非useVbenForm\n-->\n<script setup lang=\"ts\">\nimport type { RuleObject } from 'ant-design-vue/es/form';\n\nimport { computed, ref } from 'vue';\n\nimport { useVbenModal } from '@vben/common-ui';\nimport { DictEnum } from '@vben/constants';\nimport { $t } from '@vben/locales';\nimport { cloneDeep } from '@vben/utils';\n\nimport { Form, FormItem, Input, RadioGroup } from 'ant-design-vue';\nimport { pick } from 'lodash-es';\n\nimport { noticeAdd, noticeInfo, noticeUpdate } from '#/api/system/notice';\nimport { Tinymce } from '#/components/tinymce';\nimport { contentWithOssIdTransform } from '#/components/tinymce/src/helper';\nimport { getDictOptions } from '#/utils/dict';\nimport { useBeforeCloseDiff } from '#/utils/popup';\n\nconst emit = defineEmits<{ reload: [] }>();\n\nconst isUpdate = ref(false);\nconst title = computed(() => {\n  return isUpdate.value ? $t('pages.common.edit') : $t('pages.common.add');\n});\n\n/**\n * 定义表单数据类型\n */\ninterface FormData {\n  noticeId?: number;\n  noticeTitle?: string;\n  status?: string;\n  noticeType?: string;\n  noticeContent?: string;\n}\n\n/**\n * 定义默认值 用于reset\n */\nconst defaultValues: FormData = {\n  noticeId: undefined,\n  noticeTitle: '',\n  status: '0',\n  noticeType: '1',\n  noticeContent: '',\n};\n\n/**\n * 表单数据ref\n */\nconst formData = ref(defaultValues);\n\ntype AntdFormRules<T> = Partial<Record<keyof T, RuleObject[]>> & {\n  [key: string]: RuleObject[];\n};\n/**\n * 表单校验规则\n */\nconst formRules = ref<AntdFormRules<FormData>>({\n  status: [{ required: true, message: $t('ui.formRules.selectRequired') }],\n  noticeContent: [{ required: true, message: $t('ui.formRules.required') }],\n  noticeType: [{ required: true, message: $t('ui.formRules.selectRequired') }],\n  noticeTitle: [{ required: true, message: $t('ui.formRules.required') }],\n});\n\n/**\n * useForm解构出表单方法\n */\nconst { validate, validateInfos, resetFields } = Form.useForm(\n  formData,\n  formRules,\n);\n\nfunction customFormValueGetter() {\n  return JSON.stringify(formData.value);\n}\n\nconst { onBeforeClose, markInitialized, resetInitialized } = useBeforeCloseDiff(\n  {\n    initializedGetter: customFormValueGetter,\n    currentGetter: customFormValueGetter,\n  },\n);\n\nconst [BasicModal, modalApi] = useVbenModal({\n  class: 'w-[800px]',\n  fullscreenButton: true,\n  onBeforeClose,\n  onClosed: handleClosed,\n  onConfirm: handleConfirm,\n  onOpenChange: async (isOpen) => {\n    if (!isOpen) {\n      return null;\n    }\n    modalApi.modalLoading(true);\n\n    const { id } = modalApi.getData() as { id?: number | string };\n    isUpdate.value = !!id;\n    if (isUpdate.value && id) {\n      const record = await noticeInfo(id);\n      // 只赋值存在的字段\n      const filterRecord = pick(record, Object.keys(defaultValues));\n\n      // 你可以调用这个方法来显示私有桶的图片（每次获取最新）\n      // 如果你是公开桶 最好去掉这段代码 会造成不必要的查询\n      filterRecord.noticeContent =\n        (await contentWithOssIdTransform(record.noticeContent)) ?? '';\n\n      formData.value = filterRecord;\n    }\n    await markInitialized();\n\n    modalApi.modalLoading(false);\n  },\n});\n\nasync function handleConfirm() {\n  try {\n    modalApi.lock(true);\n    await validate();\n    // 可能会做数据处理 使用cloneDeep深拷贝\n    const data = cloneDeep(formData.value);\n    await (isUpdate.value ? noticeUpdate(data) : noticeAdd(data));\n    resetInitialized();\n    emit('reload');\n    modalApi.close();\n  } catch (error) {\n    console.error(error);\n  } finally {\n    modalApi.lock(false);\n  }\n}\n\nasync function handleClosed() {\n  formData.value = defaultValues;\n  resetFields();\n  resetInitialized();\n}\n</script>\n\n<template>\n  <BasicModal :title=\"title\">\n    <Form layout=\"vertical\">\n      <FormItem label=\"公告标题\" v-bind=\"validateInfos.noticeTitle\">\n        <Input\n          :placeholder=\"$t('ui.formRules.required')\"\n          v-model:value=\"formData.noticeTitle\"\n        />\n      </FormItem>\n      <div class=\"grid sm:grid-cols-1 lg:grid-cols-2\">\n        <FormItem label=\"公告状态\" v-bind=\"validateInfos.status\">\n          <RadioGroup\n            button-style=\"solid\"\n            option-type=\"button\"\n            v-model:value=\"formData.status\"\n            :options=\"getDictOptions(DictEnum.SYS_NOTICE_STATUS)\"\n          />\n        </FormItem>\n        <FormItem label=\"公告类型\" v-bind=\"validateInfos.noticeType\">\n          <RadioGroup\n            button-style=\"solid\"\n            option-type=\"button\"\n            v-model:value=\"formData.noticeType\"\n            :options=\"getDictOptions(DictEnum.SYS_NOTICE_TYPE)\"\n          />\n        </FormItem>\n      </div>\n      <FormItem label=\"公告内容\" v-bind=\"validateInfos.noticeContent\">\n        <Tinymce v-model=\"formData.noticeContent\" />\n      </FormItem>\n    </Form>\n  </BasicModal>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/system/oss/config.vue",
    "content": "<!--\n后端版本>=5.4.0  这个从本地路由变为从后台返回\n未修改文件名 而是新加了这个文件\n-->\n<script setup lang=\"ts\">\nimport OssConfigPage from '#/views/system/oss-config/index.vue';\n</script>\n\n<template>\n  <OssConfigPage />\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/system/oss/constant.ts",
    "content": "/** 支持的图片列表 */\nexport const supportImageList = ['jpg', 'jpeg', 'png', 'gif', 'webp'];\n"
  },
  {
    "path": "apps/web-antd/src/views/system/oss/data.tsx",
    "content": "import type { FormSchemaGetter } from '#/adapter/form';\nimport type { VxeGridProps } from '#/adapter/vxe-table';\n\nexport const querySchema: FormSchemaGetter = () => [\n  {\n    component: 'Input',\n    fieldName: 'fileName',\n    label: '文件名',\n  },\n  {\n    component: 'Input',\n    fieldName: 'originalName',\n    label: '原名',\n  },\n  {\n    component: 'Input',\n    fieldName: 'fileSuffix',\n    label: '拓展名',\n  },\n  {\n    component: 'Input',\n    fieldName: 'service',\n    label: '服务商',\n  },\n  {\n    component: 'RangePicker',\n    fieldName: 'createTime',\n    label: '创建时间',\n  },\n];\n\nexport const columns: VxeGridProps['columns'] = [\n  { type: 'checkbox', width: 60 },\n  {\n    title: '文件名',\n    field: 'fileName',\n    showOverflow: true,\n  },\n  {\n    title: '文件原名',\n    field: 'originalName',\n    showOverflow: true,\n  },\n  {\n    title: '文件拓展名',\n    field: 'fileSuffix',\n  },\n  {\n    title: '文件预览',\n    field: 'url',\n    showOverflow: true,\n    slots: { default: 'url' },\n  },\n  {\n    title: '创建时间',\n    field: 'createTime',\n    sortable: true,\n  },\n  {\n    title: '上传人',\n    field: 'createByName',\n  },\n  {\n    title: '服务商',\n    field: 'service',\n  },\n  {\n    field: 'action',\n    fixed: 'right',\n    slots: { default: 'action' },\n    title: '操作',\n    resizable: false,\n    width: 'auto',\n  },\n];\n"
  },
  {
    "path": "apps/web-antd/src/views/system/oss/fallback-image.txt",
    "content": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMIAAADDCAYAAADQvc6UAAABRWlDQ1BJQ0MgUHJvZmlsZQAAKJFjYGASSSwoyGFhYGDIzSspCnJ3UoiIjFJgf8LAwSDCIMogwMCcmFxc4BgQ4ANUwgCjUcG3awyMIPqyLsis7PPOq3QdDFcvjV3jOD1boQVTPQrgSkktTgbSf4A4LbmgqISBgTEFyFYuLykAsTuAbJEioKOA7DkgdjqEvQHEToKwj4DVhAQ5A9k3gGyB5IxEoBmML4BsnSQk8XQkNtReEOBxcfXxUQg1Mjc0dyHgXNJBSWpFCYh2zi+oLMpMzyhRcASGUqqCZ16yno6CkYGRAQMDKMwhqj/fAIcloxgHQqxAjIHBEugw5sUIsSQpBobtQPdLciLEVJYzMPBHMDBsayhILEqEO4DxG0txmrERhM29nYGBddr//5/DGRjYNRkY/l7////39v///y4Dmn+LgeHANwDrkl1AuO+pmgAAADhlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAAqACAAQAAAABAAAAwqADAAQAAAABAAAAwwAAAAD9b/HnAAAHlklEQVR4Ae3dP3PTWBSGcbGzM6GCKqlIBRV0dHRJFarQ0eUT8LH4BnRU0NHR0UEFVdIlFRV7TzRksomPY8uykTk/zewQfKw/9znv4yvJynLv4uLiV2dBoDiBf4qP3/ARuCRABEFAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghgg0Aj8i0JO4OzsrPv69Wv+hi2qPHr0qNvf39+iI97soRIh4f3z58/u7du3SXX7Xt7Z2enevHmzfQe+oSN2apSAPj09TSrb+XKI/f379+08+A0cNRE2ANkupk+ACNPvkSPcAAEibACyXUyfABGm3yNHuAECRNgAZLuYPgEirKlHu7u7XdyytGwHAd8jjNyng4OD7vnz51dbPT8/7z58+NB9+/bt6jU/TI+AGWHEnrx48eJ/EsSmHzx40L18+fLyzxF3ZVMjEyDCiEDjMYZZS5wiPXnyZFbJaxMhQIQRGzHvWR7XCyOCXsOmiDAi1HmPMMQjDpbpEiDCiL358eNHurW/5SnWdIBbXiDCiA38/Pnzrce2YyZ4//59F3ePLNMl4PbpiL2J0L979+7yDtHDhw8vtzzvdGnEXdvUigSIsCLAWavHp/+qM0BcXMd/q25n1vF57TYBp0a3mUzilePj4+7k5KSLb6gt6ydAhPUzXnoPR0dHl79WGTNCfBnn1uvSCJdegQhLI1vvCk+fPu2ePXt2tZOYEV6/fn31dz+shwAR1sP1cqvLntbEN9MxA9xcYjsxS1jWR4AIa2Ibzx0tc44fYX/16lV6NDFLXH+YL32jwiACRBiEbf5KcXoTIsQSpzXx4N28Ja4BQoK7rgXiydbHjx/P25TaQAJEGAguWy0+2Q8PD6/Ki4R8EVl+bzBOnZY95fq9rj9zAkTI2SxdidBHqG9+skdw43borCXO/ZcJdraPWdv22uIEiLA4q7nvvCug8WTqzQveOH26fodo7g6uFe/a17W3+nFBAkRYENRdb1vkkz1CH9cPsVy/jrhr27PqMYvENYNlHAIesRiBYwRy0V+8iXP8+/fvX11Mr7L7ECueb/r48eMqm7FuI2BGWDEG8cm+7G3NEOfmdcTQw4h9/55lhm7DekRYKQPZF2ArbXTAyu4kDYB2YxUzwg0gi/41ztHnfQG26HbGel/crVrm7tNY+/1btkOEAZ2M05r4FB7r9GbAIdxaZYrHdOsgJ/wCEQY0J74TmOKnbxxT9n3FgGGWWsVdowHtjt9Nnvf7yQM2aZU/TIAIAxrw6dOnAWtZZcoEnBpNuTuObWMEiLAx1HY0ZQJEmHJ3HNvGCBBhY6jtaMoEiJB0Z29vL6ls58vxPcO8/zfrdo5qvKO+d3Fx8Wu8zf1dW4p/cPzLly/dtv9Ts/EbcvGAHhHyfBIhZ6NSiIBTo0LNNtScABFyNiqFCBChULMNNSdAhJyNSiECRCjUbEPNCRAhZ6NSiAARCjXbUHMCRMjZqBQiQIRCzTbUnAARcjYqhQgQoVCzDTUnQIScjUohAkQo1GxDzQkQIWejUogAEQo121BzAkTI2agUIkCEQs021JwAEXI2KoUIEKFQsw01J0CEnI1KIQJEKNRsQ80JECFno1KIABEKNdtQcwJEyNmoFCJAhELNNtScABFyNiqFCBChULMNNSdAhJyNSiECRCjUbEPNCRAhZ6NSiAARCjXbUHMCRMjZqBQiQIRCzTbUnAARcjYqhQgQoVCzDTUnQIScjUohAkQo1GxDzQkQIWejUogAEQo121BzAkTI2agUIkCEQs021JwAEXI2KoUIEKFQsw01J0CEnI1KIQJEKNRsQ80JECFno1KIABEKNdtQcwJEyNmoFCJAhELNNtScABFyNiqFCBChULMNNSdAhJyNSiECRCjUbEPNCRAhZ6NSiAARCjXbUHMCRMjZqBQiQIRCzTbUnAARcjYqhQgQoVCzDTUnQIScjUohAkQo1GxDzQkQIWejUogAEQo121BzAkTI2agUIkCEQs021JwAEXI2KoUIEKFQsw01J0CEnI1KIQJEKNRsQ80JECFno1KIABEKNdtQcwJEyNmoFCJAhELNNtScABFyNiqFCBChULMNNSdAhJyNSiEC/wGgKKC4YMA4TAAAAABJRU5ErkJggg==\n"
  },
  {
    "path": "apps/web-antd/src/views/system/oss/file-upload-modal.vue",
    "content": "<script setup lang=\"ts\">\nimport { ref } from 'vue';\n\nimport { useVbenModal } from '@vben/common-ui';\n\nimport { FileUpload } from '#/components/upload';\n\nconst emit = defineEmits<{ reload: [] }>();\n\nconst fileList = ref<string[]>([]);\nconst [BasicModal, modalApi] = useVbenModal({\n  onOpenChange: (isOpen) => {\n    if (isOpen) {\n      return null;\n    }\n    if (fileList.value.length > 0) {\n      fileList.value = [];\n      emit('reload');\n      modalApi.close();\n      return null;\n    }\n  },\n});\n</script>\n\n<template>\n  <BasicModal\n    :close-on-click-modal=\"false\"\n    :footer=\"false\"\n    :fullscreen-button=\"false\"\n    title=\"文件上传\"\n  >\n    <div class=\"flex flex-col gap-4\">\n      <FileUpload\n        v-model:value=\"fileList\"\n        :enable-drag-upload=\"true\"\n        :max-count=\"3\"\n      />\n    </div>\n  </BasicModal>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/system/oss/image-upload-modal.vue",
    "content": "<script setup lang=\"ts\">\nimport { ref } from 'vue';\n\nimport { useVbenModal } from '@vben/common-ui';\n\nimport { ImageUpload } from '#/components/upload';\n\nconst emit = defineEmits<{ reload: [] }>();\n\nconst fileList = ref<string[]>([]);\nconst [BasicModal, modalApi] = useVbenModal({\n  onOpenChange: (isOpen) => {\n    if (isOpen) {\n      return null;\n    }\n    if (fileList.value.length > 0) {\n      fileList.value = [];\n      emit('reload');\n      modalApi.close();\n      return null;\n    }\n  },\n});\n</script>\n\n<template>\n  <BasicModal\n    :close-on-click-modal=\"false\"\n    :footer=\"false\"\n    :fullscreen-button=\"false\"\n    title=\"图片上传\"\n  >\n    <div class=\"flex flex-col gap-4\">\n      <ImageUpload v-model:value=\"fileList\" :max-count=\"3\" />\n    </div>\n  </BasicModal>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/system/oss/index.vue",
    "content": "<script setup lang=\"ts\">\nimport type { VbenFormProps } from '@vben/common-ui';\n\nimport type { VxeGridProps } from '#/adapter/vxe-table';\nimport type { PageQuery } from '#/api/common';\nimport type { OssFile } from '#/api/system/oss/model';\n\nimport { onMounted, ref } from 'vue';\nimport { useRouter } from 'vue-router';\n\nimport { Page, useVbenModal } from '@vben/common-ui';\nimport { useAppConfig } from '@vben/hooks';\nimport { $t } from '@vben/locales';\nimport { stringify } from '@vben/request';\nimport { useAccessStore } from '@vben/stores';\nimport { getVxePopupContainer } from '@vben/utils';\n\nimport {\n  Image,\n  Modal,\n  Popconfirm,\n  Space,\n  Spin,\n  Switch,\n  Tooltip,\n} from 'ant-design-vue';\n\nimport {\n  addSortParams,\n  useVbenVxeGrid,\n  vxeCheckboxChecked,\n} from '#/adapter/vxe-table';\nimport { configInfoByKey } from '#/api/system/config';\nimport { checkLoginBeforeDownload, ossList, ossRemove } from '#/api/system/oss';\nimport { downloadByUrl } from '#/utils/file/download';\n\nimport { supportImageList } from './constant';\nimport { columns, querySchema } from './data';\nimport fallbackImageBase64 from './fallback-image.txt?raw';\nimport fileUploadModal from './file-upload-modal.vue';\nimport imageUploadModal from './image-upload-modal.vue';\n\nconst formOptions: VbenFormProps = {\n  commonConfig: {\n    labelWidth: 80,\n    componentProps: {\n      allowClear: true,\n    },\n  },\n  schema: querySchema(),\n  wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4',\n  // 日期选择格式化\n  fieldMappingTime: [\n    [\n      'createTime',\n      ['params[beginCreateTime]', 'params[endCreateTime]'],\n      ['YYYY-MM-DD 00:00:00', 'YYYY-MM-DD 23:59:59'],\n    ],\n  ],\n};\n\nconst gridOptions: VxeGridProps = {\n  checkboxConfig: {\n    // 高亮\n    highlight: true,\n    // 翻页时保留选中状态\n    reserve: true,\n    // 点击行选中\n    // trigger: 'row',\n  },\n  columns,\n  height: 'auto',\n  keepSource: true,\n  pagerConfig: {},\n  proxyConfig: {\n    ajax: {\n      query: async ({ page, sorts }, formValues = {}) => {\n        const params: PageQuery = {\n          pageNum: page.currentPage,\n          pageSize: page.pageSize,\n          ...formValues,\n        };\n        // 添加排序参数\n        addSortParams(params, sorts);\n        return await ossList(params);\n      },\n    },\n  },\n  headerCellConfig: {\n    height: 44,\n  },\n  cellConfig: {\n    height: 65,\n  },\n  rowConfig: {\n    keyField: 'ossId',\n  },\n  sortConfig: {\n    // 远程排序\n    remote: true,\n    // 支持多字段排序 默认关闭\n    multiple: false,\n  },\n  id: 'system-oss-index',\n};\n\nconst [BasicTable, tableApi] = useVbenVxeGrid({\n  formOptions,\n  gridOptions,\n  gridEvents: {\n    // 排序 重新请求接口\n    sortChange: () => tableApi.query(),\n  },\n});\n\n// async function handleDownload(row: OssFile) {\n//   const downloadSize = ref($t('pages.common.downloadLoading'));\n//   const hideLoading = message.loading({\n//     content: () => downloadSize.value,\n//     duration: 0,\n//   });\n//   try {\n//     const data = await ossDownload(row.ossId, (e) => {\n//       // 计算下载进度\n//       const percent = Math.floor((e.loaded / e.total!) * 100);\n//       // 已经下载\n//       const current = calculateFileSize(e.loaded);\n//       // 总大小\n//       const total = calculateFileSize(e.total!);\n//       downloadSize.value = `已下载: ${current}/${total} (${percent}%)`;\n//     });\n//     downloadByData(data, row.originalName);\n//     message.success('下载完成');\n//   } finally {\n//     hideLoading();\n//   }\n// }\n\nconst { apiURL, clientId } = useAppConfig(\n  import.meta.env,\n  import.meta.env.PROD,\n);\nconst accessStore = useAccessStore();\n\n/**\n * 浏览器直接接管下载 相较于axios请求 不会阻塞\n * @param row oss信息\n */\nasync function handleDownload(row: OssFile) {\n  await checkLoginBeforeDownload();\n\n  const params = {\n    clientid: clientId,\n    Authorization: `Bearer ${accessStore.accessToken}`,\n  };\n\n  const downloadLink = `${apiURL}/resource/oss/download/${row.ossId}?${stringify(params)}`;\n  // fileName不设置也行 默认会取header里的名称\n  downloadByUrl({ fileName: row.fileName, url: downloadLink });\n}\n\nasync function handleDelete(row: OssFile) {\n  await ossRemove([row.ossId]);\n  await tableApi.query();\n}\n\nfunction handleMultiDelete() {\n  const rows = tableApi.grid.getCheckboxRecords();\n  const ids = rows.map((row: OssFile) => row.ossId);\n  Modal.confirm({\n    title: '提示',\n    okType: 'danger',\n    content: `确认删除选中的${ids.length}条记录吗？`,\n    onOk: async () => {\n      await ossRemove(ids);\n      await tableApi.query();\n    },\n  });\n}\n\nconst router = useRouter();\nfunction handleToSettings() {\n  router.push('/system/oss-config/index');\n}\n\nconst preview = ref(false);\nonMounted(async () => {\n  const previewStr = await configInfoByKey('sys.oss.previewListResource');\n  preview.value = previewStr === 'true';\n});\n\n/**\n * 根据拓展名判断是否是图片\n * @param ext 拓展名\n */\nfunction isImageFile(ext: string) {\n  return supportImageList.some((item) =>\n    ext.toLocaleLowerCase().includes(item),\n  );\n}\n\n/**\n * 判断是否是pdf文件\n * @param ext 扩展名\n */\nfunction isPdfFile(ext: string) {\n  return ext.toLocaleLowerCase().includes('pdf');\n}\n\n/**\n * pdf预览 使用浏览器接管\n * @param url 文件地址\n */\nfunction pdfPreview(url: string) {\n  window.open(url);\n}\n\nconst [ImageUploadModal, imageUploadApi] = useVbenModal({\n  connectedComponent: imageUploadModal,\n});\n\nconst [FileUploadModal, fileUploadApi] = useVbenModal({\n  connectedComponent: fileUploadModal,\n});\n</script>\n\n<template>\n  <Page :auto-content-height=\"true\">\n    <BasicTable table-title=\"文件列表\">\n      <template #toolbar-tools>\n        <Space>\n          <Tooltip title=\"预览图片\">\n            <Switch v-model:checked=\"preview\" />\n          </Tooltip>\n          <a-button\n            v-access:code=\"['system:ossConfig:list']\"\n            @click=\"handleToSettings\"\n          >\n            配置管理\n          </a-button>\n          <a-button\n            :disabled=\"!vxeCheckboxChecked(tableApi)\"\n            danger\n            type=\"primary\"\n            v-access:code=\"['system:oss:remove']\"\n            @click=\"handleMultiDelete\"\n          >\n            {{ $t('pages.common.delete') }}\n          </a-button>\n          <a-button\n            v-access:code=\"['system:oss:upload']\"\n            @click=\"fileUploadApi.open\"\n          >\n            文件上传\n          </a-button>\n          <a-button\n            v-access:code=\"['system:oss:upload']\"\n            @click=\"imageUploadApi.open\"\n          >\n            图片上传\n          </a-button>\n        </Space>\n      </template>\n      <template #url=\"{ row }\">\n        <!-- placeholder为图片未加载时显示的占位图 -->\n        <!-- fallback为图片加载失败时显示 -->\n        <!-- 需要设置key属性 否则切换翻页会有延迟 -->\n        <Image\n          :key=\"row.ossId\"\n          v-if=\"preview && isImageFile(row.url)\"\n          :src=\"row.url\"\n          height=\"50px\"\n          :fallback=\"fallbackImageBase64\"\n        >\n          <template #placeholder>\n            <div class=\"flex size-full items-center justify-center\">\n              <Spin />\n            </div>\n          </template>\n        </Image>\n        <!-- pdf预览 使用浏览器开新窗口 -->\n        <span\n          v-else-if=\"preview && isPdfFile(row.url)\"\n          class=\"icon-[vscode-icons--file-type-pdf2] size-10 cursor-pointer\"\n          @click.stop=\"pdfPreview(row.url)\"\n        ></span>\n        <span v-else>{{ row.url }}</span>\n      </template>\n      <template #action=\"{ row }\">\n        <Space>\n          <ghost-button\n            v-access:code=\"['system:oss:download']\"\n            @click=\"handleDownload(row)\"\n          >\n            {{ $t('pages.common.download') }}\n          </ghost-button>\n          <Popconfirm\n            :get-popup-container=\"getVxePopupContainer\"\n            placement=\"left\"\n            title=\"确认删除？\"\n            @confirm=\"handleDelete(row)\"\n          >\n            <ghost-button\n              danger\n              v-access:code=\"['system:oss:remove']\"\n              @click.stop=\"\"\n            >\n              {{ $t('pages.common.delete') }}\n            </ghost-button>\n          </Popconfirm>\n        </Space>\n      </template>\n    </BasicTable>\n    <ImageUploadModal @reload=\"tableApi.query\" />\n    <FileUploadModal @reload=\"tableApi.query\" />\n  </Page>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/system/oss-config/data.tsx",
    "content": "import type { FormSchemaGetter } from '#/adapter/form';\nimport type { VxeGridProps } from '#/adapter/vxe-table';\n\nimport { DictEnum } from '@vben/constants';\n\nimport { Tag } from 'ant-design-vue';\n\nimport { z } from '#/adapter/form';\nimport { getDictOptions } from '#/utils/dict';\n\nconst accessPolicyOptions = [\n  { color: 'orange', label: '私有', value: '0' },\n  { color: 'green', label: '公开', value: '1' },\n  { color: 'blue', label: '自定义', value: '2' },\n];\n\nexport const querySchema: FormSchemaGetter = () => [\n  {\n    component: 'Input',\n    fieldName: 'configKey',\n    label: '配置名称',\n  },\n  {\n    component: 'Input',\n    fieldName: 'bucketName',\n    label: '桶名称',\n  },\n  {\n    component: 'Select',\n    componentProps: {\n      options: [\n        { label: '是', value: '0' },\n        { label: '否', value: '1' },\n      ],\n    },\n    fieldName: 'status',\n    label: '是否默认',\n  },\n];\n\nexport const columns: VxeGridProps['columns'] = [\n  { type: 'checkbox', width: 60 },\n  {\n    title: '配置名称',\n    field: 'configKey',\n  },\n  {\n    title: '访问站点',\n    field: 'endpoint',\n    showOverflow: true,\n  },\n  {\n    title: '桶名称',\n    field: 'bucketName',\n  },\n  {\n    title: '域',\n    field: 'region',\n  },\n  {\n    title: '权限桶类型',\n    field: 'accessPolicy',\n    slots: {\n      default: ({ row }) => {\n        const current = accessPolicyOptions.find(\n          (item) => item.value === row.accessPolicy,\n        );\n        if (current) {\n          return <Tag color={current.color}>{current.label}</Tag>;\n        }\n        return '未知类型';\n      },\n    },\n  },\n  {\n    title: '是否默认',\n    field: 'status',\n    slots: {\n      default: 'status',\n    },\n  },\n  {\n    field: 'action',\n    fixed: 'right',\n    slots: { default: 'action' },\n    title: '操作',\n    resizable: false,\n    width: 'auto',\n  },\n];\n\nexport const drawerSchema: FormSchemaGetter = () => [\n  {\n    component: 'Input',\n    dependencies: {\n      show: () => false,\n      triggerFields: [''],\n    },\n    fieldName: 'ossConfigId',\n  },\n  {\n    component: 'Divider',\n    componentProps: {\n      orientation: 'center',\n    },\n    fieldName: 'divider1',\n    hideLabel: true,\n    renderComponentContent: () => ({\n      default: () => '基本信息',\n    }),\n  },\n  {\n    component: 'Input',\n    fieldName: 'configKey',\n    label: '配置名称',\n    rules: 'required',\n  },\n  {\n    component: 'Input',\n    fieldName: 'endpoint',\n    label: '服务地址',\n    renderComponentContent: (formModel) => ({\n      addonBefore: () => (formModel.isHttps === 'Y' ? 'https://' : 'http://'),\n    }),\n    rules: z\n      .string()\n      .refine((domain) => domain && !/^https?:\\/\\/.*/.test(domain), {\n        message: '请输入正确的域名, 不需要http(s)',\n      }),\n  },\n  {\n    component: 'Input',\n    fieldName: 'domain',\n    label: '自定义域名',\n  },\n  {\n    component: 'Input',\n    fieldName: 'tip',\n    label: '占位作为提示使用',\n    hideLabel: true,\n  },\n  {\n    component: 'Divider',\n    componentProps: {\n      orientation: 'center',\n    },\n    fieldName: 'divider2',\n    hideLabel: true,\n    renderComponentContent: () => ({\n      default: () => '认证信息',\n    }),\n  },\n  {\n    component: 'Input',\n    fieldName: 'accessKey',\n    label: 'accessKey',\n    rules: 'required',\n  },\n  {\n    component: 'Input',\n    fieldName: 'secretKey',\n    label: 'secretKey',\n    rules: 'required',\n  },\n  {\n    component: 'Divider',\n    componentProps: {\n      orientation: 'center',\n    },\n    fieldName: 'divider3',\n    hideLabel: true,\n    renderComponentContent: () => ({\n      default: () => '其他信息',\n    }),\n  },\n  {\n    component: 'Input',\n    fieldName: 'bucketName',\n    label: '桶名称',\n    rules: 'required',\n  },\n  {\n    component: 'Input',\n    fieldName: 'prefix',\n    label: '前缀',\n  },\n  {\n    component: 'RadioGroup',\n    componentProps: {\n      buttonStyle: 'solid',\n      options: accessPolicyOptions,\n      optionType: 'button',\n    },\n    defaultValue: '0',\n    fieldName: 'accessPolicy',\n    formItemClass: 'col-span-6 lg:col-span-3',\n    label: '权限桶类型',\n  },\n  {\n    component: 'RadioGroup',\n    componentProps: {\n      buttonStyle: 'solid',\n      options: getDictOptions(DictEnum.SYS_YES_NO),\n      optionType: 'button',\n    },\n    defaultValue: 'N',\n    fieldName: 'isHttps',\n    formItemClass: 'col-span-6 lg:col-span-3',\n    label: '是否https',\n    rules: 'required',\n  },\n  {\n    component: 'Input',\n    fieldName: 'region',\n    label: '区域',\n  },\n  {\n    component: 'Textarea',\n    fieldName: 'remark',\n    formItemClass: 'items-start',\n    label: '备注',\n  },\n];\n"
  },
  {
    "path": "apps/web-antd/src/views/system/oss-config/index.vue",
    "content": "<script setup lang=\"ts\">\nimport type { VbenFormProps } from '@vben/common-ui';\n\nimport type { VxeGridProps } from '#/adapter/vxe-table';\nimport type { OssConfig } from '#/api/system/oss-config/model';\n\nimport { useAccess } from '@vben/access';\nimport { Page, useVbenDrawer } from '@vben/common-ui';\nimport { getVxePopupContainer } from '@vben/utils';\n\nimport { Modal, Popconfirm, Space } from 'ant-design-vue';\n\nimport { useVbenVxeGrid, vxeCheckboxChecked } from '#/adapter/vxe-table';\nimport {\n  ossConfigChangeStatus,\n  ossConfigList,\n  ossConfigRemove,\n} from '#/api/system/oss-config';\nimport { TableSwitch } from '#/components/table';\n\nimport { columns, querySchema } from './data';\nimport ossConfigDrawer from './oss-config-drawer.vue';\n\nconst formOptions: VbenFormProps = {\n  schema: querySchema(),\n  commonConfig: {\n    labelWidth: 80,\n    componentProps: {\n      allowClear: true,\n    },\n  },\n  wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4',\n};\n\nconst gridOptions: VxeGridProps = {\n  checkboxConfig: {\n    // 高亮\n    highlight: true,\n    // 翻页时保留选中状态\n    reserve: true,\n    // 点击行选中\n    // trigger: 'row',\n  },\n  columns,\n  height: 'auto',\n  keepSource: true,\n  pagerConfig: {},\n  proxyConfig: {\n    ajax: {\n      query: async ({ page }, formValues = {}) => {\n        return await ossConfigList({\n          pageNum: page.currentPage,\n          pageSize: page.pageSize,\n          ...formValues,\n        });\n      },\n    },\n  },\n  rowConfig: {\n    keyField: 'ossConfigId',\n  },\n  id: 'system-oss-config-index',\n};\n\nconst [BasicTable, tableApi] = useVbenVxeGrid({\n  formOptions,\n  gridOptions,\n});\n\nconst [OssConfigDrawer, drawerApi] = useVbenDrawer({\n  connectedComponent: ossConfigDrawer,\n});\n\nfunction handleAdd() {\n  drawerApi.setData({});\n  drawerApi.open();\n}\n\nasync function handleEdit(record: OssConfig) {\n  drawerApi.setData({ id: record.ossConfigId });\n  drawerApi.open();\n}\n\nasync function handleDelete(row: OssConfig) {\n  await ossConfigRemove([row.ossConfigId]);\n  await tableApi.query();\n}\n\nfunction handleMultiDelete() {\n  const rows = tableApi.grid.getCheckboxRecords();\n  const ids = rows.map((row: OssConfig) => row.ossConfigId);\n  Modal.confirm({\n    title: '提示',\n    okType: 'danger',\n    content: `确认删除选中的${ids.length}条记录吗？`,\n    onOk: async () => {\n      await ossConfigRemove(ids);\n      await tableApi.query();\n    },\n  });\n}\n\nconst { hasAccessByCodes } = useAccess();\n</script>\n\n<template>\n  <Page :auto-content-height=\"true\">\n    <BasicTable table-title=\"oss配置列表\">\n      <template #toolbar-tools>\n        <Space>\n          <a-button\n            :disabled=\"!vxeCheckboxChecked(tableApi)\"\n            danger\n            type=\"primary\"\n            v-access:code=\"['system:ossConfig:remove']\"\n            @click=\"handleMultiDelete\"\n          >\n            {{ $t('pages.common.delete') }}\n          </a-button>\n          <a-button\n            type=\"primary\"\n            v-access:code=\"['system:ossConfig:add']\"\n            @click=\"handleAdd\"\n          >\n            {{ $t('pages.common.add') }}\n          </a-button>\n        </Space>\n      </template>\n      <template #status=\"{ row }\">\n        <TableSwitch\n          v-model:value=\"row.status\"\n          :api=\"() => ossConfigChangeStatus(row)\"\n          :disabled=\"!hasAccessByCodes(['system:ossConfig:edit'])\"\n          checked-text=\"是\"\n          un-checked-text=\"否\"\n          @reload=\"tableApi.query()\"\n        />\n      </template>\n      <template #action=\"{ row }\">\n        <Space>\n          <ghost-button\n            v-access:code=\"['system:ossConfig:edit']\"\n            @click=\"handleEdit(row)\"\n          >\n            {{ $t('pages.common.edit') }}\n          </ghost-button>\n          <Popconfirm\n            :get-popup-container=\"getVxePopupContainer\"\n            placement=\"left\"\n            title=\"确认删除？\"\n            @confirm=\"handleDelete(row)\"\n          >\n            <ghost-button\n              danger\n              v-access:code=\"['system:ossConfig:remove']\"\n              @click.stop=\"\"\n            >\n              {{ $t('pages.common.delete') }}\n            </ghost-button>\n          </Popconfirm>\n        </Space>\n      </template>\n    </BasicTable>\n    <OssConfigDrawer @reload=\"tableApi.query()\" />\n  </Page>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/system/oss-config/oss-config-drawer.vue",
    "content": "<script setup lang=\"ts\">\nimport { computed, ref } from 'vue';\n\nimport { useVbenDrawer } from '@vben/common-ui';\nimport { $t } from '@vben/locales';\nimport { cloneDeep } from '@vben/utils';\n\nimport { Alert } from 'ant-design-vue';\n\nimport { useVbenForm } from '#/adapter/form';\nimport {\n  ossConfigAdd,\n  ossConfigInfo,\n  ossConfigUpdate,\n} from '#/api/system/oss-config';\nimport { defaultFormValueGetter, useBeforeCloseDiff } from '#/utils/popup';\n\nimport { drawerSchema } from './data';\n\nconst emit = defineEmits<{ reload: [] }>();\n\nconst isUpdate = ref(false);\nconst title = computed(() => {\n  return isUpdate.value ? $t('pages.common.edit') : $t('pages.common.add');\n});\n\nconst [BasicForm, formApi] = useVbenForm({\n  commonConfig: {\n    formItemClass: 'col-span-6',\n    labelWidth: 100,\n  },\n  schema: drawerSchema(),\n  showDefaultActions: false,\n  wrapperClass: 'grid-cols-6',\n});\n\nconst { onBeforeClose, markInitialized, resetInitialized } = useBeforeCloseDiff(\n  {\n    initializedGetter: defaultFormValueGetter(formApi),\n    currentGetter: defaultFormValueGetter(formApi),\n  },\n);\n\nconst [BasicDrawer, drawerApi] = useVbenDrawer({\n  onBeforeClose,\n  onClosed: handleClosed,\n  onConfirm: handleConfirm,\n  async onOpenChange(isOpen) {\n    if (!isOpen) {\n      return null;\n    }\n    drawerApi.drawerLoading(true);\n\n    const { id } = drawerApi.getData() as { id?: number | string };\n    isUpdate.value = !!id;\n    if (isUpdate.value && id) {\n      const record = await ossConfigInfo(id);\n      await formApi.setValues(record);\n    }\n    await markInitialized();\n\n    drawerApi.drawerLoading(false);\n  },\n});\n\nasync function handleConfirm() {\n  try {\n    drawerApi.lock(true);\n    /**\n     * 这里解构出来的values只能获取到自定义校验参数的值\n     * 需要自行调用formApi.getValues()获取表单值\n     */\n    const { valid } = await formApi.validate();\n    if (!valid) {\n      return;\n    }\n    const data = cloneDeep(await formApi.getValues());\n    await (isUpdate.value ? ossConfigUpdate(data) : ossConfigAdd(data));\n    resetInitialized();\n    emit('reload');\n    drawerApi.close();\n  } catch (error) {\n    console.error(error);\n  } finally {\n    drawerApi.lock(false);\n  }\n}\n\nasync function handleClosed() {\n  await formApi.resetForm();\n  resetInitialized();\n}\n</script>\n\n<template>\n  <BasicDrawer :title=\"title\" class=\"w-[650px]\">\n    <BasicForm>\n      <template #tip>\n        <div class=\"ml-7 w-full\">\n          <Alert show-icon type=\"warning\">\n            <template #message>\n              私有桶(minio)使用自定义域名需要参考\n              <a\n                href=\"https://gitee.com/dromara/RuoYi-Vue-Plus/issues/IBQIKC\"\n                target=\"_blank\"\n                class=\"text-primary\"\n              >\n                支持minio预览私有桶\n              </a>\n              , 否则无法预览\n            </template>\n          </Alert>\n        </div>\n      </template>\n    </BasicForm>\n  </BasicDrawer>\n</template>\n\n<style lang=\"scss\" scoped>\n:deep(.ant-divider) {\n  margin: 8px 0;\n}\n</style>\n"
  },
  {
    "path": "apps/web-antd/src/views/system/post/data.ts",
    "content": "import type { FormSchemaGetter } from '#/adapter/form';\nimport type { VxeGridProps } from '#/adapter/vxe-table';\n\nimport { DictEnum } from '@vben/constants';\nimport { getPopupContainer } from '@vben/utils';\n\nimport { getDictOptions } from '#/utils/dict';\nimport { renderDict } from '#/utils/render';\n\nexport const querySchema: FormSchemaGetter = () => [\n  {\n    component: 'Input',\n    fieldName: 'postCode',\n    label: '岗位编码',\n  },\n  {\n    component: 'Input',\n    fieldName: 'postName',\n    label: '岗位名称',\n  },\n  {\n    component: 'Select',\n    componentProps: {\n      getPopupContainer,\n      options: getDictOptions(DictEnum.SYS_NORMAL_DISABLE),\n    },\n    fieldName: 'status',\n    label: '状态',\n  },\n];\n\nexport const columns: VxeGridProps['columns'] = [\n  { type: 'checkbox', width: 60 },\n  {\n    title: '岗位编码',\n    field: 'postCode',\n  },\n  {\n    title: '类别编码',\n    field: 'postCategory',\n  },\n  {\n    title: '岗位名称',\n    field: 'postName',\n  },\n  {\n    title: '排序',\n    field: 'postSort',\n  },\n  {\n    title: '状态',\n    field: 'status',\n    slots: {\n      default: ({ row }) => {\n        return renderDict(row.status, DictEnum.SYS_NORMAL_DISABLE);\n      },\n    },\n  },\n  {\n    title: '创建时间',\n    field: 'createTime',\n  },\n  {\n    field: 'action',\n    fixed: 'right',\n    slots: { default: 'action' },\n    title: '操作',\n    resizable: false,\n    width: 'auto',\n  },\n];\n\nexport const drawerSchema: FormSchemaGetter = () => [\n  {\n    component: 'Input',\n    dependencies: {\n      show: () => false,\n      triggerFields: [''],\n    },\n    fieldName: 'postId',\n    label: 'postId',\n  },\n  {\n    component: 'TreeSelect',\n    componentProps: {\n      getPopupContainer,\n    },\n    fieldName: 'deptId',\n    label: '所属部门',\n    rules: 'selectRequired',\n  },\n  {\n    component: 'Input',\n    fieldName: 'postName',\n    label: '岗位名称',\n    rules: 'required',\n  },\n  {\n    component: 'Input',\n    fieldName: 'postCode',\n    label: '岗位编码',\n    rules: 'required',\n  },\n  {\n    component: 'Input',\n    fieldName: 'postCategory',\n    label: '类别编码',\n  },\n  {\n    component: 'InputNumber',\n    fieldName: 'postSort',\n    label: '岗位排序',\n    rules: 'required',\n    defaultValue: 0,\n  },\n  {\n    component: 'RadioGroup',\n    componentProps: {\n      buttonStyle: 'solid',\n      options: getDictOptions(DictEnum.SYS_NORMAL_DISABLE),\n      optionType: 'button',\n    },\n    defaultValue: '0',\n    fieldName: 'status',\n    label: '岗位状态',\n    rules: 'required',\n  },\n  {\n    component: 'Textarea',\n    fieldName: 'remark',\n    formItemClass: 'items-start',\n    label: '备注',\n  },\n];\n"
  },
  {
    "path": "apps/web-antd/src/views/system/post/index.vue",
    "content": "<script setup lang=\"ts\">\nimport type { VbenFormProps } from '@vben/common-ui';\n\nimport type { VxeGridProps } from '#/adapter/vxe-table';\nimport type { Post } from '#/api/system/post/model';\n\nimport { ref } from 'vue';\n\nimport { Page, useVbenDrawer } from '@vben/common-ui';\nimport { getVxePopupContainer } from '@vben/utils';\n\nimport { Modal, Popconfirm, Space } from 'ant-design-vue';\n\nimport { useVbenVxeGrid, vxeCheckboxChecked } from '#/adapter/vxe-table';\nimport {\n  postDeptTreeSelect,\n  postExport,\n  postList,\n  postRemove,\n} from '#/api/system/post';\nimport { commonDownloadExcel } from '#/utils/file/download';\nimport DeptTree from '#/views/system/user/dept-tree.vue';\n\nimport { columns, querySchema } from './data';\nimport postDrawer from './post-drawer.vue';\n\n// 左边部门用\nconst selectDeptId = ref<string[]>([]);\nconst formOptions: VbenFormProps = {\n  commonConfig: {\n    labelWidth: 80,\n    componentProps: {\n      allowClear: true,\n    },\n  },\n  schema: querySchema(),\n  wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4',\n  handleReset: async () => {\n    selectDeptId.value = [];\n    const { formApi, reload } = tableApi;\n    await formApi.resetForm();\n    const formValues = formApi.form.values;\n    formApi.setLatestSubmissionValues(formValues);\n    await reload(formValues);\n  },\n};\n\nconst gridOptions: VxeGridProps = {\n  checkboxConfig: {\n    // 高亮\n    highlight: true,\n    // 翻页时保留选中状态\n    reserve: true,\n    trigger: 'cell',\n  },\n  columns,\n  height: 'auto',\n  keepSource: true,\n  pagerConfig: {},\n  proxyConfig: {\n    ajax: {\n      query: async ({ page }, formValues = {}) => {\n        // 部门树选择处理\n        if (selectDeptId.value.length === 1) {\n          formValues.belongDeptId = selectDeptId.value[0];\n        } else {\n          Reflect.deleteProperty(formValues, 'belongDeptId');\n        }\n\n        return await postList({\n          pageNum: page.currentPage,\n          pageSize: page.pageSize,\n          ...formValues,\n        });\n      },\n    },\n  },\n  rowConfig: {\n    keyField: 'postId',\n  },\n  id: 'system-post-index',\n};\n\nconst [BasicTable, tableApi] = useVbenVxeGrid({\n  formOptions,\n  gridOptions,\n});\n\nconst [PostDrawer, drawerApi] = useVbenDrawer({\n  connectedComponent: postDrawer,\n});\n\nfunction handleAdd() {\n  drawerApi.setData({});\n  drawerApi.open();\n}\n\nasync function handleEdit(record: Post) {\n  drawerApi.setData({ id: record.postId });\n  drawerApi.open();\n}\n\nasync function handleDelete(row: Post) {\n  await postRemove([row.postId]);\n  await tableApi.query();\n}\n\nfunction handleMultiDelete() {\n  const rows = tableApi.grid.getCheckboxRecords();\n  const ids = rows.map((row: Post) => row.postId);\n  Modal.confirm({\n    title: '提示',\n    okType: 'danger',\n    content: `确认删除选中的${ids.length}条记录吗？`,\n    onOk: async () => {\n      await postRemove(ids);\n      await tableApi.query();\n    },\n  });\n}\n\nfunction handleDownloadExcel() {\n  commonDownloadExcel(postExport, '岗位信息', tableApi.formApi.form.values);\n}\n</script>\n\n<template>\n  <Page :auto-content-height=\"true\" content-class=\"flex gap-[8px] w-full\">\n    <DeptTree\n      :api=\"postDeptTreeSelect\"\n      v-model:select-dept-id=\"selectDeptId\"\n      class=\"w-[260px]\"\n      @reload=\"() => tableApi.reload()\"\n      @select=\"() => tableApi.reload()\"\n    />\n    <BasicTable class=\"flex-1 overflow-hidden\" table-title=\"岗位列表\">\n      <template #toolbar-tools>\n        <Space>\n          <a-button\n            v-access:code=\"['system:post:export']\"\n            @click=\"handleDownloadExcel\"\n          >\n            {{ $t('pages.common.export') }}\n          </a-button>\n          <a-button\n            :disabled=\"!vxeCheckboxChecked(tableApi)\"\n            danger\n            type=\"primary\"\n            v-access:code=\"['system:post:remove']\"\n            @click=\"handleMultiDelete\"\n          >\n            {{ $t('pages.common.delete') }}\n          </a-button>\n          <a-button\n            type=\"primary\"\n            v-access:code=\"['system:post:add']\"\n            @click=\"handleAdd\"\n          >\n            {{ $t('pages.common.add') }}\n          </a-button>\n        </Space>\n      </template>\n      <template #action=\"{ row }\">\n        <Space>\n          <GhostButton\n            v-access:code=\"['system:post:edit']\"\n            @click=\"handleEdit(row)\"\n          >\n            {{ $t('pages.common.edit') }}\n          </GhostButton>\n          <Popconfirm\n            :get-popup-container=\"getVxePopupContainer\"\n            placement=\"left\"\n            title=\"确认删除？\"\n            @confirm=\"handleDelete(row)\"\n          >\n            <GhostButton\n              danger\n              v-access:code=\"['system:post:remove']\"\n              @click.stop=\"\"\n            >\n              {{ $t('pages.common.delete') }}\n            </GhostButton>\n          </Popconfirm>\n        </Space>\n      </template>\n    </BasicTable>\n    <PostDrawer @reload=\"tableApi.query()\" />\n  </Page>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/system/post/post-drawer.vue",
    "content": "<script setup lang=\"ts\">\nimport { computed, ref } from 'vue';\n\nimport { useVbenDrawer } from '@vben/common-ui';\nimport { $t } from '@vben/locales';\nimport { addFullName, cloneDeep } from '@vben/utils';\n\nimport { useVbenForm } from '#/adapter/form';\nimport { postAdd, postInfo, postUpdate } from '#/api/system/post';\nimport { getDeptTree } from '#/api/system/user';\nimport { defaultFormValueGetter, useBeforeCloseDiff } from '#/utils/popup';\n\nimport { drawerSchema } from './data';\n\nconst emit = defineEmits<{ reload: [] }>();\n\nconst isUpdate = ref(false);\nconst title = computed(() => {\n  return isUpdate.value ? $t('pages.common.edit') : $t('pages.common.add');\n});\n\nconst [BasicForm, formApi] = useVbenForm({\n  commonConfig: {\n    formItemClass: 'col-span-2',\n    componentProps: {\n      class: 'w-full',\n    },\n    labelWidth: 80,\n  },\n  schema: drawerSchema(),\n  showDefaultActions: false,\n  wrapperClass: 'grid-cols-2',\n});\n\nasync function setupDeptSelect() {\n  const deptTree = await getDeptTree();\n  // 选中后显示在输入框的值 即父节点 / 子节点\n  addFullName(deptTree, 'label', ' / ');\n  formApi.updateSchema([\n    {\n      componentProps: {\n        fieldNames: { label: 'label', value: 'id' },\n        treeData: deptTree,\n        treeDefaultExpandAll: true,\n        treeLine: { showLeafIcon: false },\n        // 选中后显示在输入框的值\n        treeNodeLabelProp: 'fullName',\n      },\n      fieldName: 'deptId',\n    },\n  ]);\n}\n\nconst { onBeforeClose, markInitialized, resetInitialized } = useBeforeCloseDiff(\n  {\n    initializedGetter: defaultFormValueGetter(formApi),\n    currentGetter: defaultFormValueGetter(formApi),\n  },\n);\n\nconst [BasicDrawer, drawerApi] = useVbenDrawer({\n  onBeforeClose,\n  onClosed: handleClosed,\n  onConfirm: handleConfirm,\n  async onOpenChange(isOpen) {\n    if (!isOpen) {\n      return null;\n    }\n    drawerApi.drawerLoading(true);\n    const { id } = drawerApi.getData() as { id?: number | string };\n    isUpdate.value = !!id;\n    // 初始化\n    await setupDeptSelect();\n    // 更新 && 赋值\n    if (isUpdate.value && id) {\n      const record = await postInfo(id);\n      await formApi.setValues(record);\n    }\n    await markInitialized();\n    drawerApi.drawerLoading(false);\n  },\n});\n\nasync function handleConfirm() {\n  try {\n    drawerApi.lock(true);\n    const { valid } = await formApi.validate();\n    if (!valid) {\n      return;\n    }\n    const data = cloneDeep(await formApi.getValues());\n    await (isUpdate.value ? postUpdate(data) : postAdd(data));\n    resetInitialized();\n    emit('reload');\n    drawerApi.close();\n  } catch (error) {\n    console.error(error);\n  } finally {\n    drawerApi.lock(false);\n  }\n}\n\nasync function handleClosed() {\n  await formApi.resetForm();\n  resetInitialized();\n}\n</script>\n\n<template>\n  <BasicDrawer :title=\"title\" class=\"w-[600px]\">\n    <BasicForm />\n  </BasicDrawer>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/system/role/authUser.vue",
    "content": "<!--\n后端版本>=5.4.0  这个从本地路由变为从后台返回\n未修改文件名 而是新加了这个文件\n-->\n<script setup lang=\"ts\">\nimport RoleAssignPage from '#/views/system/role-assign/index.vue';\n</script>\n\n<template>\n  <RoleAssignPage />\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/system/role/data.tsx",
    "content": "import type { FormSchemaGetter } from '#/adapter/form';\nimport type { VxeGridProps } from '#/adapter/vxe-table';\n\nimport { markRaw } from 'vue';\n\nimport { DictEnum } from '@vben/constants';\nimport { getPopupContainer } from '@vben/utils';\n\nimport { Tag } from 'ant-design-vue';\n\nimport { DefaultSlot } from '#/components/global/slot';\nimport { TreeSelectPanel } from '#/components/tree';\nimport { getDictOptions } from '#/utils/dict';\n\n/**\n * authScopeOptions user也会用到\n */\nexport const authScopeOptions = [\n  { color: 'green', label: '全部数据权限', value: '1' },\n  { color: 'default', label: '自定数据权限', value: '2' },\n  { color: 'orange', label: '本部门数据权限', value: '3' },\n  { color: 'cyan', label: '本部门及以下数据权限', value: '4' },\n  { color: 'error', label: '仅本人数据权限', value: '5' },\n  { color: 'default', label: '部门及以下或本人数据权限', value: '6' },\n];\n\nexport const querySchema: FormSchemaGetter = () => [\n  {\n    component: 'Input',\n    fieldName: 'roleName',\n    label: '角色名称',\n  },\n  {\n    component: 'Input',\n    fieldName: 'roleKey',\n    label: '权限字符',\n  },\n  {\n    component: 'Select',\n    componentProps: {\n      options: getDictOptions(DictEnum.SYS_NORMAL_DISABLE),\n    },\n    fieldName: 'status',\n    label: '状态',\n  },\n  {\n    component: 'RangePicker',\n    fieldName: 'createTime',\n    label: '创建时间',\n  },\n];\n\nexport const columns: VxeGridProps['columns'] = [\n  { type: 'checkbox', width: 60 },\n  {\n    title: '角色名称',\n    field: 'roleName',\n  },\n  {\n    title: '权限字符',\n    field: 'roleKey',\n    slots: {\n      default: ({ row }) => {\n        return <Tag color=\"processing\">{row.roleKey}</Tag>;\n      },\n    },\n  },\n  {\n    title: '数据权限',\n    field: 'dataScope',\n    slots: {\n      default: ({ row }) => {\n        const found = authScopeOptions.find(\n          (item) => item.value === row.dataScope,\n        );\n        if (found) {\n          return <Tag color={found.color}>{found.label}</Tag>;\n        }\n        return <Tag>{row.dataScope}</Tag>;\n      },\n    },\n  },\n  {\n    title: '排序',\n    field: 'roleSort',\n  },\n  {\n    title: '状态',\n    field: 'status',\n    slots: { default: 'status' },\n  },\n  {\n    title: '创建时间',\n    field: 'createTime',\n  },\n  {\n    field: 'action',\n    fixed: 'right',\n    slots: { default: 'action' },\n    title: '操作',\n    resizable: false,\n    width: 'auto',\n  },\n];\n\nexport const drawerSchema: FormSchemaGetter = () => [\n  {\n    component: 'Input',\n    dependencies: {\n      show: () => false,\n      triggerFields: [''],\n    },\n    fieldName: 'roleId',\n    label: '角色ID',\n  },\n  {\n    component: 'Input',\n    fieldName: 'roleName',\n    label: '角色名称',\n    rules: 'required',\n  },\n  {\n    component: 'Input',\n    fieldName: 'roleKey',\n    help: '如: test simpleUser等',\n    label: '权限标识',\n    rules: 'required',\n  },\n  {\n    component: 'InputNumber',\n    fieldName: 'roleSort',\n    label: '角色排序',\n    rules: 'required',\n    defaultValue: 0,\n  },\n  {\n    component: 'Select',\n    componentProps: {\n      allowClear: false,\n      options: getDictOptions(DictEnum.SYS_NORMAL_DISABLE),\n      getPopupContainer,\n    },\n    defaultValue: '0',\n    fieldName: 'status',\n    help: '修改后, 拥有该角色的用户将自动下线.',\n    label: '角色状态',\n    rules: 'required',\n  },\n  {\n    component: 'Radio',\n    dependencies: {\n      show: () => false,\n      triggerFields: [''],\n    },\n    fieldName: 'menuCheckStrictly',\n    label: '菜单权限',\n  },\n  {\n    component: 'Input',\n    defaultValue: [],\n    fieldName: 'menuIds',\n    label: '菜单权限',\n    formItemClass: 'col-span-2',\n  },\n  {\n    component: 'Textarea',\n    defaultValue: '',\n    fieldName: 'remark',\n    formItemClass: 'col-span-2',\n    label: '备注',\n  },\n];\n\nexport const authModalSchemas: FormSchemaGetter = () => [\n  {\n    component: 'Input',\n    dependencies: {\n      show: () => false,\n      triggerFields: [''],\n    },\n    fieldName: 'roleId',\n    label: '角色ID',\n  },\n  {\n    component: 'Input',\n    componentProps: {\n      disabled: true,\n    },\n    fieldName: 'roleName',\n    label: '角色名称',\n  },\n  {\n    component: 'Input',\n    componentProps: {\n      disabled: true,\n    },\n    fieldName: 'roleKey',\n    label: '权限标识',\n  },\n  {\n    component: 'Select',\n    componentProps: {\n      allowClear: false,\n      getPopupContainer,\n      options: authScopeOptions,\n    },\n    fieldName: 'dataScope',\n    help: '更改后需要用户重新登录才能生效',\n    label: '权限范围',\n  },\n  {\n    component: 'Radio',\n    dependencies: {\n      show: () => false,\n      triggerFields: [''],\n    },\n    fieldName: 'deptCheckStrictly',\n    label: 'deptCheckStrictly',\n  },\n  {\n    // 这种的场景基本上是一个组件需要绑定两个或以上的场景\n    component: markRaw(DefaultSlot),\n    defaultValue: [],\n    componentProps: {\n      rootDivAttrs: {\n        class: 'w-full',\n      },\n    },\n    dependencies: {\n      show: (values) => values.dataScope === '2',\n      triggerFields: ['dataScope'],\n    },\n    renderComponentContent: (model) => ({\n      default: (attrs: any) => {\n        return (\n          <TreeSelectPanel\n            expand-all-on-init={true}\n            treeData={attrs.treeData}\n            v-model:checkStrictly={model.deptCheckStrictly}\n            v-model:value={model.deptIds}\n          />\n        );\n      },\n    }),\n    fieldName: 'deptIds',\n    help: '更改后立即生效',\n    label: '部门权限',\n  },\n];\n"
  },
  {
    "path": "apps/web-antd/src/views/system/role/index.vue",
    "content": "<script setup lang=\"ts\">\nimport type { VbenFormProps } from '@vben/common-ui';\n\nimport type { VxeGridProps } from '#/adapter/vxe-table';\nimport type { Role } from '#/api/system/role/model';\n\nimport { computed } from 'vue';\nimport { useRouter } from 'vue-router';\n\nimport { useAccess } from '@vben/access';\nimport { Page, useVbenDrawer, useVbenModal } from '@vben/common-ui';\nimport { getVxePopupContainer } from '@vben/utils';\n\nimport { Modal, Popconfirm, Space } from 'ant-design-vue';\n\nimport { useVbenVxeGrid, vxeCheckboxChecked } from '#/adapter/vxe-table';\nimport {\n  roleChangeStatus,\n  roleExport,\n  roleList,\n  roleRemove,\n} from '#/api/system/role';\nimport { TableSwitch } from '#/components/table';\nimport { commonDownloadExcel } from '#/utils/file/download';\n\nimport { columns, querySchema } from './data';\nimport roleAuthModal from './role-auth-modal.vue';\nimport roleDrawer from './role-drawer.vue';\n\nconst formOptions: VbenFormProps = {\n  commonConfig: {\n    labelWidth: 80,\n    componentProps: {\n      allowClear: true,\n    },\n  },\n  schema: querySchema(),\n  wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4',\n  // 日期选择格式化\n  fieldMappingTime: [\n    [\n      'createTime',\n      ['params[beginTime]', 'params[endTime]'],\n      ['YYYY-MM-DD 00:00:00', 'YYYY-MM-DD 23:59:59'],\n    ],\n  ],\n};\n\nconst gridOptions: VxeGridProps = {\n  checkboxConfig: {\n    // 高亮\n    highlight: true,\n    // 翻页时保留选中状态\n    reserve: true,\n    // 点击行选中\n    // trigger: 'row',\n    checkMethod: ({ row }) => row.roleId !== 1,\n  },\n  columns,\n  height: 'auto',\n  keepSource: true,\n  pagerConfig: {},\n  proxyConfig: {\n    ajax: {\n      query: async ({ page }, formValues = {}) => {\n        return await roleList({\n          pageNum: page.currentPage,\n          pageSize: page.pageSize,\n          ...formValues,\n        });\n      },\n    },\n  },\n  rowConfig: {\n    keyField: 'roleId',\n  },\n  id: 'system-role-index',\n};\n\nconst [BasicTable, tableApi] = useVbenVxeGrid({\n  formOptions,\n  gridOptions,\n});\nconst [RoleDrawer, drawerApi] = useVbenDrawer({\n  connectedComponent: roleDrawer,\n});\n\nfunction handleAdd() {\n  drawerApi.setData({});\n  drawerApi.open();\n}\n\nasync function handleEdit(record: Role) {\n  drawerApi.setData({ id: record.roleId });\n  drawerApi.open();\n}\n\nasync function handleDelete(row: Role) {\n  await roleRemove([row.roleId]);\n  await tableApi.query();\n}\n\nfunction handleMultiDelete() {\n  const rows = tableApi.grid.getCheckboxRecords();\n  const ids = rows.map((row: Role) => row.roleId);\n  Modal.confirm({\n    title: '提示',\n    okType: 'danger',\n    content: `确认删除选中的${ids.length}条记录吗？`,\n    onOk: async () => {\n      await roleRemove(ids);\n      await tableApi.query();\n    },\n  });\n}\n\nfunction handleDownloadExcel() {\n  commonDownloadExcel(roleExport, '角色数据', tableApi.formApi.form.values, {\n    fieldMappingTime: formOptions.fieldMappingTime,\n  });\n}\n\nconst { hasAccessByCodes, hasAccessByRoles } = useAccess();\n\nconst isSuperAdmin = computed(() => hasAccessByRoles(['superadmin']));\n\nconst [RoleAuthModal, authModalApi] = useVbenModal({\n  connectedComponent: roleAuthModal,\n});\n\nfunction handleAuthEdit(record: Role) {\n  authModalApi.setData({ id: record.roleId });\n  authModalApi.open();\n}\n\nconst router = useRouter();\nfunction handleAssignRole(record: Role) {\n  router.push(`/system/role-auth/user/${record.roleId}`);\n}\n</script>\n\n<template>\n  <Page :auto-content-height=\"true\">\n    <BasicTable table-title=\"角色列表\">\n      <template #toolbar-tools>\n        <Space>\n          <a-button\n            v-access:code=\"['system:role:export']\"\n            @click=\"handleDownloadExcel\"\n          >\n            {{ $t('pages.common.export') }}\n          </a-button>\n          <a-button\n            :disabled=\"!vxeCheckboxChecked(tableApi)\"\n            danger\n            type=\"primary\"\n            v-access:code=\"['system:role:remove']\"\n            @click=\"handleMultiDelete\"\n          >\n            {{ $t('pages.common.delete') }}\n          </a-button>\n          <a-button\n            type=\"primary\"\n            v-access:code=\"['system:role:add']\"\n            @click=\"handleAdd\"\n          >\n            {{ $t('pages.common.add') }}\n          </a-button>\n        </Space>\n      </template>\n      <template #status=\"{ row }\">\n        <TableSwitch\n          v-model:value=\"row.status\"\n          :api=\"() => roleChangeStatus(row)\"\n          :disabled=\"\n            row.roleId === 1 ||\n            row.roleKey === 'admin' ||\n            !hasAccessByCodes(['system:role:edit'])\n          \"\n          @reload=\"tableApi.query()\"\n        />\n      </template>\n      <template #action=\"{ row }\">\n        <!-- 租户管理员不可修改admin角色 防止误操作 -->\n        <!-- 超级管理员可通过租户切换来操作租户管理员角色 -->\n        <template\n          v-if=\"!row.superAdmin && (row.roleKey !== 'admin' || isSuperAdmin)\"\n        >\n          <Space>\n            <ghost-button\n              v-access:code=\"['system:role:edit']\"\n              @click.stop=\"handleEdit(row)\"\n            >\n              {{ $t('pages.common.edit') }}\n            </ghost-button>\n            <ghost-button\n              v-access:code=\"['system:role:edit']\"\n              @click.stop=\"handleAuthEdit(row)\"\n            >\n              权限\n            </ghost-button>\n            <ghost-button\n              v-access:code=\"['system:role:edit']\"\n              @click.stop=\"handleAssignRole(row)\"\n            >\n              分配\n            </ghost-button>\n            <Popconfirm\n              :get-popup-container=\"getVxePopupContainer\"\n              placement=\"left\"\n              title=\"确认删除？\"\n              @confirm=\"handleDelete(row)\"\n            >\n              <ghost-button\n                danger\n                v-access:code=\"['system:role:remove']\"\n                @click.stop=\"\"\n              >\n                {{ $t('pages.common.delete') }}\n              </ghost-button>\n            </Popconfirm>\n          </Space>\n        </template>\n      </template>\n    </BasicTable>\n    <RoleDrawer @reload=\"tableApi.query()\" />\n    <RoleAuthModal @reload=\"tableApi.query()\" />\n  </Page>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/system/role/role-auth-modal.vue",
    "content": "<script setup lang=\"ts\">\nimport type { DeptOption } from '#/api/system/role/model';\n\nimport { useVbenModal } from '@vben/common-ui';\nimport { cloneDeep, findGroupParentIds } from '@vben/utils';\n\nimport { uniq } from 'lodash-es';\n\nimport { useVbenForm } from '#/adapter/form';\nimport { roleDataScope, roleDeptTree, roleInfo } from '#/api/system/role';\nimport { defaultFormValueGetter, useBeforeCloseDiff } from '#/utils/popup';\n\nimport { authModalSchemas } from './data';\n\nconst emit = defineEmits<{ reload: [] }>();\n\nconst [BasicForm, formApi] = useVbenForm({\n  commonConfig: {\n    componentProps: {\n      class: 'w-full',\n    },\n  },\n  layout: 'vertical',\n  schema: authModalSchemas(),\n  showDefaultActions: false,\n});\n\n/**\n * 保存部门数据 用于获取祖先节点\n */\nlet treeData: DeptOption[] = [];\nasync function setupDeptTree(id: number | string) {\n  const resp = await roleDeptTree(id);\n  const { checkedKeys, depts } = resp;\n\n  /**\n   * 设置部门树数据\n   */\n  formApi.updateSchema([\n    { fieldName: 'deptIds', componentProps: { treeData: depts } },\n  ]);\n  /**\n   * 设置选中 必须先传递treeData\n   * Note: Tree missing follow keys: '1981565541727186945'\n   */\n  await formApi.setFieldValue('deptIds', checkedKeys);\n  treeData = depts;\n}\n\nconst { onBeforeClose, markInitialized, resetInitialized } = useBeforeCloseDiff(\n  {\n    initializedGetter: defaultFormValueGetter(formApi),\n    currentGetter: defaultFormValueGetter(formApi),\n  },\n);\n\nconst [BasicModal, modalApi] = useVbenModal({\n  fullscreenButton: false,\n  onBeforeClose,\n  onClosed: handleClosed,\n  onConfirm: handleConfirm,\n  onOpenChange: async (isOpen) => {\n    if (!isOpen) {\n      treeData = [];\n      return null;\n    }\n    modalApi.modalLoading(true);\n\n    const { id } = modalApi.getData() as { id: number | string };\n\n    const [record] = await Promise.all([roleInfo(id), setupDeptTree(id)]);\n    await formApi.setValues(record);\n    markInitialized();\n\n    modalApi.modalLoading(false);\n  },\n});\n\nasync function handleConfirm() {\n  try {\n    modalApi.lock(true);\n    const { valid } = await formApi.validate();\n    if (!valid) {\n      return;\n    }\n    // formApi.getValues拿到的是一个readonly对象，不能直接修改，需要cloneDeep\n    const data = cloneDeep(await formApi.getValues());\n    // 不为自定义权限的话 删除部门id\n    if (data.dataScope === '2') {\n      let { deptIds, deptCheckStrictly } = data;\n      // 节点关联 需要拼接上祖级ID(获取的是不带的)\n      if (deptCheckStrictly) {\n        // 找到所有父级ID\n        const parentIds = findGroupParentIds(treeData, deptIds, { id: 'id' });\n        // 去重\n        deptIds = uniq([...parentIds, ...deptIds]);\n      }\n      // 赋值\n      data.deptIds = deptIds;\n    } else {\n      data.deptIds = [];\n    }\n    await roleDataScope(data);\n    resetInitialized();\n    emit('reload');\n    modalApi.close();\n  } catch (error) {\n    console.error(error);\n  } finally {\n    modalApi.lock(false);\n  }\n}\n\nasync function handleClosed() {\n  await formApi.resetForm();\n  resetInitialized();\n}\n</script>\n\n<template>\n  <BasicModal class=\"min-h-[600px] w-[550px]\" title=\"分配权限\">\n    <BasicForm />\n  </BasicModal>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/system/role/role-drawer.vue",
    "content": "<!--\nTODO: 这个页面要优化逻辑\n-->\n<script setup lang=\"ts\">\nimport type { MenuOption } from '#/api/system/menu/model';\n\nimport { computed, nextTick, ref } from 'vue';\n\nimport { useVbenDrawer } from '@vben/common-ui';\nimport { $t } from '@vben/locales';\nimport { cloneDeep, eachTree } from '@vben/utils';\n\nimport { useVbenForm } from '#/adapter/form';\nimport { menuTreeSelect, roleMenuTreeSelect } from '#/api/system/menu';\nimport { roleAdd, roleInfo, roleUpdate } from '#/api/system/role';\nimport { MenuSelectTable } from '#/components/tree';\nimport { defaultFormValueGetter, useBeforeCloseDiff } from '#/utils/popup';\n\nimport { drawerSchema } from './data';\n\nconst emit = defineEmits<{ reload: [] }>();\n\nconst isUpdate = ref(false);\nconst title = computed(() => {\n  return isUpdate.value ? $t('pages.common.edit') : $t('pages.common.add');\n});\n\nconst [BasicForm, formApi] = useVbenForm({\n  commonConfig: {\n    componentProps: {\n      class: 'w-full',\n    },\n    formItemClass: 'col-span-1',\n  },\n  layout: 'vertical',\n  schema: drawerSchema(),\n  showDefaultActions: false,\n  wrapperClass: 'grid-cols-2 gap-x-4',\n});\n\nconst menuTree = ref<MenuOption[]>([]);\nasync function setupMenuTree(id?: number | string) {\n  if (id) {\n    const resp = await roleMenuTreeSelect(id);\n    const menus = resp.menus;\n    // i18n处理\n    eachTree(menus, (node) => {\n      node.label = $t(node.label);\n    });\n    // 设置菜单信息\n    menuTree.value = resp.menus;\n    // keys依赖于menu 需要先加载menu\n    await nextTick();\n    await formApi.setFieldValue('menuIds', resp.checkedKeys);\n  } else {\n    const resp = await menuTreeSelect();\n    // i18n处理\n    eachTree(resp, (node) => {\n      node.label = $t(node.label);\n    });\n    // 设置菜单信息\n    menuTree.value = resp;\n    // keys依赖于menu 需要先加载menu\n    await nextTick();\n    await formApi.setFieldValue('menuIds', []);\n  }\n}\n\nasync function customFormValueGetter() {\n  const v = await defaultFormValueGetter(formApi)();\n  // 获取勾选信息\n  const menuIds = menuSelectRef.value?.getCheckedKeys?.() ?? [];\n  const mixStr = v + menuIds.join(',');\n  return mixStr;\n}\n\nconst { onBeforeClose, markInitialized, resetInitialized } = useBeforeCloseDiff(\n  {\n    initializedGetter: customFormValueGetter,\n    currentGetter: customFormValueGetter,\n  },\n);\n\nconst [BasicDrawer, drawerApi] = useVbenDrawer({\n  onBeforeClose,\n  onClosed: handleClosed,\n  onConfirm: handleConfirm,\n  destroyOnClose: true,\n  async onOpenChange(isOpen) {\n    if (!isOpen) {\n      return null;\n    }\n    drawerApi.drawerLoading(true);\n\n    const { id } = drawerApi.getData() as { id?: number | string };\n    isUpdate.value = !!id;\n\n    if (isUpdate.value && id) {\n      const record = await roleInfo(id);\n      await formApi.setValues(record);\n    }\n    // init菜单 注意顺序要放在赋值record之后 内部watch会依赖record\n    await setupMenuTree(id);\n    await markInitialized();\n\n    drawerApi.drawerLoading(false);\n  },\n});\n\nconst menuSelectRef = ref<InstanceType<typeof MenuSelectTable>>();\nasync function handleConfirm() {\n  try {\n    drawerApi.lock(true);\n\n    const { valid } = await formApi.validate();\n    if (!valid) {\n      return;\n    }\n    // 这个用于提交\n    const menuIds = menuSelectRef.value?.getCheckedKeys?.() ?? [];\n    // formApi.getValues拿到的是一个readonly对象，不能直接修改，需要cloneDeep\n    const data = cloneDeep(await formApi.getValues());\n    data.menuIds = menuIds;\n    await (isUpdate.value ? roleUpdate(data) : roleAdd(data));\n    emit('reload');\n    resetInitialized();\n    drawerApi.close();\n  } catch (error) {\n    console.error(error);\n  } finally {\n    drawerApi.lock(false);\n  }\n}\n\nasync function handleClosed() {\n  await formApi.resetForm();\n  resetInitialized();\n}\n\n/**\n * 通过回调更新 无法通过v-model\n * @param value 菜单选择是否严格模式\n */\nfunction handleMenuCheckStrictlyChange(value: boolean) {\n  formApi.setFieldValue('menuCheckStrictly', value);\n}\n</script>\n\n<template>\n  <BasicDrawer :title=\"title\" class=\"w-[800px]\">\n    <BasicForm>\n      <template #menuIds=\"slotProps\">\n        <div class=\"h-[600px] w-full\">\n          <!-- association为readonly 不能通过v-model绑定 -->\n          <MenuSelectTable\n            ref=\"menuSelectRef\"\n            :checked-keys=\"slotProps.value\"\n            :association=\"formApi.form.values.menuCheckStrictly\"\n            :menus=\"menuTree\"\n            @update:association=\"handleMenuCheckStrictlyChange\"\n          />\n        </div>\n      </template>\n    </BasicForm>\n  </BasicDrawer>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/system/role-assign/data.tsx",
    "content": "import type { FormSchemaGetter } from '#/adapter/form';\nimport type { VxeGridProps } from '#/adapter/vxe-table';\n\nexport const querySchema: FormSchemaGetter = () => [\n  {\n    component: 'Input',\n    fieldName: 'userName',\n    label: '用户账号',\n  },\n  {\n    component: 'Input',\n    fieldName: 'phonenumber',\n    label: '手机号码',\n  },\n];\n\nexport const columns: VxeGridProps['columns'] = [\n  { type: 'checkbox', width: 60 },\n  {\n    title: '用户账号',\n    field: 'userName',\n  },\n  {\n    title: '用户昵称',\n    field: 'nickName',\n  },\n  {\n    title: '邮箱',\n    field: 'email',\n  },\n  {\n    title: '手机号',\n    field: 'phonenumber',\n  },\n  {\n    field: 'action',\n    fixed: 'right',\n    slots: { default: 'action' },\n    title: '操作',\n    resizable: false,\n    width: 'auto',\n  },\n];\n"
  },
  {
    "path": "apps/web-antd/src/views/system/role-assign/index.vue",
    "content": "<script setup lang=\"ts\">\nimport type { VbenFormProps } from '@vben/common-ui';\n\nimport type { VxeGridProps } from '#/adapter/vxe-table';\nimport type { User } from '#/api/system/user/model';\n\nimport { useRoute } from 'vue-router';\n\nimport { Page, useVbenDrawer } from '@vben/common-ui';\nimport { getVxePopupContainer } from '@vben/utils';\n\nimport { Modal, Popconfirm, Space } from 'ant-design-vue';\n\nimport { useVbenVxeGrid, vxeCheckboxChecked } from '#/adapter/vxe-table';\nimport {\n  roleAllocatedList,\n  roleAuthCancel,\n  roleAuthCancelAll,\n} from '#/api/system/role';\n\nimport { columns, querySchema } from './data';\nimport roleAssignDrawer from './role-assign-drawer.vue';\n\nconst route = useRoute();\nconst roleId = route.params.roleId as string;\n\nconst formOptions: VbenFormProps = {\n  commonConfig: {\n    labelWidth: 80,\n    componentProps: {\n      allowClear: true,\n    },\n  },\n  schema: querySchema(),\n  wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4',\n};\n\nconst gridOptions: VxeGridProps = {\n  checkboxConfig: {\n    // 高亮\n    highlight: true,\n    // 翻页时保留选中状态\n    reserve: true,\n    // 点击行选中\n    // trigger: 'row',\n  },\n  columns,\n  height: 'auto',\n  keepSource: true,\n  pagerConfig: {},\n  proxyConfig: {\n    ajax: {\n      query: async ({ page }, formValues = {}) => {\n        return await roleAllocatedList({\n          pageNum: page.currentPage,\n          pageSize: page.pageSize,\n          roleId,\n          ...formValues,\n        });\n      },\n    },\n  },\n  rowConfig: {\n    keyField: 'userId',\n  },\n  id: 'system-role-assign-index',\n};\n\nconst [BasicTable, tableApi] = useVbenVxeGrid({\n  formOptions,\n  gridOptions,\n});\n\nconst [RoleAssignDrawer, drawerApi] = useVbenDrawer({\n  connectedComponent: roleAssignDrawer,\n});\n\nfunction handleAdd() {\n  drawerApi.setData({});\n  drawerApi.open();\n}\n\n/**\n * 取消授权 一条记录\n */\nasync function handleAuthCancel(record: User) {\n  await roleAuthCancel({ userId: record.userId, roleId });\n  await tableApi.query();\n}\n\n/**\n * 批量取消授权\n */\nfunction handleMultipleAuthCancel() {\n  const rows = tableApi.grid.getCheckboxRecords();\n  const ids = rows.map((row: User) => row.userId);\n  Modal.confirm({\n    title: '提示',\n    okType: 'danger',\n    content: `确认取消选中的${ids.length}条授权记录吗？`,\n    onOk: async () => {\n      await roleAuthCancelAll(roleId, ids);\n      await tableApi.query();\n      tableApi.grid.clearCheckboxRow();\n    },\n  });\n}\n</script>\n\n<template>\n  <Page :auto-content-height=\"true\">\n    <BasicTable table-title=\"已分配的用户列表\">\n      <template #toolbar-tools>\n        <Space>\n          <a-button\n            :disabled=\"!vxeCheckboxChecked(tableApi)\"\n            danger\n            type=\"primary\"\n            v-access:code=\"['system:role:remove']\"\n            @click=\"handleMultipleAuthCancel\"\n          >\n            取消授权\n          </a-button>\n          <a-button\n            type=\"primary\"\n            v-access:code=\"['system:role:add']\"\n            @click=\"handleAdd\"\n          >\n            {{ $t('pages.common.add') }}\n          </a-button>\n        </Space>\n      </template>\n      <template #action=\"{ row }\">\n        <Popconfirm\n          :get-popup-container=\"getVxePopupContainer\"\n          :title=\"`是否取消授权用户[${row.userName} - ${row.nickName}]?`\"\n          placement=\"left\"\n          @confirm=\"handleAuthCancel(row)\"\n        >\n          <ghost-button\n            danger\n            v-access:code=\"['system:role:remove']\"\n            @click.stop=\"\"\n          >\n            取消授权\n          </ghost-button>\n        </Popconfirm>\n      </template>\n    </BasicTable>\n    <RoleAssignDrawer @reload=\"tableApi.query()\" />\n  </Page>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/system/role-assign/role-assign-drawer.vue",
    "content": "<script setup lang=\"ts\">\nimport type { VbenFormProps } from '@vben/common-ui';\n\nimport type { VxeGridProps } from '#/adapter/vxe-table';\n\nimport { useRoute } from 'vue-router';\n\nimport { useVbenDrawer } from '@vben/common-ui';\n\nimport { useVbenVxeGrid } from '#/adapter/vxe-table';\nimport { roleSelectAll, roleUnallocatedList } from '#/api/system/role';\n\nimport { columns, querySchema } from './data';\n\nconst emit = defineEmits<{ reload: [] }>();\n\nconst [BasicDrawer, drawerApi] = useVbenDrawer({\n  onConfirm: handleSubmit,\n  onCancel: handleReset,\n  destroyOnClose: true,\n});\n\nconst route = useRoute();\nconst roleId = route.params.roleId as string;\n\nconst formOptions: VbenFormProps = {\n  commonConfig: {\n    labelWidth: 80,\n  },\n  schema: querySchema(),\n  wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3',\n};\n\nconst gridOptions: VxeGridProps = {\n  checkboxConfig: {\n    // 高亮\n    highlight: true,\n    // 翻页时保留选中状态\n    reserve: true,\n    // 点击行选中\n    trigger: 'row',\n  },\n  columns: columns?.filter((item) => item.field !== 'action'),\n  height: 'auto',\n  keepSource: true,\n  pagerConfig: {},\n  proxyConfig: {\n    ajax: {\n      query: async ({ page }, formValues = {}) => {\n        return await roleUnallocatedList({\n          pageNum: page.currentPage,\n          pageSize: page.pageSize,\n          roleId,\n          ...formValues,\n        });\n      },\n    },\n  },\n  rowConfig: {\n    keyField: 'userId',\n  },\n};\n\nconst [BasicTable, tableApi] = useVbenVxeGrid({\n  formOptions,\n  gridOptions,\n});\n\nasync function handleSubmit() {\n  const records = tableApi.grid.getCheckboxRecords();\n  const userIds = records.map((item) => item.userId);\n  if (userIds.length > 0) {\n    await roleSelectAll(roleId, userIds);\n  }\n  handleReset();\n  emit('reload');\n}\n\nfunction handleReset() {\n  drawerApi.close();\n}\n</script>\n\n<template>\n  <BasicDrawer class=\"w-[800px]\" title=\"选择用户\">\n    <BasicTable />\n  </BasicDrawer>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/system/tenant/data.tsx",
    "content": "import type { FormSchemaGetter } from '#/adapter/form';\nimport type { VxeGridProps } from '#/adapter/vxe-table';\n\nimport { getPopupContainer } from '@vben/utils';\n\nimport dayjs from 'dayjs';\n\nimport { z } from '#/adapter/form';\n\nexport const querySchema: FormSchemaGetter = () => [\n  {\n    component: 'Input',\n    fieldName: 'tenantId',\n    label: '租户编号',\n  },\n  {\n    component: 'Input',\n    fieldName: 'companyName',\n    label: '租户名称',\n  },\n  {\n    component: 'Input',\n    fieldName: 'contactUserName',\n    label: '联系人',\n  },\n  {\n    component: 'Input',\n    fieldName: 'contactPhone',\n    label: '联系电话',\n  },\n];\n\nexport const columns: VxeGridProps['columns'] = [\n  { type: 'checkbox', width: 60 },\n  {\n    title: '租户编号',\n    field: 'tenantId',\n  },\n  {\n    title: '租户名称',\n    field: 'companyName',\n  },\n  {\n    title: '联系人',\n    field: 'contactUserName',\n  },\n  {\n    title: '联系电话',\n    field: 'contactPhone',\n  },\n  {\n    title: '到期时间',\n    field: 'expireTime',\n    formatter: ({ cellValue }) => {\n      if (!cellValue) {\n        return '无期限';\n      }\n      return cellValue;\n    },\n  },\n  {\n    title: '租户状态',\n    field: 'status',\n    slots: { default: 'status' },\n  },\n  {\n    field: 'action',\n    fixed: 'right',\n    slots: { default: 'action' },\n    title: '操作',\n    resizable: false,\n    width: 'auto',\n  },\n];\n\nconst defaultExpireTime = dayjs()\n  .add(365, 'days')\n  .startOf('day')\n  .format('YYYY-MM-DD HH:mm:ss');\n\nexport const drawerSchema: FormSchemaGetter = () => [\n  {\n    component: 'Input',\n    dependencies: {\n      show: () => false,\n      triggerFields: [''],\n    },\n    fieldName: 'id',\n    label: 'id',\n  },\n  {\n    component: 'Input',\n    dependencies: {\n      show: () => false,\n      triggerFields: [''],\n    },\n    fieldName: 'tenantId',\n    label: 'tenantId',\n  },\n  {\n    component: 'Divider',\n    componentProps: {\n      orientation: 'center',\n    },\n    fieldName: 'divider1',\n    hideLabel: true,\n    renderComponentContent: () => ({\n      default: () => '基本信息',\n    }),\n  },\n  {\n    component: 'Input',\n    fieldName: 'companyName',\n    label: '企业名称',\n    rules: 'required',\n  },\n  {\n    component: 'Input',\n    fieldName: 'contactUserName',\n    label: '联系人',\n    rules: 'required',\n  },\n  {\n    component: 'Input',\n    fieldName: 'contactPhone',\n    label: '联系电话',\n    rules: z\n      .string()\n      .regex(/^1[3-9]\\d{9}$/, { message: '请输入正确的联系电话' }),\n  },\n  {\n    component: 'Divider',\n    componentProps: {\n      orientation: 'center',\n    },\n    fieldName: 'divider2',\n    hideLabel: true,\n    renderComponentContent: () => ({\n      default: () => '管理员信息',\n    }),\n    dependencies: {\n      if: (values) => !values?.tenantId,\n      triggerFields: ['tenantId'],\n    },\n  },\n  {\n    component: 'Input',\n    fieldName: 'username',\n    label: '用户账号',\n    rules: 'required',\n    dependencies: {\n      if: (values) => !values?.tenantId,\n      triggerFields: ['tenantId'],\n    },\n  },\n  {\n    component: 'InputPassword',\n    fieldName: 'password',\n    label: '用户密码',\n    rules: 'required',\n    dependencies: {\n      if: (values) => !values?.tenantId,\n      triggerFields: ['tenantId'],\n    },\n  },\n  {\n    component: 'Divider',\n    componentProps: {\n      orientation: 'center',\n    },\n    fieldName: 'divider3',\n    hideLabel: true,\n    renderComponentContent: () => ({\n      default: () => '租户设置',\n    }),\n  },\n  {\n    component: 'Select',\n    componentProps: {\n      getPopupContainer,\n    },\n    fieldName: 'packageId',\n    label: '租户套餐',\n    rules: 'selectRequired',\n  },\n  {\n    component: 'DatePicker',\n    componentProps: {\n      format: 'YYYY-MM-DD HH:mm:ss',\n      showTime: true,\n      valueFormat: 'YYYY-MM-DD HH:mm:ss',\n      getPopupContainer,\n    },\n    defaultValue: defaultExpireTime,\n    fieldName: 'expireTime',\n    help: `已经设置过期时间不允许重置为'无期限'\\n即在开通时未设置无期限 以后都不允许设置`,\n    label: '过期时间',\n  },\n  {\n    component: 'InputNumber',\n    componentProps: {\n      min: -1,\n    },\n    defaultValue: -1,\n    fieldName: 'accountCount',\n    help: '-1不限制用户数量',\n    label: '用户数量',\n    renderComponentContent(model) {\n      return {\n        addonBefore: () =>\n          model.accountCount === -1 ? '不限制数量' : '输入数量',\n      };\n    },\n    rules: 'required',\n  },\n  {\n    component: 'Input',\n    fieldName: 'domain',\n    help: '可填写域名/端口 填写域名如: www.test.com 或者 www.test.com:8080 填写ip:端口如: 127.0.0.1:8080',\n    label: '绑定域名',\n    renderComponentContent() {\n      return {\n        addonBefore: () => 'http(s)://',\n      };\n    },\n    rules: z\n      .string()\n      .refine(\n        (domain) =>\n          !(domain.startsWith('http://') || domain.startsWith('https://')),\n        { message: '请输入正确的域名, 不需要http(s)' },\n      )\n      .optional(),\n  },\n  {\n    component: 'Divider',\n    componentProps: {\n      orientation: 'center',\n    },\n    fieldName: 'divider4',\n    hideLabel: true,\n    renderComponentContent: () => ({\n      default: () => '企业信息',\n    }),\n  },\n  {\n    component: 'Input',\n    fieldName: 'address',\n    label: '企业地址',\n  },\n  {\n    component: 'Input',\n    fieldName: 'licenseNumber',\n    label: '企业代码',\n  },\n  {\n    component: 'Textarea',\n    fieldName: 'intro',\n    formItemClass: 'items-start',\n    label: '企业介绍',\n  },\n  {\n    component: 'Textarea',\n    fieldName: 'remark',\n    formItemClass: 'items-start',\n    label: '备注',\n  },\n];\n"
  },
  {
    "path": "apps/web-antd/src/views/system/tenant/index.vue",
    "content": "<script setup lang=\"ts\">\nimport type { VbenFormProps } from '@vben/common-ui';\n\nimport type { VxeGridProps } from '#/adapter/vxe-table';\nimport type { Tenant } from '#/api/system/tenant/model';\n\nimport { computed } from 'vue';\n\nimport { useAccess } from '@vben/access';\nimport { Fallback, Page, useVbenDrawer } from '@vben/common-ui';\nimport { getVxePopupContainer } from '@vben/utils';\n\nimport { Modal, Popconfirm, Space } from 'ant-design-vue';\n\nimport { useVbenVxeGrid, vxeCheckboxChecked } from '#/adapter/vxe-table';\nimport {\n  dictSyncTenant,\n  syncTenantConfig,\n  tenantExport,\n  tenantList,\n  tenantRemove,\n  tenantStatusChange,\n  tenantSyncPackage,\n} from '#/api/system/tenant';\nimport { TableSwitch } from '#/components/table';\nimport { useTenantStore } from '#/store/tenant';\nimport { commonDownloadExcel } from '#/utils/file/download';\n\nimport { columns, querySchema } from './data';\nimport tenantDrawer from './tenant-drawer.vue';\n\nconst formOptions: VbenFormProps = {\n  commonConfig: {\n    labelWidth: 80,\n    componentProps: {\n      allowClear: true,\n    },\n  },\n  schema: querySchema(),\n  wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4',\n};\n\nconst gridOptions: VxeGridProps = {\n  checkboxConfig: {\n    // 高亮\n    highlight: true,\n    // 翻页时保留选中状态\n    reserve: true,\n    // 点击行选中\n    // trigger: 'row',\n    checkMethod: ({ row }) => row?.id !== 1,\n  },\n  columns,\n  height: 'auto',\n  keepSource: true,\n  pagerConfig: {},\n  proxyConfig: {\n    ajax: {\n      query: async ({ page }, formValues = {}) => {\n        return await tenantList({\n          pageNum: page.currentPage,\n          pageSize: page.pageSize,\n          ...formValues,\n        });\n      },\n    },\n  },\n  rowConfig: {\n    keyField: 'id',\n  },\n  id: 'system-tenant-index',\n};\n\nconst [BasicTable, tableApi] = useVbenVxeGrid({\n  formOptions,\n  gridOptions,\n});\n\nconst [TenantDrawer, drawerApi] = useVbenDrawer({\n  connectedComponent: tenantDrawer,\n});\n\nfunction handleAdd() {\n  drawerApi.setData({});\n  drawerApi.open();\n}\n\nasync function handleEdit(record: Tenant) {\n  drawerApi.setData({ id: record.id });\n  drawerApi.open();\n}\n\nasync function handleSync(record: Tenant) {\n  const { tenantId, packageId } = record;\n  await tenantSyncPackage(tenantId, packageId);\n  await tableApi.query();\n}\n\nconst tenantStore = useTenantStore();\nasync function handleDelete(row: Tenant) {\n  await tenantRemove([row.id]);\n  await tableApi.query();\n  // 重新加载租户信息\n  tenantStore.initTenant();\n}\n\nfunction handleMultiDelete() {\n  const rows = tableApi.grid.getCheckboxRecords();\n  const ids = rows.map((row: Tenant) => row.id);\n  Modal.confirm({\n    title: '提示',\n    okType: 'danger',\n    content: `确认删除选中的${ids.length}条记录吗？`,\n    onOk: async () => {\n      await tenantRemove(ids);\n      await tableApi.query();\n      // 重新加载租户信息\n      tenantStore.initTenant();\n    },\n  });\n}\n\nfunction handleDownloadExcel() {\n  commonDownloadExcel(tenantExport, '租户数据', tableApi.formApi.form.values);\n}\n\n/**\n * 与后台逻辑相同\n * 只有超级管理员能访问租户相关\n */\nconst { hasAccessByCodes, hasAccessByRoles } = useAccess();\n\nconst isSuperAdmin = computed(() => {\n  return hasAccessByRoles(['superadmin']);\n});\n\nfunction handleSyncTenantDict() {\n  Modal.confirm({\n    title: '提示',\n    iconType: 'warning',\n    content: '确认同步租户字典？',\n    onOk: async () => {\n      await dictSyncTenant();\n      await tableApi.query();\n    },\n  });\n}\n\nfunction handleSyncTenantConfig() {\n  Modal.confirm({\n    title: '提示',\n    iconType: 'warning',\n    content: '确认同步租户参数配置？',\n    onOk: async () => {\n      await syncTenantConfig();\n      await tableApi.query();\n    },\n  });\n}\n</script>\n\n<template>\n  <Page v-if=\"isSuperAdmin\" :auto-content-height=\"true\">\n    <BasicTable table-title=\"租户列表\">\n      <template #toolbar-tools>\n        <Space>\n          <a-button\n            v-access:code=\"['system:tenant:edit']\"\n            @click=\"handleSyncTenantDict\"\n          >\n            同步租户字典\n          </a-button>\n          <a-button\n            v-access:code=\"['system:tenant:edit']\"\n            @click=\"handleSyncTenantConfig\"\n          >\n            同步租户参数配置\n          </a-button>\n          <a-button\n            v-access:code=\"['system:tenant:export']\"\n            @click=\"handleDownloadExcel\"\n          >\n            {{ $t('pages.common.export') }}\n          </a-button>\n          <a-button\n            :disabled=\"!vxeCheckboxChecked(tableApi)\"\n            danger\n            type=\"primary\"\n            v-access:code=\"['system:tenant:remove']\"\n            @click=\"handleMultiDelete\"\n          >\n            {{ $t('pages.common.delete') }}\n          </a-button>\n          <a-button\n            type=\"primary\"\n            v-access:code=\"['system:tenant:add']\"\n            @click=\"handleAdd\"\n          >\n            {{ $t('pages.common.add') }}\n          </a-button>\n        </Space>\n      </template>\n      <template #status=\"{ row }\">\n        <TableSwitch\n          v-model:value=\"row.status\"\n          :api=\"() => tenantStatusChange(row)\"\n          :disabled=\"row.id === 1 || !hasAccessByCodes(['system:tenant:edit'])\"\n          @reload=\"tableApi.query()\"\n        />\n      </template>\n      <template #action=\"{ row }\">\n        <Space v-if=\"row.id !== 1\">\n          <ghost-button\n            v-access:code=\"['system:tenant:edit']\"\n            @click=\"handleEdit(row)\"\n          >\n            {{ $t('pages.common.edit') }}\n          </ghost-button>\n          <Popconfirm\n            :get-popup-container=\"getVxePopupContainer\"\n            :title=\"`确认同步[${row.companyName}]的套餐吗?`\"\n            placement=\"left\"\n            @confirm=\"handleSync(row)\"\n          >\n            <ghost-button\n              class=\"btn-success\"\n              v-access:code=\"['system:tenant:edit']\"\n            >\n              {{ $t('pages.common.sync') }}\n            </ghost-button>\n          </Popconfirm>\n          <Popconfirm\n            :get-popup-container=\"getVxePopupContainer\"\n            placement=\"left\"\n            title=\"确认删除？\"\n            @confirm=\"handleDelete(row)\"\n          >\n            <ghost-button\n              danger\n              v-access:code=\"['system:tenant:remove']\"\n              @click.stop=\"\"\n            >\n              {{ $t('pages.common.delete') }}\n            </ghost-button>\n          </Popconfirm>\n        </Space>\n      </template>\n    </BasicTable>\n    <TenantDrawer @reload=\"tableApi.query()\" />\n  </Page>\n  <Fallback v-else description=\"您没有租户的访问权限\" status=\"403\" />\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/system/tenant/tenant-drawer.vue",
    "content": "<script setup lang=\"ts\">\nimport { computed, h, ref } from 'vue';\nimport { useRouter } from 'vue-router';\n\nimport { useVbenDrawer } from '@vben/common-ui';\nimport { $t } from '@vben/locales';\nimport { cloneDeep } from '@vben/utils';\n\nimport { Button, message, Skeleton } from 'ant-design-vue';\n\nimport { useVbenForm } from '#/adapter/form';\nimport { tenantAdd, tenantInfo, tenantUpdate } from '#/api/system/tenant';\nimport { packageSelectList } from '#/api/system/tenant-package';\nimport { useTenantStore } from '#/store/tenant';\nimport { defaultFormValueGetter, useBeforeCloseDiff } from '#/utils/popup';\n\nimport { drawerSchema } from './data';\n\nconst emit = defineEmits<{ reload: [] }>();\n\nconst isUpdate = ref(false);\nconst title = computed(() => {\n  return isUpdate.value ? $t('pages.common.edit') : $t('pages.common.add');\n});\n\nconst [BasicForm, formApi] = useVbenForm({\n  commonConfig: {\n    formItemClass: 'col-span-2',\n    labelWidth: 100,\n    componentProps: {\n      class: 'w-full',\n    },\n  },\n  schema: drawerSchema(),\n  showDefaultActions: false,\n  wrapperClass: 'grid-cols-2',\n});\n\nconst router = useRouter();\nasync function setupPackageSelect() {\n  const tenantPackageList = await packageSelectList();\n  /**\n   * 检测是否存在租户套餐 你也不想表单填完了发现套餐为0无法选中吧\n   */\n  if (tenantPackageList.length === 0) {\n    const closeMessage = message.error(\n      h('span', {}, [\n        '请先配置租户套餐',\n        h(\n          Button,\n          {\n            onClick: () => {\n              router.push('/tenant/tenantPackage');\n              closeMessage();\n            },\n            type: 'link',\n            size: 'small',\n          },\n          '前往配置',\n        ),\n      ]),\n    );\n    throw new Error('请先配置租户套餐');\n  }\n\n  const options = tenantPackageList.map((item) => ({\n    label: item.packageName,\n    value: item.packageId,\n  }));\n  formApi.updateSchema([\n    {\n      componentProps: {\n        optionFilterProp: 'label',\n        optionLabelProp: 'label',\n        options,\n        showSearch: true,\n      },\n      fieldName: 'packageId',\n    },\n  ]);\n}\n\nconst { onBeforeClose, markInitialized, resetInitialized } = useBeforeCloseDiff(\n  {\n    initializedGetter: defaultFormValueGetter(formApi),\n    currentGetter: defaultFormValueGetter(formApi),\n  },\n);\n\nconst loading = ref(false);\nconst [BasicDrawer, drawerApi] = useVbenDrawer({\n  onBeforeClose,\n  onClosed: handleClosed,\n  onConfirm: handleConfirm,\n  async onOpenChange(isOpen) {\n    if (!isOpen) {\n      return null;\n    }\n    drawerApi.drawerLoading(true);\n    loading.value = true;\n\n    const { id } = drawerApi.getData() as { id?: number | string };\n    isUpdate.value = !!id;\n\n    if (isUpdate.value && id) {\n      const [record] = await Promise.all([\n        tenantInfo(id),\n        setupPackageSelect(),\n      ]);\n      await formApi.setValues(record);\n    } else {\n      await setupPackageSelect();\n    }\n\n    formApi.updateSchema([\n      {\n        fieldName: 'packageId',\n        componentProps: {\n          disabled: isUpdate.value,\n        },\n      },\n    ]);\n    await markInitialized();\n\n    drawerApi.drawerLoading(false);\n    loading.value = false;\n  },\n});\n\nconst tenantStore = useTenantStore();\nasync function handleConfirm() {\n  try {\n    drawerApi.lock(true);\n    const { valid } = await formApi.validate();\n    if (!valid) {\n      return;\n    }\n    const data = cloneDeep(await formApi.getValues());\n    await (isUpdate.value ? tenantUpdate(data) : tenantAdd(data));\n    resetInitialized();\n    emit('reload');\n    drawerApi.close();\n    // 重新加载租户信息\n    tenantStore.initTenant();\n  } catch (error) {\n    console.error(error);\n  } finally {\n    drawerApi.lock(false);\n  }\n}\n\nasync function handleClosed() {\n  await formApi.resetForm();\n  resetInitialized();\n}\n</script>\n\n<template>\n  <BasicDrawer :title=\"title\" class=\"w-[600px]\">\n    <Skeleton v-if=\"loading\" active />\n    <BasicForm v-show=\"!loading\" />\n  </BasicDrawer>\n</template>\n\n<style lang=\"scss\" scoped>\n:deep(.ant-divider) {\n  margin: 8px 0;\n}\n</style>\n"
  },
  {
    "path": "apps/web-antd/src/views/system/tenantPackage/data.ts",
    "content": "import type { FormSchemaGetter } from '#/adapter/form';\nimport type { VxeGridProps } from '#/adapter/vxe-table';\n\nexport const querySchema: FormSchemaGetter = () => [\n  {\n    component: 'Input',\n    fieldName: 'packageName',\n    label: '套餐名称',\n  },\n];\n\nexport const columns: VxeGridProps['columns'] = [\n  { type: 'checkbox', width: 60 },\n  {\n    title: '套餐名称',\n    field: 'packageName',\n  },\n  {\n    title: '备注',\n    field: 'remark',\n  },\n  {\n    title: '状态',\n    field: 'status',\n    slots: { default: 'status' },\n  },\n  {\n    field: 'action',\n    fixed: 'right',\n    slots: { default: 'action' },\n    title: '操作',\n    resizable: false,\n    width: 'auto',\n  },\n];\n\nexport const drawerSchema: FormSchemaGetter = () => [\n  {\n    component: 'Input',\n    dependencies: {\n      show: () => false,\n      triggerFields: [''],\n    },\n    fieldName: 'packageId',\n  },\n  {\n    component: 'Radio',\n    dependencies: {\n      show: () => false,\n      triggerFields: [''],\n    },\n    fieldName: 'menuCheckStrictly',\n  },\n  {\n    component: 'Input',\n    fieldName: 'packageName',\n    label: '套餐名称',\n    rules: 'required',\n  },\n  {\n    component: 'menuIds',\n    defaultValue: [],\n    fieldName: 'menuIds',\n    label: '关联菜单',\n  },\n  {\n    component: 'Textarea',\n    fieldName: 'remark',\n    label: '备注',\n  },\n];\n"
  },
  {
    "path": "apps/web-antd/src/views/system/tenantPackage/index.vue",
    "content": "<script setup lang=\"ts\">\nimport type { VbenFormProps } from '@vben/common-ui';\n\nimport type { VxeGridProps } from '#/adapter/vxe-table';\nimport type { TenantPackage } from '#/api/system/tenant-package/model';\n\nimport { computed } from 'vue';\n\nimport { useAccess } from '@vben/access';\nimport { Fallback, Page, useVbenDrawer } from '@vben/common-ui';\nimport { getVxePopupContainer } from '@vben/utils';\n\nimport { Modal, Popconfirm, Space } from 'ant-design-vue';\n\nimport { useVbenVxeGrid, vxeCheckboxChecked } from '#/adapter/vxe-table';\nimport {\n  packageChangeStatus,\n  packageExport,\n  packageList,\n  packageRemove,\n} from '#/api/system/tenant-package';\nimport { TableSwitch } from '#/components/table';\nimport { commonDownloadExcel } from '#/utils/file/download';\n\nimport { columns, querySchema } from './data';\nimport tenantPackageDrawer from './tenant-package-drawer.vue';\n\nconst formOptions: VbenFormProps = {\n  commonConfig: {\n    labelWidth: 80,\n    componentProps: {\n      allowClear: true,\n    },\n  },\n  schema: querySchema(),\n  wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4',\n};\n\nconst gridOptions: VxeGridProps = {\n  checkboxConfig: {\n    // 高亮\n    highlight: true,\n    // 翻页时保留选中状态\n    reserve: true,\n    // 点击行选中\n    // trigger: 'row',\n  },\n  columns,\n  height: 'auto',\n  keepSource: true,\n  pagerConfig: {},\n  proxyConfig: {\n    ajax: {\n      query: async ({ page }, formValues = {}) => {\n        return await packageList({\n          pageNum: page.currentPage,\n          pageSize: page.pageSize,\n          ...formValues,\n        });\n      },\n    },\n  },\n  rowConfig: {\n    keyField: 'packageId',\n  },\n  id: 'system-tenant-package-index',\n};\n\nconst [BasicTable, tableApi] = useVbenVxeGrid({\n  formOptions,\n  gridOptions,\n});\n\nconst [TenantPackageDrawer, drawerApi] = useVbenDrawer({\n  connectedComponent: tenantPackageDrawer,\n});\n\nfunction handleAdd() {\n  drawerApi.setData({});\n  drawerApi.open();\n}\n\nasync function handleEdit(record: TenantPackage) {\n  drawerApi.setData({ id: record.packageId });\n  drawerApi.open();\n}\n\nasync function handleDelete(row: TenantPackage) {\n  await packageRemove([row.packageId]);\n  await tableApi.query();\n}\n\nfunction handleMultiDelete() {\n  const rows = tableApi.grid.getCheckboxRecords();\n  const ids = rows.map((row: TenantPackage) => row.packageId);\n  Modal.confirm({\n    title: '提示',\n    okType: 'danger',\n    content: `确认删除选中的${ids.length}条记录吗？`,\n    onOk: async () => {\n      await packageRemove(ids);\n      await tableApi.query();\n    },\n  });\n}\n\nfunction handleDownloadExcel() {\n  commonDownloadExcel(\n    packageExport,\n    '租户套餐数据',\n    tableApi.formApi.form.values,\n  );\n}\n\n/**\n * 与后台逻辑相同\n * 只有超级管理员能访问租户相关\n */\nconst { hasAccessByCodes, hasAccessByRoles } = useAccess();\n\nconst isSuperAdmin = computed(() => {\n  return hasAccessByRoles(['superadmin']);\n});\n</script>\n\n<template>\n  <Page v-if=\"isSuperAdmin\" :auto-content-height=\"true\">\n    <BasicTable table-title=\"租户套餐列表\">\n      <template #toolbar-tools>\n        <Space>\n          <a-button\n            v-access:code=\"['system:tenantPackage:export']\"\n            @click=\"handleDownloadExcel\"\n          >\n            {{ $t('pages.common.export') }}\n          </a-button>\n          <a-button\n            :disabled=\"!vxeCheckboxChecked(tableApi)\"\n            danger\n            type=\"primary\"\n            v-access:code=\"['system:tenantPackage:remove']\"\n            @click=\"handleMultiDelete\"\n          >\n            {{ $t('pages.common.delete') }}\n          </a-button>\n          <a-button\n            type=\"primary\"\n            v-access:code=\"['system:tenantPackage:add']\"\n            @click=\"handleAdd\"\n          >\n            {{ $t('pages.common.add') }}\n          </a-button>\n        </Space>\n      </template>\n      <template #status=\"{ row }\">\n        <TableSwitch\n          v-model:value=\"row.status\"\n          :api=\"() => packageChangeStatus(row)\"\n          :disabled=\"!hasAccessByCodes(['system:tenantPackage:edit'])\"\n          @reload=\"tableApi.query()\"\n        />\n      </template>\n      <template #action=\"{ row }\">\n        <Space>\n          <ghost-button\n            v-access:code=\"['system:tenantPackage:edit']\"\n            @click=\"handleEdit(row)\"\n          >\n            {{ $t('pages.common.edit') }}\n          </ghost-button>\n          <Popconfirm\n            :get-popup-container=\"getVxePopupContainer\"\n            placement=\"left\"\n            title=\"确认删除？\"\n            @confirm=\"handleDelete(row)\"\n          >\n            <ghost-button\n              danger\n              v-access:code=\"['system:tenantPackage:remove']\"\n              @click.stop=\"\"\n            >\n              {{ $t('pages.common.delete') }}\n            </ghost-button>\n          </Popconfirm>\n        </Space>\n      </template>\n    </BasicTable>\n    <TenantPackageDrawer @reload=\"tableApi.query()\" />\n  </Page>\n  <Fallback v-else description=\"您没有租户的访问权限\" status=\"403\" />\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/system/tenantPackage/tenant-package-drawer.vue",
    "content": "<script setup lang=\"ts\">\nimport type { MenuOption } from '#/api/system/menu/model';\n\nimport { computed, nextTick, ref } from 'vue';\n\nimport { useVbenDrawer } from '@vben/common-ui';\nimport { $t } from '@vben/locales';\nimport { cloneDeep, eachTree } from '@vben/utils';\n\nimport { omit } from 'lodash-es';\n\nimport { useVbenForm } from '#/adapter/form';\nimport { tenantPackageMenuTreeSelect } from '#/api/system/menu';\nimport {\n  packageAdd,\n  packageInfo,\n  packageUpdate,\n} from '#/api/system/tenant-package';\nimport { MenuSelectTable } from '#/components/tree';\nimport { defaultFormValueGetter, useBeforeCloseDiff } from '#/utils/popup';\n\nimport { drawerSchema } from './data';\n\nconst emit = defineEmits<{ reload: [] }>();\n\nconst isUpdate = ref(false);\nconst title = computed(() => {\n  return isUpdate.value ? $t('pages.common.edit') : $t('pages.common.add');\n});\n\nconst [BasicForm, formApi] = useVbenForm({\n  commonConfig: {\n    formItemClass: 'col-span-2',\n  },\n  layout: 'vertical',\n  schema: drawerSchema(),\n  showDefaultActions: false,\n  wrapperClass: 'grid-cols-2',\n});\n\nconst menuTree = ref<MenuOption[]>([]);\nasync function setupMenuTree(id?: number | string) {\n  // 0为新增使用  获取除了`租户管理`的所有菜单\n  const resp = await tenantPackageMenuTreeSelect(id ?? 0);\n  const menus = resp.menus;\n  // i18n处理\n  eachTree(menus, (node) => {\n    node.label = $t(node.label);\n  });\n  // 设置菜单信息\n  menuTree.value = menus;\n  // keys依赖于menu 需要先加载menu\n  await nextTick();\n  await formApi.setFieldValue('menuIds', resp.checkedKeys);\n}\n\nasync function customFormValueGetter() {\n  const v = await defaultFormValueGetter(formApi)();\n  // 获取勾选信息\n  const menuIds = menuSelectRef.value?.getCheckedKeys?.() ?? [];\n  const mixStr = v + menuIds.join(',');\n  return mixStr;\n}\n\nconst { onBeforeClose, markInitialized, resetInitialized } = useBeforeCloseDiff(\n  {\n    initializedGetter: customFormValueGetter,\n    currentGetter: customFormValueGetter,\n  },\n);\n\nconst [BasicDrawer, drawerApi] = useVbenDrawer({\n  onBeforeClose,\n  onClosed: handleClosed,\n  onConfirm: handleConfirm,\n  destroyOnClose: true,\n  async onOpenChange(isOpen) {\n    if (!isOpen) {\n      return null;\n    }\n    drawerApi.drawerLoading(true);\n\n    const { id } = drawerApi.getData() as { id?: number | string };\n    isUpdate.value = !!id;\n    if (isUpdate.value && id) {\n      const record = await packageInfo(id);\n      // 需要排除menuIds menuIds为string\n      // 通过setupMenuTreeSelect设置\n      await formApi.setValues(omit(record, ['menuIds']));\n    }\n    // init菜单 注意顺序要放在赋值record之后 内部watch会依赖record\n    await setupMenuTree(id);\n    await markInitialized();\n\n    drawerApi.drawerLoading(false);\n  },\n});\n\nconst menuSelectRef = ref<InstanceType<typeof MenuSelectTable>>();\nasync function handleConfirm() {\n  try {\n    drawerApi.drawerLoading(true);\n    const { valid } = await formApi.validate();\n    if (!valid) {\n      return;\n    }\n    // 这个用于提交\n    const menuIds = menuSelectRef.value?.getCheckedKeys?.() ?? [];\n    // formApi.getValues拿到的是一个readonly对象，不能直接修改，需要cloneDeep\n    const data = cloneDeep(await formApi.getValues());\n    data.menuIds = menuIds;\n    await (isUpdate.value ? packageUpdate(data) : packageAdd(data));\n    resetInitialized();\n    emit('reload');\n    drawerApi.close();\n  } catch (error) {\n    console.error(error);\n  } finally {\n    drawerApi.drawerLoading(false);\n  }\n}\n\nasync function handleClosed() {\n  await formApi.resetForm();\n  resetInitialized();\n}\n\n/**\n * 通过回调更新 无法通过v-model\n * @param value 菜单选择是否严格模式\n */\nfunction handleMenuCheckStrictlyChange(value: boolean) {\n  formApi.setFieldValue('menuCheckStrictly', value);\n}\n</script>\n\n<template>\n  <BasicDrawer :title=\"title\" class=\"w-[800px]\">\n    <BasicForm>\n      <template #menuIds=\"slotProps\">\n        <div class=\"h-[600px] w-full\">\n          <!-- association为readonly 不能通过v-model绑定 -->\n          <MenuSelectTable\n            ref=\"menuSelectRef\"\n            :checked-keys=\"slotProps.value\"\n            :association=\"formApi.form.values.menuCheckStrictly\"\n            :menus=\"menuTree\"\n            @update:association=\"handleMenuCheckStrictlyChange\"\n          />\n        </div>\n      </template>\n    </BasicForm>\n  </BasicDrawer>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/system/user/authRole.vue",
    "content": "<template>\n  <div>\n    ele版本会使用这个文件 只是为了不报错`未找到对应组件`才新建的这个文件\n    无实际意义\n  </div>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/system/user/data.tsx",
    "content": "import type { FormSchemaGetter } from '#/adapter/form';\nimport type { VxeGridProps } from '#/adapter/vxe-table';\n\nimport { DictEnum } from '@vben/constants';\nimport { getPopupContainer } from '@vben/utils';\n\nimport { z } from '#/adapter/form';\nimport { getDictOptions } from '#/utils/dict';\n\nexport const querySchema: FormSchemaGetter = () => [\n  {\n    component: 'Input',\n    fieldName: 'userName',\n    label: '用户账号',\n  },\n  {\n    component: 'Input',\n    fieldName: 'nickName',\n    label: '用户昵称',\n  },\n  {\n    component: 'Input',\n    fieldName: 'phonenumber',\n    label: '手机号码',\n  },\n  {\n    component: 'Select',\n    componentProps: {\n      getPopupContainer,\n      options: getDictOptions(DictEnum.SYS_NORMAL_DISABLE),\n    },\n    fieldName: 'status',\n    label: '用户状态',\n  },\n  {\n    component: 'RangePicker',\n    fieldName: 'createTime',\n    label: '创建时间',\n  },\n];\n\nexport const columns: VxeGridProps['columns'] = [\n  { type: 'checkbox', width: 60 },\n  {\n    field: 'userName',\n    title: '名称',\n    minWidth: 80,\n  },\n  {\n    field: 'nickName',\n    title: '昵称',\n    minWidth: 130,\n  },\n  {\n    field: 'avatar',\n    title: '头像',\n    slots: { default: 'avatar' },\n    minWidth: 80,\n  },\n  {\n    field: 'deptName',\n    title: '部门',\n    minWidth: 120,\n  },\n  {\n    field: 'phonenumber',\n    title: '手机号',\n    formatter({ cellValue }) {\n      return cellValue || '暂无';\n    },\n    minWidth: 120,\n  },\n  {\n    field: 'status',\n    title: '状态',\n    slots: { default: 'status' },\n    minWidth: 100,\n  },\n  {\n    field: 'createTime',\n    title: '创建时间',\n    minWidth: 150,\n  },\n  {\n    field: 'action',\n    fixed: 'right',\n    slots: { default: 'action' },\n    title: '操作',\n    resizable: false,\n    width: 'auto',\n  },\n];\n\nexport const drawerSchema: FormSchemaGetter = () => [\n  {\n    component: 'Input',\n    dependencies: {\n      show: () => false,\n      triggerFields: [''],\n    },\n    fieldName: 'userId',\n  },\n  {\n    component: 'Input',\n    fieldName: 'userName',\n    label: '用户账号',\n    rules: 'required',\n  },\n  {\n    component: 'InputPassword',\n    fieldName: 'password',\n    label: '用户密码',\n    rules: 'required',\n  },\n  {\n    component: 'Input',\n    fieldName: 'nickName',\n    label: '用户昵称',\n    rules: 'required',\n  },\n  {\n    component: 'TreeSelect',\n    // 在drawer里更新 这里不需要默认的componentProps\n    defaultValue: undefined,\n    fieldName: 'deptId',\n    label: '所属部门',\n    rules: 'selectRequired',\n  },\n  {\n    component: 'Input',\n    fieldName: 'phonenumber',\n    label: '手机号码',\n    defaultValue: undefined,\n    rules: z\n      .string()\n      .regex(/^1[3-9]\\d{9}$/, '请输入正确的手机号码')\n      .optional()\n      .or(z.literal('')),\n  },\n  {\n    component: 'Input',\n    fieldName: 'email',\n    defaultValue: undefined,\n    label: '邮箱',\n    /**\n     * z.literal 是 Zod 中的一种类型，用于定义一个特定的字面量值。\n     * 它可以用于确保输入的值与指定的字面量完全匹配。\n     * 例如，你可以使用 z.literal 来确保某个字段的值只能是特定的字符串、数字、布尔值等。\n     * 即空字符串也可通过校验\n     */\n    rules: z.string().email('请输入正确的邮箱').optional().or(z.literal('')),\n  },\n  {\n    component: 'RadioGroup',\n    componentProps: {\n      buttonStyle: 'solid',\n      options: getDictOptions(DictEnum.SYS_USER_SEX),\n      optionType: 'button',\n    },\n    defaultValue: '0',\n    fieldName: 'sex',\n    formItemClass: 'col-span-2 lg:col-span-1',\n    label: '性别',\n  },\n  {\n    component: 'RadioGroup',\n    componentProps: {\n      buttonStyle: 'solid',\n      options: getDictOptions(DictEnum.SYS_NORMAL_DISABLE),\n      optionType: 'button',\n    },\n    defaultValue: '0',\n    fieldName: 'status',\n    formItemClass: 'col-span-2 lg:col-span-1',\n    label: '状态',\n  },\n  {\n    component: 'Select',\n    componentProps: {\n      getPopupContainer,\n      mode: 'multiple',\n      optionFilterProp: 'label',\n      optionLabelProp: 'label',\n      placeholder: '请先选择部门',\n    },\n    fieldName: 'postIds',\n    help: '选择部门后, 将自动加载该部门下所有的岗位',\n    label: '岗位',\n  },\n  {\n    component: 'Select',\n    componentProps: {\n      getPopupContainer,\n      mode: 'multiple',\n      optionFilterProp: 'title',\n      optionLabelProp: 'title',\n    },\n    fieldName: 'roleIds',\n    label: '角色',\n  },\n  {\n    component: 'Textarea',\n    fieldName: 'remark',\n    formItemClass: 'items-start',\n    label: '备注',\n  },\n];\n"
  },
  {
    "path": "apps/web-antd/src/views/system/user/dept-tree.vue",
    "content": "<script setup lang=\"ts\">\nimport type { PropType } from 'vue';\n\nimport type { DeptTree } from '#/api/system/user/model';\n\nimport { onMounted, ref } from 'vue';\n\nimport { SyncOutlined } from '@ant-design/icons-vue';\nimport { Empty, InputSearch, Skeleton, Tree } from 'ant-design-vue';\n\nimport { getDeptTree } from '#/api/system/user';\n\ndefineOptions({ inheritAttrs: false });\n\nconst props = withDefaults(defineProps<Props>(), {\n  showSearch: true,\n  api: getDeptTree,\n});\n\nconst emit = defineEmits<{\n  /**\n   * 点击刷新按钮的事件\n   */\n  reload: [];\n  /**\n   * 点击节点的事件\n   */\n  select: [];\n}>();\n\ninterface Props {\n  /**\n   * 调用的接口\n   */\n  api?: () => Promise<DeptTree[]>;\n  /**\n   * 是否显示搜索框\n   */\n  showSearch?: boolean;\n}\n\nconst selectDeptId = defineModel('selectDeptId', {\n  required: true,\n  type: Array as PropType<string[]>,\n});\n\nconst searchValue = defineModel('searchValue', {\n  type: String,\n  default: '',\n});\n\n/** 部门数据源 */\ntype DeptTreeArray = DeptTree[];\nconst deptTreeArray = ref<DeptTreeArray>([]);\n/** 骨架屏加载 */\nconst showTreeSkeleton = ref<boolean>(true);\n\nasync function loadTree() {\n  showTreeSkeleton.value = true;\n  searchValue.value = '';\n  selectDeptId.value = [];\n\n  const ret = await props.api();\n\n  deptTreeArray.value = ret;\n  showTreeSkeleton.value = false;\n}\n\nasync function handleReload() {\n  await loadTree();\n  emit('reload');\n}\n\nonMounted(loadTree);\n</script>\n\n<template>\n  <div :class=\"$attrs.class\">\n    <Skeleton\n      :loading=\"showTreeSkeleton\"\n      :paragraph=\"{ rows: 8 }\"\n      active\n      class=\"p-[8px]\"\n    >\n      <div\n        class=\"bg-background flex h-full flex-col overflow-y-auto rounded-lg\"\n      >\n        <!-- 固定在顶部 必须加上bg-background背景色 否则会产生'穿透'效果 -->\n        <div\n          v-if=\"showSearch\"\n          class=\"bg-background z-100 sticky left-0 top-0 p-[8px]\"\n        >\n          <InputSearch\n            v-model:value=\"searchValue\"\n            :placeholder=\"$t('pages.common.search')\"\n            size=\"small\"\n            allow-clear\n          >\n            <template #enterButton>\n              <a-button @click=\"handleReload\">\n                <SyncOutlined class=\"text-primary\" />\n              </a-button>\n            </template>\n          </InputSearch>\n        </div>\n        <div class=\"h-full overflow-x-hidden px-[8px]\">\n          <Tree\n            v-bind=\"$attrs\"\n            v-if=\"deptTreeArray.length > 0\"\n            v-model:selected-keys=\"selectDeptId\"\n            :class=\"$attrs.class\"\n            :field-names=\"{ title: 'label', key: 'id' }\"\n            :show-line=\"{ showLeafIcon: false }\"\n            :tree-data=\"deptTreeArray\"\n            :virtual=\"false\"\n            default-expand-all\n            @select=\"$emit('select')\"\n          >\n            <template #title=\"{ label }\">\n              <span v-if=\"label.includes(searchValue)\">\n                {{ label.substring(0, label.indexOf(searchValue)) }}\n                <span class=\"text-primary\">{{ searchValue }}</span>\n                {{\n                  label.substring(\n                    label.indexOf(searchValue) + searchValue.length,\n                  )\n                }}\n              </span>\n              <span v-else>{{ label }}</span>\n            </template>\n          </Tree>\n          <!-- 仅本人数据权限 可以考虑直接不显示 -->\n          <div v-else class=\"mt-5\">\n            <Empty\n              :image=\"Empty.PRESENTED_IMAGE_SIMPLE\"\n              description=\"无部门数据\"\n            />\n          </div>\n        </div>\n      </div>\n    </Skeleton>\n  </div>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/system/user/index.vue",
    "content": "<script setup lang=\"ts\">\nimport type { VbenFormProps } from '@vben/common-ui';\n\nimport type { VxeGridProps } from '#/adapter/vxe-table';\nimport type { User } from '#/api/system/user/model';\n\nimport { ref } from 'vue';\n\nimport { useAccess } from '@vben/access';\nimport { Page, useVbenDrawer, useVbenModal } from '@vben/common-ui';\nimport { $t } from '@vben/locales';\nimport { preferences } from '@vben/preferences';\nimport { getVxePopupContainer } from '@vben/utils';\n\nimport {\n  Avatar,\n  Dropdown,\n  Menu,\n  MenuItem,\n  Modal,\n  Popconfirm,\n  Space,\n} from 'ant-design-vue';\n\nimport { useVbenVxeGrid, vxeCheckboxChecked } from '#/adapter/vxe-table';\nimport {\n  userExport,\n  userList,\n  userRemove,\n  userStatusChange,\n} from '#/api/system/user';\nimport { TableSwitch } from '#/components/table';\nimport { commonDownloadExcel } from '#/utils/file/download';\n\nimport { columns, querySchema } from './data';\nimport DeptTree from './dept-tree.vue';\nimport userDrawer from './user-drawer.vue';\nimport userImportModal from './user-import-modal.vue';\nimport userInfoModal from './user-info-modal.vue';\nimport userResetPwdModal from './user-reset-pwd-modal.vue';\n\n/**\n * 导入\n */\nconst [UserImpotModal, userImportModalApi] = useVbenModal({\n  connectedComponent: userImportModal,\n});\n\nfunction handleImport() {\n  userImportModalApi.open();\n}\n\n// 左边部门用\nconst selectDeptId = ref<string[]>([]);\n\nconst formOptions: VbenFormProps = {\n  schema: querySchema(),\n  commonConfig: {\n    labelWidth: 80,\n    componentProps: {\n      allowClear: true,\n    },\n  },\n  wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4',\n  handleReset: async () => {\n    selectDeptId.value = [];\n\n    const { formApi, reload } = tableApi;\n    await formApi.resetForm();\n    const formValues = formApi.form.values;\n    formApi.setLatestSubmissionValues(formValues);\n    await reload(formValues);\n  },\n  // 日期选择格式化\n  fieldMappingTime: [\n    [\n      'createTime',\n      ['params[beginTime]', 'params[endTime]'],\n      ['YYYY-MM-DD 00:00:00', 'YYYY-MM-DD 23:59:59'],\n    ],\n  ],\n};\n\nconst gridOptions: VxeGridProps = {\n  checkboxConfig: {\n    // 高亮\n    highlight: true,\n    // 翻页时保留选中状态\n    reserve: true,\n    // 点击行选中\n    trigger: 'default',\n    checkMethod: ({ row }) => row?.userId !== 1,\n  },\n  columns,\n  height: 'auto',\n  keepSource: true,\n  pagerConfig: {},\n  proxyConfig: {\n    ajax: {\n      query: async ({ page }, formValues = {}) => {\n        // 部门树选择处理\n        if (selectDeptId.value.length === 1) {\n          formValues.deptId = selectDeptId.value[0];\n        } else {\n          Reflect.deleteProperty(formValues, 'deptId');\n        }\n\n        return await userList({\n          pageNum: page.currentPage,\n          pageSize: page.pageSize,\n          ...formValues,\n        });\n      },\n    },\n  },\n  headerCellConfig: {\n    height: 44,\n  },\n  cellConfig: {\n    height: 48,\n  },\n  rowConfig: {\n    keyField: 'userId',\n  },\n  id: 'system-user-index',\n};\nconst [BasicTable, tableApi] = useVbenVxeGrid({\n  formOptions,\n  gridOptions,\n});\n\nconst [UserDrawer, userDrawerApi] = useVbenDrawer({\n  connectedComponent: userDrawer,\n});\n\nfunction handleAdd() {\n  userDrawerApi.setData({});\n  userDrawerApi.open();\n}\n\nfunction handleEdit(row: User) {\n  userDrawerApi.setData({ id: row.userId });\n  userDrawerApi.open();\n}\n\nasync function handleDelete(row: User) {\n  await userRemove([row.userId]);\n  await tableApi.query();\n}\n\nfunction handleMultiDelete() {\n  const rows = tableApi.grid.getCheckboxRecords();\n  const ids = rows.map((row: User) => row.userId);\n  Modal.confirm({\n    title: '提示',\n    okType: 'danger',\n    content: `确认删除选中的${ids.length}条记录吗？`,\n    onOk: async () => {\n      await userRemove(ids);\n      await tableApi.query();\n    },\n  });\n}\n\nfunction handleDownloadExcel() {\n  commonDownloadExcel(userExport, '用户管理', tableApi.formApi.form.values, {\n    fieldMappingTime: formOptions.fieldMappingTime,\n  });\n}\n\nconst [UserInfoModal, userInfoModalApi] = useVbenModal({\n  connectedComponent: userInfoModal,\n});\nfunction handleUserInfo(row: User) {\n  userInfoModalApi.setData({ userId: row.userId });\n  userInfoModalApi.open();\n}\n\nconst [UserResetPwdModal, userResetPwdModalApi] = useVbenModal({\n  connectedComponent: userResetPwdModal,\n});\n\nfunction handleResetPwd(record: User) {\n  userResetPwdModalApi.setData({ record });\n  userResetPwdModalApi.open();\n}\n\nconst { hasAccessByCodes } = useAccess();\n</script>\n\n<template>\n  <Page :auto-content-height=\"true\">\n    <div class=\"flex h-full gap-[8px]\">\n      <DeptTree\n        v-model:select-dept-id=\"selectDeptId\"\n        class=\"w-[260px]\"\n        @reload=\"() => tableApi.reload()\"\n        @select=\"() => tableApi.reload()\"\n      />\n      <BasicTable class=\"flex-1 overflow-hidden\" table-title=\"用户列表\">\n        <template #toolbar-tools>\n          <Space>\n            <a-button\n              v-access:code=\"['system:user:export']\"\n              @click=\"handleDownloadExcel\"\n            >\n              {{ $t('pages.common.export') }}\n            </a-button>\n            <a-button\n              v-access:code=\"['system:user:import']\"\n              @click=\"handleImport\"\n            >\n              {{ $t('pages.common.import') }}\n            </a-button>\n            <a-button\n              :disabled=\"!vxeCheckboxChecked(tableApi)\"\n              danger\n              type=\"primary\"\n              v-access:code=\"['system:user:remove']\"\n              @click=\"handleMultiDelete\"\n            >\n              {{ $t('pages.common.delete') }}\n            </a-button>\n            <a-button\n              type=\"primary\"\n              v-access:code=\"['system:user:add']\"\n              @click=\"handleAdd\"\n            >\n              {{ $t('pages.common.add') }}\n            </a-button>\n          </Space>\n        </template>\n        <template #avatar=\"{ row }\">\n          <!-- 可能要判断空字符串情况 所以没有使用?? -->\n          <Avatar :src=\"row.avatar || preferences.app.defaultAvatar\" />\n        </template>\n        <template #status=\"{ row }\">\n          <TableSwitch\n            v-model:value=\"row.status\"\n            :api=\"() => userStatusChange(row)\"\n            :disabled=\"\n              row.userId === 1 || !hasAccessByCodes(['system:user:edit'])\n            \"\n            @reload=\"() => tableApi.query()\"\n          />\n        </template>\n        <template #action=\"{ row }\">\n          <template v-if=\"row.userId !== 1\">\n            <Space>\n              <ghost-button\n                v-access:code=\"['system:user:edit']\"\n                @click.stop=\"handleEdit(row)\"\n              >\n                {{ $t('pages.common.edit') }}\n              </ghost-button>\n              <Popconfirm\n                :get-popup-container=\"getVxePopupContainer\"\n                placement=\"left\"\n                title=\"确认删除？\"\n                @confirm=\"handleDelete(row)\"\n              >\n                <ghost-button\n                  danger\n                  v-access:code=\"['system:user:remove']\"\n                  @click.stop=\"\"\n                >\n                  {{ $t('pages.common.delete') }}\n                </ghost-button>\n              </Popconfirm>\n            </Space>\n            <Dropdown placement=\"bottomRight\">\n              <template #overlay>\n                <Menu>\n                  <MenuItem key=\"1\" @click=\"handleUserInfo(row)\">\n                    用户信息\n                  </MenuItem>\n                  <span v-access:code=\"['system:user:resetPwd']\">\n                    <MenuItem key=\"2\" @click=\"handleResetPwd(row)\">\n                      重置密码\n                    </MenuItem>\n                  </span>\n                </Menu>\n              </template>\n              <a-button size=\"small\" type=\"link\">\n                {{ $t('pages.common.more') }}\n              </a-button>\n            </Dropdown>\n          </template>\n        </template>\n      </BasicTable>\n    </div>\n    <UserImpotModal @reload=\"tableApi.query()\" />\n    <UserDrawer @reload=\"tableApi.query()\" />\n    <UserInfoModal />\n    <UserResetPwdModal />\n  </Page>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/system/user/user-drawer.vue",
    "content": "<script setup lang=\"ts\">\nimport type { Role } from '#/api/system/user/model';\n\nimport { computed, h, onMounted, ref } from 'vue';\n\nimport { useVbenDrawer } from '@vben/common-ui';\nimport { $t } from '@vben/locales';\nimport { addFullName, cloneDeep, getPopupContainer } from '@vben/utils';\n\nimport { Tag } from 'ant-design-vue';\n\nimport { useVbenForm } from '#/adapter/form';\nimport { configInfoByKey } from '#/api/system/config';\nimport { postOptionSelect } from '#/api/system/post';\nimport {\n  findUserInfo,\n  getDeptTree,\n  userAdd,\n  userUpdate,\n} from '#/api/system/user';\nimport { defaultFormValueGetter, useBeforeCloseDiff } from '#/utils/popup';\nimport { authScopeOptions } from '#/views/system/role/data';\n\nimport { drawerSchema } from './data';\n\nconst emit = defineEmits<{ reload: [] }>();\n\nconst isUpdate = ref(false);\nconst title = computed(() => {\n  return isUpdate.value ? $t('pages.common.edit') : $t('pages.common.add');\n});\n\nconst [BasicForm, formApi] = useVbenForm({\n  commonConfig: {\n    formItemClass: 'col-span-2',\n    componentProps: {\n      class: 'w-full',\n    },\n    labelWidth: 80,\n  },\n  schema: drawerSchema(),\n  showDefaultActions: false,\n  wrapperClass: 'grid-cols-2',\n});\n\n/**\n * 生成角色的自定义label\n * 也可以用option插槽来做\n * renderComponentContent: () => ({\n    option: ({value, label, [disabled, key, title]}) => '',\n  }),\n */\nfunction genRoleOptionlabel(role: Role) {\n  const found = authScopeOptions.find((item) => item.value === role.dataScope);\n  if (!found) {\n    return role.roleName;\n  }\n  return h('div', { class: 'flex items-center gap-[6px]' }, [\n    h('span', null, role.roleName),\n    h(Tag, { color: found.color }, () => found.label),\n  ]);\n}\n\n/**\n * 岗位的加载\n */\nasync function setupPostOptions(deptId: number | string) {\n  const postListResp = await postOptionSelect(deptId);\n  const options = postListResp.map((item) => ({\n    label: item.postName,\n    value: item.postId,\n  }));\n  const placeholder = options.length > 0 ? '请选择' : '该部门下暂无岗位';\n  formApi.updateSchema([\n    {\n      componentProps: { options, placeholder },\n      fieldName: 'postIds',\n    },\n  ]);\n}\n\n/**\n * 初始化部门选择\n */\nasync function setupDeptSelect() {\n  // updateSchema\n  const deptTree = await getDeptTree();\n  // 选中后显示在输入框的值 即父节点 / 子节点\n  addFullName(deptTree, 'label', ' / ');\n  formApi.updateSchema([\n    {\n      componentProps: (formModel) => ({\n        class: 'w-full',\n        fieldNames: {\n          key: 'id',\n          value: 'id',\n          children: 'children',\n        },\n        getPopupContainer,\n        async onSelect(deptId: number | string) {\n          /** 根据部门ID加载岗位 */\n          await setupPostOptions(deptId);\n          /** 变化后需要重新选择岗位 */\n          formModel.postIds = [];\n        },\n        placeholder: '请选择',\n        showSearch: true,\n        treeData: deptTree,\n        treeDefaultExpandAll: true,\n        treeLine: { showLeafIcon: false },\n        // 筛选的字段\n        treeNodeFilterProp: 'label',\n        // 选中后显示在输入框的值\n        treeNodeLabelProp: 'fullName',\n      }),\n      fieldName: 'deptId',\n    },\n  ]);\n}\n\nconst defaultPassword = ref('');\nonMounted(async () => {\n  const password = await configInfoByKey('sys.user.initPassword');\n  if (password) {\n    defaultPassword.value = password;\n  }\n});\n\n/**\n * 新增时候 从参数设置获取默认密码\n */\nasync function loadDefaultPassword(update: boolean) {\n  if (!update && defaultPassword.value) {\n    formApi.setFieldValue('password', defaultPassword.value);\n  }\n}\n\nconst { onBeforeClose, markInitialized, resetInitialized } = useBeforeCloseDiff(\n  {\n    initializedGetter: defaultFormValueGetter(formApi),\n    currentGetter: defaultFormValueGetter(formApi),\n  },\n);\n\nconst [BasicDrawer, drawerApi] = useVbenDrawer({\n  onBeforeClose,\n  onClosed: handleClosed,\n  onConfirm: handleConfirm,\n  async onOpenChange(isOpen) {\n    if (!isOpen) {\n      // 需要重置岗位选择\n      formApi.updateSchema([\n        {\n          componentProps: { options: [], placeholder: '请先选择部门' },\n          fieldName: 'postIds',\n        },\n      ]);\n      return null;\n    }\n    drawerApi.drawerLoading(true);\n\n    const { id } = drawerApi.getData() as { id?: number | string };\n    isUpdate.value = !!id;\n    /** update时 禁用用户名修改 不显示密码框 */\n    formApi.updateSchema([\n      { componentProps: { disabled: isUpdate.value }, fieldName: 'userName' },\n      {\n        dependencies: { if: () => !isUpdate.value, triggerFields: ['id'] },\n        fieldName: 'password',\n      },\n    ]);\n    // 更新 && 赋值\n    const { postIds, posts, roleIds, roles, user } = await findUserInfo(id);\n    const postOptions = (posts ?? []).map((item) => ({\n      label: item.postName,\n      value: item.postId,\n    }));\n    formApi.updateSchema([\n      {\n        componentProps: {\n          // title用于选中后回填到输入框 默认为label\n          optionLabelProp: 'title',\n          options: roles.map((item) => ({\n            label: genRoleOptionlabel(item),\n            // title用于选中后回填到输入框 默认为label\n            title: item.roleName,\n            value: item.roleId,\n          })),\n        },\n        fieldName: 'roleIds',\n      },\n      {\n        componentProps: {\n          options: postOptions,\n        },\n        fieldName: 'postIds',\n      },\n    ]);\n\n    // 部门选择、初始密码及用户相关操作并行处理\n    const promises = [setupDeptSelect(), loadDefaultPassword(isUpdate.value)];\n    if (user) {\n      promises.push(\n        // 添加基础信息\n        formApi.setValues(user),\n        // 添加角色和岗位\n        formApi.setFieldValue('postIds', postIds),\n        formApi.setFieldValue('roleIds', roleIds),\n        // 更新时不会触发onSelect 需要手动调用\n        setupPostOptions(user.deptId),\n      );\n    }\n    // 并行处理 重构后会带来10-50ms的优化\n    await Promise.all(promises);\n    await markInitialized();\n\n    drawerApi.drawerLoading(false);\n  },\n});\n\nasync function handleConfirm() {\n  try {\n    drawerApi.lock(true);\n    const { valid } = await formApi.validate();\n    if (!valid) {\n      return;\n    }\n    const data = cloneDeep(await formApi.getValues());\n    await (isUpdate.value ? userUpdate(data) : userAdd(data));\n    resetInitialized();\n    emit('reload');\n    drawerApi.close();\n  } catch (error) {\n    console.error(error);\n  } finally {\n    drawerApi.lock(false);\n  }\n}\n\nasync function handleClosed() {\n  formApi.resetForm();\n  resetInitialized();\n}\n</script>\n\n<template>\n  <BasicDrawer :title=\"title\" class=\"w-[600px]\">\n    <BasicForm />\n  </BasicDrawer>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/system/user/user-import-modal.vue",
    "content": "<script setup lang=\"ts\">\nimport type { UploadFile } from 'ant-design-vue/es/upload/interface';\n\nimport { h, ref, unref } from 'vue';\n\nimport { useVbenModal } from '@vben/common-ui';\nimport { ExcelIcon, InBoxIcon } from '@vben/icons';\n\nimport { Modal, Switch, Upload } from 'ant-design-vue';\n\nimport { downloadImportTemplate, userImportData } from '#/api/system/user';\nimport { commonDownloadExcel } from '#/utils/file/download';\n\nconst emit = defineEmits<{ reload: [] }>();\n\nconst UploadDragger = Upload.Dragger;\n\nconst [BasicModal, modalApi] = useVbenModal({\n  onCancel: handleCancel,\n  onConfirm: handleSubmit,\n});\n\nconst fileList = ref<UploadFile[]>([]);\nconst checked = ref(false);\n\nasync function handleSubmit() {\n  try {\n    modalApi.modalLoading(true);\n    if (fileList.value.length !== 1) {\n      handleCancel();\n      return;\n    }\n    const data = {\n      file: fileList.value[0]!.originFileObj as Blob,\n      updateSupport: unref(checked),\n    };\n    const { code, msg } = await userImportData(data);\n    let modal = Modal.success;\n    if (code === 200) {\n      emit('reload');\n    } else {\n      modal = Modal.error;\n    }\n    handleCancel();\n    modal({\n      content: h('div', {\n        class: 'max-h-[260px] overflow-y-auto',\n        innerHTML: msg, // 后台已经处理xss问题\n      }),\n      title: '提示',\n    });\n  } catch (error) {\n    console.warn(error);\n    modalApi.close();\n  } finally {\n    modalApi.modalLoading(false);\n  }\n}\n\nfunction handleCancel() {\n  modalApi.close();\n  fileList.value = [];\n  checked.value = false;\n}\n</script>\n\n<template>\n  <BasicModal\n    :close-on-click-modal=\"false\"\n    :fullscreen-button=\"false\"\n    title=\"用户导入\"\n  >\n    <!-- z-index不设置会遮挡模板下载loading -->\n    <!-- 手动处理 而不是放入文件就上传 -->\n    <UploadDragger\n      v-model:file-list=\"fileList\"\n      :before-upload=\"() => false\"\n      :max-count=\"1\"\n      :show-upload-list=\"true\"\n      accept=\"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel\"\n    >\n      <p class=\"ant-upload-drag-icon flex items-center justify-center\">\n        <InBoxIcon class=\"text-primary size-[48px]\" />\n      </p>\n      <p class=\"ant-upload-text\">点击或者拖拽到此处上传文件</p>\n    </UploadDragger>\n    <div class=\"mt-2 flex flex-col gap-2\">\n      <div class=\"flex items-center gap-2\">\n        <span>允许导入xlsx, xls文件</span>\n        <a-button\n          type=\"link\"\n          @click=\"commonDownloadExcel(downloadImportTemplate, '用户导入模板')\"\n        >\n          <div class=\"flex items-center gap-[4px]\">\n            <ExcelIcon />\n            <span>下载模板</span>\n          </div>\n        </a-button>\n      </div>\n      <div class=\"flex items-center gap-2\">\n        <span :class=\"{ 'text-red-500': checked }\">\n          是否更新/覆盖已存在的用户数据\n        </span>\n        <Switch v-model:checked=\"checked\" />\n      </div>\n    </div>\n  </BasicModal>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/system/user/user-info-modal.vue",
    "content": "<script setup lang=\"ts\">\nimport type { User } from '#/api/system/user/model';\n\nimport { computed, shallowRef } from 'vue';\n\nimport { useVbenModal } from '@vben/common-ui';\nimport { DictEnum } from '@vben/constants';\n\nimport { Descriptions, DescriptionsItem, Tag } from 'ant-design-vue';\nimport dayjs from 'dayjs';\nimport duration from 'dayjs/plugin/duration';\nimport relativeTime from 'dayjs/plugin/relativeTime';\n\nimport { findUserInfo } from '#/api/system/user';\nimport { renderDict } from '#/utils/render';\n\ndayjs.extend(duration);\ndayjs.extend(relativeTime);\n\nconst [BasicModal, modalApi] = useVbenModal({\n  onOpenChange: handleOpenChange,\n  onClosed() {\n    currentUser.value = null;\n  },\n});\n\ninterface UserWithNames extends User {\n  postNames: string[];\n  roleNames: string[];\n}\nconst currentUser = shallowRef<null | UserWithNames>(null);\n\nasync function handleOpenChange(open: boolean) {\n  if (!open) {\n    return null;\n  }\n  modalApi.modalLoading(true);\n\n  const { userId } = modalApi.getData() as { userId: number | string };\n  const response = await findUserInfo(userId);\n  // 外部的roleIds postIds才是真正对应的  新增时为空\n  // posts有为Null的情况 需要给默认值\n  const { postIds = [], posts = [], roleIds = [], roles = [], user } = response;\n\n  const postNames = posts\n    .filter((item) => postIds.includes(item.postId))\n    .map((item) => item.postName);\n\n  const roleNames = roles\n    .filter((item) => roleIds.includes(item.roleId))\n    .map((item) => item.roleName);\n\n  (user as UserWithNames).postNames = postNames;\n  (user as UserWithNames).roleNames = roleNames;\n  // 赋值\n  currentUser.value = user as UserWithNames;\n\n  modalApi.modalLoading(false);\n}\n\nconst mixInfo = computed(() => {\n  if (!currentUser.value) {\n    return '-';\n  }\n  const { deptName, nickName, userName } = currentUser.value;\n  return `${userName} / ${nickName} / ${deptName ?? '-'}`;\n});\n\nconst diffLoginTime = computed(() => {\n  if (!currentUser.value) {\n    return '-';\n  }\n  const { loginDate } = currentUser.value;\n  // 默认en显示\n  dayjs.locale('zh-cn');\n  // 计算相差秒数\n  const diffSeconds = dayjs().diff(dayjs(loginDate), 'second');\n  /**\n   * 转为时间显示(x月 x天)\n   * https://dayjs.fenxianglu.cn/category/duration.html#%E4%BA%BA%E6%80%A7%E5%8C%96\n   *\n   */\n  const diffText = dayjs.duration(diffSeconds, 'seconds').humanize();\n  return diffText;\n});\n</script>\n\n<template>\n  <BasicModal :footer=\"false\" :fullscreen-button=\"false\" title=\"用户信息\">\n    <Descriptions v-if=\"currentUser\" size=\"small\" :column=\"1\" bordered>\n      <DescriptionsItem label=\"userId\">\n        {{ currentUser.userId }}\n      </DescriptionsItem>\n      <DescriptionsItem label=\"用户状态\">\n        <component\n          :is=\"renderDict(currentUser.status, DictEnum.SYS_NORMAL_DISABLE)\"\n        />\n      </DescriptionsItem>\n      <DescriptionsItem label=\"用户信息\">\n        {{ mixInfo }}\n      </DescriptionsItem>\n      <DescriptionsItem label=\"手机号\">\n        {{ currentUser.phonenumber || '-' }}\n      </DescriptionsItem>\n      <DescriptionsItem label=\"邮箱\">\n        {{ currentUser.email || '-' }}\n      </DescriptionsItem>\n      <DescriptionsItem label=\"岗位\">\n        <div\n          v-if=\"currentUser.postNames.length > 0\"\n          class=\"flex flex-wrap gap-0.5\"\n        >\n          <Tag v-for=\"item in currentUser.postNames\" :key=\"item\">\n            {{ item }}\n          </Tag>\n        </div>\n        <span v-else>-</span>\n      </DescriptionsItem>\n      <DescriptionsItem label=\"权限\">\n        <div\n          v-if=\"currentUser.roleNames.length > 0\"\n          class=\"flex flex-wrap gap-0.5\"\n        >\n          <Tag v-for=\"item in currentUser.roleNames\" :key=\"item\">\n            {{ item }}\n          </Tag>\n        </div>\n        <span v-else>-</span>\n      </DescriptionsItem>\n      <DescriptionsItem label=\"创建时间\">\n        {{ currentUser.createTime }}\n      </DescriptionsItem>\n      <DescriptionsItem label=\"上次登录IP\">\n        {{ currentUser.loginIp ?? '-' }}\n      </DescriptionsItem>\n      <DescriptionsItem label=\"上次登录时间\">\n        <span>{{ currentUser.loginDate ?? '-' }}</span>\n        <Tag\n          class=\"ml-2\"\n          v-if=\"diffLoginTime\"\n          :bordered=\"false\"\n          color=\"processing\"\n        >\n          {{ diffLoginTime }}前\n        </Tag>\n      </DescriptionsItem>\n      <DescriptionsItem label=\"备注\">\n        {{ currentUser.remark ?? '-' }}\n      </DescriptionsItem>\n    </Descriptions>\n  </BasicModal>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/system/user/user-reset-pwd-modal.vue",
    "content": "<script setup lang=\"ts\">\nimport type { ResetPwdParam, User } from '#/api/system/user/model';\n\nimport { ref } from 'vue';\n\nimport { useVbenModal, z } from '@vben/common-ui';\n\nimport { Descriptions, DescriptionsItem } from 'ant-design-vue';\n\nimport { useVbenForm } from '#/adapter/form';\nimport { userResetPassword } from '#/api/system/user';\n\nconst emit = defineEmits<{ reload: [] }>();\n\nconst [BasicModal, modalApi] = useVbenModal({\n  onClosed: handleClosed,\n  onConfirm: handleSubmit,\n  onOpenChange: handleOpenChange,\n});\n\nconst [BasicForm, formApi] = useVbenForm({\n  schema: [\n    {\n      component: 'Input',\n      dependencies: {\n        show: () => false,\n        triggerFields: [''],\n      },\n      fieldName: 'userId',\n      label: '用户ID',\n      rules: 'required',\n    },\n    {\n      component: 'InputPassword',\n      componentProps: {\n        placeholder: '请输入新的密码, 密码长度为5 - 20',\n      },\n      fieldName: 'password',\n      label: '新的密码',\n      rules: z\n        .string()\n        .min(5, { message: '密码长度为5 - 20' })\n        .max(20, { message: '密码长度为5 - 20' }),\n    },\n  ],\n  showDefaultActions: false,\n  commonConfig: {\n    labelWidth: 80,\n  },\n});\n\nconst currentUser = ref<null | User>(null);\nasync function handleOpenChange(open: boolean) {\n  if (!open) {\n    return null;\n  }\n  modalApi.modalLoading(true);\n\n  const { record } = modalApi.getData() as { record: User };\n  currentUser.value = record;\n  await formApi.setValues({ userId: record.userId });\n\n  modalApi.modalLoading(false);\n}\n\nasync function handleSubmit() {\n  try {\n    modalApi.modalLoading(true);\n    const { valid } = await formApi.validate();\n    if (!valid) {\n      return;\n    }\n    const data = await formApi.getValues();\n    await userResetPassword(data as ResetPwdParam);\n    emit('reload');\n    handleClosed();\n  } catch (error) {\n    console.error(error);\n  } finally {\n    modalApi.modalLoading(false);\n  }\n}\n\nasync function handleClosed() {\n  modalApi.close();\n  await formApi.resetForm();\n  currentUser.value = null;\n}\n</script>\n\n<template>\n  <BasicModal\n    :close-on-click-modal=\"false\"\n    :fullscreen-button=\"false\"\n    title=\"重置密码\"\n  >\n    <div class=\"flex flex-col gap-[12px]\">\n      <Descriptions v-if=\"currentUser\" size=\"small\" :column=\"1\" bordered>\n        <DescriptionsItem label=\"用户ID\">\n          {{ currentUser.userId }}\n        </DescriptionsItem>\n        <DescriptionsItem label=\"用户名\">\n          {{ currentUser.userName }}\n        </DescriptionsItem>\n        <DescriptionsItem label=\"昵称\">\n          {{ currentUser.nickName }}\n        </DescriptionsItem>\n      </Descriptions>\n      <BasicForm />\n    </div>\n  </BasicModal>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/tool/gen/code-preview-modal.vue",
    "content": "<script setup lang=\"ts\">\nimport type { Key } from 'ant-design-vue/es/vc-tree/interface';\n\nimport type { Component } from 'vue';\n\nimport type { LanguageSupport } from '@vben/common-ui';\nimport type { Recordable } from '@vben/types';\n\nimport { markRaw, ref } from 'vue';\n\nimport { CodeMirror, useVbenModal } from '@vben/common-ui';\nimport {\n  DefaultFileIcon,\n  FolderIcon,\n  JavaIcon,\n  SqlIcon,\n  TsIcon,\n  VueIcon,\n  XmlIcon,\n} from '@vben/icons';\n\nimport { useClipboard } from '@vueuse/core';\nimport { Alert, Skeleton, Tree } from 'ant-design-vue';\n\nimport { previewCode } from '#/api/tool/gen';\n\ninterface TreeNode {\n  children: TreeNode[];\n  title: string;\n  key: string;\n  icon: Component; // 树左边图标\n}\n\nconst treeData = ref<TreeNode[]>([]);\n/** modal标题 */\nconst modalTitle = ref('代码预览');\n/** 代码内容 */\nconst codeContent = ref('点击左侧树节点查看代码');\n/** code */\nconst currentCodeData = ref<null | Recordable<any>>(null);\n\nconst [BasicModal, modalApi] = useVbenModal({\n  async onOpenChange(isOpen) {\n    if (!isOpen) {\n      handleClose();\n      return null;\n    }\n    modalApi.modalLoading(true);\n\n    const { tableId } = modalApi.getData() as { tableId: string };\n    const data = await previewCode(tableId);\n    currentCodeData.value = data;\n    const tree = convertToTree(Object.keys(data));\n    treeData.value = tree;\n\n    modalApi.modalLoading(false);\n  },\n});\n\n/**\n * 文件路径数组转树结构\n * @param paths 文件路径数组\n */\nfunction convertToTree(paths: string[]): TreeNode[] {\n  const tree: TreeNode[] = [];\n\n  for (const path of paths) {\n    const segments = path.split('/');\n    let currentNode = tree;\n    let currentPath = '';\n\n    for (let i = 0; i < segments.length; i++) {\n      const segment = segments[i];\n      currentPath += `${segment}`;\n      if (i !== segments.length - 1) {\n        currentPath += '/';\n      }\n\n      const existingNode = currentNode.find((node) => node.title === segment);\n\n      if (existingNode) {\n        currentNode = existingNode.children || [];\n      } else {\n        const title = (segment ?? '').replace('.vm', '');\n        const newNode: TreeNode = {\n          icon: findIcon(currentPath),\n          key: currentPath,\n          title,\n          children: [],\n        };\n        currentNode.push(newNode);\n        currentNode = newNode.children;\n      }\n    }\n  }\n\n  return tree;\n}\n\nconst iconMap = [\n  { key: 'java', value: markRaw(JavaIcon) },\n  { key: 'xml', value: markRaw(XmlIcon) },\n  { key: 'sql', value: markRaw(SqlIcon) },\n  { key: 'ts', value: markRaw(TsIcon) },\n  { key: 'vue', value: markRaw(VueIcon) },\n  { key: 'folder', value: markRaw(FolderIcon) },\n];\nfunction findIcon(path: string) {\n  const defaultFileIcon = DefaultFileIcon;\n  const defaultFolderIcon = FolderIcon;\n  if (path.endsWith('.vm')) {\n    const realPath = path.slice(0, -3);\n    // 是否为指定拓展名\n    const icon = iconMap.find((item) => realPath.endsWith(item.key));\n    if (icon) {\n      return icon.value;\n    }\n    return defaultFileIcon;\n  }\n  // 其他的为文件夹\n  return defaultFolderIcon;\n}\n\nconst language = ref<LanguageSupport>('html');\nfunction changeLanguageType(filename: string) {\n  const typeList: { language: LanguageSupport; type: string }[] = [\n    { language: 'ts', type: '.ts' },\n    { language: 'java', type: '.java' },\n    { language: 'xml', type: '.xml' },\n    { language: 'sql', type: 'sql' },\n    { language: 'vue', type: '.vue' },\n  ];\n  const type = typeList.find((item) => filename.includes(item.type));\n  language.value = type ? type.language : 'html';\n}\n\nfunction handleSelect(selectedKeys: Key[]) {\n  const [currentFile = ''] = selectedKeys as string[];\n  if (!currentCodeData.value) {\n    return;\n  }\n  const currentCode =\n    currentCodeData.value[currentFile as keyof typeof currentCodeData.value];\n  if (currentCode) {\n    // 设置代码type\n    changeLanguageType(currentFile);\n    // 内容\n    codeContent.value = currentCode;\n    // 修改标题\n    modalTitle.value = `代码预览: ${currentFile.replace('.vm', '')}`;\n  }\n}\n\nfunction handleClose() {\n  currentCodeData.value = null;\n  codeContent.value = '点击左侧树节点查看代码';\n  modalTitle.value = '代码预览';\n  language.value = 'html';\n}\n\nconst { copy } = useClipboard({ legacy: true });\n</script>\n\n<template>\n  <BasicModal\n    :footer=\"false\"\n    :fullscreen=\"true\"\n    :fullscreen-button=\"false\"\n    :title=\"modalTitle\"\n  >\n    <div v-if=\"currentCodeData\" class=\"flex gap-[8px]\">\n      <div class=\"h-[calc(100vh-80px)] w-[300px] overflow-y-scroll\">\n        <Tree\n          v-if=\"treeData.length > 0\"\n          :show-line=\"{ showLeafIcon: false }\"\n          :tree-data=\"treeData\"\n          :virtual=\"false\"\n          default-expand-all\n          @select=\"handleSelect\"\n        >\n          <template #title=\"{ title, icon }\">\n            <div class=\"flex items-center gap-[16px]\">\n              <component :is=\"icon\" />\n              <span>{{ title }}</span>\n            </div>\n          </template>\n        </Tree>\n        <Alert\n          class=\"mt-2\"\n          show-icon\n          message=\"👆显示的名称为模板的文件名，非最终下载文件名...\"\n        />\n      </div>\n      <CodeMirror\n        v-model=\"codeContent\"\n        :language=\"language\"\n        class=\"h-[calc(100vh-80px)] w-full overflow-y-scroll text-[16px]\"\n        readonly\n      />\n      <div class=\"fixed right-20 top-20\">\n        <a-button @click=\"copy(codeContent)\">复制</a-button>\n      </div>\n    </div>\n    <Skeleton v-if=\"!currentCodeData\" active />\n  </BasicModal>\n</template>\n\n<style lang=\"scss\" scoped>\n:deep(.ant-tree .ant-tree-switcher) {\n  display: flex;\n  align-items: center;\n}\n\n/** codeMirror 占满容器高度 即calc计算的高度 */\n:deep(.cm-editor) {\n  height: 100%;\n}\n</style>\n"
  },
  {
    "path": "apps/web-antd/src/views/tool/gen/data.tsx",
    "content": "import type { FormSchemaGetter } from '#/adapter/form';\nimport type { VxeGridProps } from '#/adapter/vxe-table';\n\nexport const querySchema: FormSchemaGetter = () => [\n  {\n    component: 'Select',\n    fieldName: 'dataName',\n    label: '数据源',\n    defaultValue: '',\n    componentProps: {\n      allowClear: false,\n    },\n  },\n  {\n    component: 'Input',\n    fieldName: 'tableName',\n    label: '表名称',\n  },\n  {\n    component: 'Input',\n    fieldName: 'tableComment',\n    label: '表描述',\n  },\n  {\n    component: 'RangePicker',\n    fieldName: 'createTime',\n    label: '创建时间',\n  },\n];\n\nexport const columns: VxeGridProps['columns'] = [\n  { type: 'checkbox', width: 60 },\n  {\n    field: 'tableName',\n    title: '表名称',\n  },\n  {\n    field: 'tableComment',\n    title: '表描述',\n  },\n  {\n    field: 'className',\n    title: '实体类',\n  },\n  {\n    field: 'createTime',\n    title: '创建时间',\n  },\n  {\n    field: 'updateTime',\n    title: '更新时间',\n  },\n  {\n    field: 'action',\n    fixed: 'right',\n    slots: { default: 'action' },\n    title: '操作',\n    width: 300,\n  },\n];\n"
  },
  {
    "path": "apps/web-antd/src/views/tool/gen/edit-gen.vue",
    "content": "<script setup lang=\"ts\">\nimport type { GenInfo } from '#/api/tool/gen/model';\n\nimport { onMounted, provide, ref, unref, useTemplateRef } from 'vue';\nimport { useRoute, useRouter } from 'vue-router';\n\nimport { Page } from '@vben/common-ui';\nimport { useTabs } from '@vben/hooks';\nimport { cloneDeep, safeParseNumber } from '@vben/utils';\n\nimport { Card, Skeleton, TabPane, Tabs } from 'ant-design-vue';\n\nimport { editSave, genInfo } from '#/api/tool/gen';\n\nimport { BasicSetting, GenConfig } from './edit-steps';\n\nconst { setTabTitle, closeCurrentTab } = useTabs();\nconst routes = useRoute();\n// 获取路由参数\nconst tableId = routes.params.tableId as string;\n\nconst genInfoData = ref<GenInfo['info']>();\n\nprovide('genInfoData', genInfoData);\n\nonMounted(async () => {\n  const resp = await genInfo(tableId);\n  // 需要做菜单转换 严格相等 才能选中回显\n  resp.info.parentMenuId = safeParseNumber(resp.info.parentMenuId);\n  genInfoData.value = resp.info;\n  setTabTitle(`生成配置: ${resp.info.tableName}`);\n});\n\nconst currentTab = ref<'fields' | 'setting'>('setting');\nconst basicSettingRef = useTemplateRef('basicSettingRef');\nconst genConfigRef = useTemplateRef('genConfigRef');\n\nconst router = useRouter();\nasync function handleSave() {\n  try {\n    // 校验tab1\n    const settingValidate = await basicSettingRef.value?.validateForm();\n    if (!settingValidate) {\n      currentTab.value = 'setting';\n      return;\n    }\n    // 校验tab2\n    const genConfigValidate = await genConfigRef.value?.validateTable();\n    if (!genConfigValidate) {\n      currentTab.value = 'fields';\n      return;\n    }\n    const requestData = cloneDeep(unref(genInfoData)!);\n    // 获取表单数据\n    const formValues = await basicSettingRef.value?.getFormValues();\n    // 合并\n    Object.assign(requestData, formValues);\n    // 从表格获取最新的\n    requestData.columns = genConfigRef.value?.getTableRecords() ?? [];\n    // 树表需要添加这个参数\n    if (requestData && requestData.tplCategory === 'tree') {\n      const { treeCode, treeName, treeParentCode } = requestData;\n      requestData.params = {\n        treeCode,\n        treeName,\n        treeParentCode,\n      };\n    }\n    // 需要进行参数转化\n    if (requestData) {\n      const transform = (ret: boolean) => (ret ? '1' : '0');\n      requestData.columns.forEach((column) => {\n        const { edit, insert, query, required, list } = column;\n        column.isInsert = transform(insert);\n        column.isEdit = transform(edit);\n        column.isList = transform(list);\n        column.isQuery = transform(query);\n        column.isRequired = transform(required);\n      });\n      // 需要手动添加父级菜单 弹窗类型\n      requestData.params = {\n        ...requestData.params,\n        parentMenuId: requestData.parentMenuId,\n        popupComponent: requestData.popupComponent,\n        formComponent: requestData.formComponent,\n      };\n    }\n    // 保存\n    await editSave(requestData);\n    // 关闭 & 跳转\n    await closeCurrentTab();\n    router.push({ path: '/tool/gen', replace: true });\n  } catch (error) {\n    console.error(error);\n  }\n}\n</script>\n\n<template>\n  <Page :auto-content-height=\"true\">\n    <Card\n      class=\"h-full\"\n      v-if=\"genInfoData\"\n      :body-style=\"{ padding: '0 16px 16px' }\"\n    >\n      <Tabs v-model:active-key=\"currentTab\" size=\"middle\">\n        <template #rightExtra>\n          <!-- 因为编辑表格判断点击单元格之外的元素会取消编辑状态，此时需要事件拦截 -->\n          <a-button\n            class=\"vxe-table--ignore-clear\"\n            type=\"primary\"\n            @click=\"handleSave\"\n          >\n            保存配置\n          </a-button>\n        </template>\n        <TabPane key=\"setting\" tab=\"生成信息\" :force-render=\"true\">\n          <BasicSetting ref=\"basicSettingRef\" />\n        </TabPane>\n        <TabPane key=\"fields\" tab=\"字段信息\" :force-render=\"true\">\n          <GenConfig ref=\"genConfigRef\" />\n        </TabPane>\n      </Tabs>\n    </Card>\n    <Skeleton v-else :active=\"true\" />\n  </Page>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/tool/gen/edit-steps/basic-setting.vue",
    "content": "<script setup lang=\"ts\">\nimport type { Ref } from 'vue';\n\nimport type { Column, GenInfo } from '#/api/tool/gen/model';\n\nimport { inject, onMounted } from 'vue';\n\nimport { useVbenForm } from '@vben/common-ui';\nimport { $t } from '@vben/locales';\nimport { addFullName, listToTree } from '@vben/utils';\n\nimport { Col, Row } from 'ant-design-vue';\n\nimport { menuList } from '#/api/system/menu';\n\nimport { formSchema } from './basic';\n\n/**\n * 从父组件注入\n */\nconst genInfoData = inject('genInfoData') as Ref<GenInfo['info']>;\n\nconst [BasicForm, formApi] = useVbenForm({\n  commonConfig: {\n    componentProps: {\n      class: 'w-full',\n      formItemClass: 'col-span-1',\n    },\n    labelWidth: 150,\n  },\n  schema: formSchema(),\n  showDefaultActions: false,\n  wrapperClass: 'grid-cols-2',\n});\n\n/**\n * 树表需要用到的数据\n */\nasync function initTreeSelect(columns: Column[]) {\n  const options = columns.map((item) => {\n    const label = `${item.columnName} | ${item.columnComment}`;\n    return { label, value: item.columnName };\n  });\n  formApi.updateSchema([\n    {\n      componentProps: {\n        options,\n      },\n      fieldName: 'treeCode',\n    },\n    {\n      componentProps: {\n        options,\n      },\n      fieldName: 'treeParentCode',\n    },\n    {\n      componentProps: {\n        options,\n      },\n      fieldName: 'treeName',\n    },\n  ]);\n}\n\n/**\n * 加载菜单选择\n */\nasync function initMenuSelect() {\n  const list = await menuList();\n  // support i18n\n  list.forEach((item) => {\n    item.menuName = $t(item.menuName);\n  });\n  const tree = listToTree(list, { id: 'menuId', pid: 'parentId' });\n  const treeData = [\n    {\n      fullName: $t('menu.root'),\n      menuId: 0,\n      menuName: $t('menu.root'),\n      children: tree,\n    },\n  ];\n  addFullName(treeData, 'menuName', ' / ');\n\n  formApi.updateSchema([\n    {\n      componentProps: {\n        fieldNames: {\n          label: 'menuName',\n          value: 'menuId',\n        },\n        // 设置弹窗滚动高度 默认256\n        listHeight: 300,\n        treeData,\n        treeDefaultExpandAll: false,\n        // 默认展开的树节点\n        treeDefaultExpandedKeys: [0],\n        treeLine: { showLeafIcon: false },\n        treeNodeLabelProp: 'fullName',\n      },\n      fieldName: 'parentMenuId',\n    },\n  ]);\n}\n\nonMounted(async () => {\n  const info = genInfoData.value;\n  await formApi.setValues(info);\n  // 弹出框类型需要手动赋值\n  if (info.options) {\n    const { popupComponent, formComponent } = JSON.parse(info.options);\n    if (popupComponent) {\n      formApi.setFieldValue('popupComponent', popupComponent);\n    }\n    if (formComponent) {\n      formApi.setFieldValue('formComponent', formComponent);\n    }\n  }\n  await Promise.all([initTreeSelect(info.columns), initMenuSelect()]);\n});\n\n/**\n * 校验表单\n */\nasync function validateForm() {\n  const { valid } = await formApi.validate();\n  if (!valid) {\n    return false;\n  }\n  return true;\n}\n\n/**\n * 获取表单值\n */\nasync function getFormValues() {\n  return await formApi.getValues();\n}\n\ndefineExpose({\n  validateForm,\n  getFormValues,\n});\n</script>\n\n<template>\n  <Row justify=\"center\">\n    <Col v-bind=\"{ xs: 24, sm: 24, md: 20, lg: 16, xl: 16 }\">\n      <BasicForm />\n    </Col>\n  </Row>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/tool/gen/edit-steps/basic.tsx",
    "content": "import type { FormSchemaGetter } from '#/adapter/form';\n\nimport { getPopupContainer } from '@vben/utils';\n\nimport { z } from '#/adapter/form';\n\nexport const formSchema: FormSchemaGetter = () => [\n  {\n    component: 'Divider',\n    componentProps: {\n      orientation: 'left',\n    },\n    fieldName: 'divider1',\n    formItemClass: 'col-span-2',\n    label: '基本信息',\n  },\n  {\n    component: 'Input',\n    fieldName: 'tableName',\n    label: '表名称',\n    rules: 'required',\n  },\n  {\n    component: 'Input',\n    fieldName: 'tableComment',\n    label: '表描述',\n    rules: 'required',\n  },\n  {\n    component: 'Input',\n    fieldName: 'className',\n    label: '实体类名称',\n    rules: 'required',\n  },\n  {\n    component: 'Input',\n    fieldName: 'functionAuthor',\n    label: '作者',\n    rules: 'required',\n  },\n  {\n    component: 'Divider',\n    componentProps: {\n      orientation: 'left',\n    },\n    fieldName: 'divider2',\n    formItemClass: 'col-span-2',\n    label: '生成信息',\n  },\n  {\n    component: 'Select',\n    componentProps: {\n      allowClear: false,\n      getPopupContainer,\n      options: [\n        { label: '单表(增删改查)', value: 'crud' },\n        { label: '树表(增删改查)', value: 'tree' },\n      ],\n    },\n    defaultValue: 'crud',\n    fieldName: 'tplCategory',\n    label: '模板类型',\n    rules: 'selectRequired',\n  },\n  {\n    component: 'Select',\n    componentProps: {\n      getPopupContainer,\n    },\n    dependencies: {\n      show: (values) => values.tplCategory === 'tree',\n      triggerFields: ['tplCategory'],\n    },\n    fieldName: 'treeCode',\n    helpMessage: '树节点显示的编码字段名， 如: dept_id (相当于id)',\n    label: '树编码字段',\n    rules: 'selectRequired',\n  },\n  {\n    component: 'Select',\n    componentProps: {\n      allowClear: false,\n    },\n    dependencies: {\n      show: (values) => values.tplCategory === 'tree',\n      triggerFields: ['tplCategory'],\n    },\n    fieldName: 'treeParentCode',\n    help: '树节点显示的父编码字段名， 如: parent_Id (相当于parentId)',\n    label: '树父编码字段',\n    rules: 'selectRequired',\n  },\n  {\n    component: 'Select',\n    componentProps: {\n      allowClear: false,\n    },\n    dependencies: {\n      show: (values) => values.tplCategory === 'tree',\n      triggerFields: ['tplCategory'],\n    },\n    fieldName: 'treeName',\n    help: '树节点的显示名称字段名， 如: dept_name (相当于label)',\n    label: '树名称字段',\n    rules: 'selectRequired',\n  },\n  {\n    component: 'Input',\n    fieldName: 'packageName',\n    help: '生成在哪个java包下, 例如 com.ruoyi.system',\n    label: '生成包路径',\n    rules: 'required',\n  },\n  {\n    component: 'Input',\n    fieldName: 'moduleName',\n    help: '可理解为子系统名，例如 system',\n    label: '生成模块名',\n    rules: 'required',\n  },\n  {\n    component: 'Input',\n    fieldName: 'businessName',\n    help: '可理解为功能英文名，例如 user',\n    label: '生成业务名',\n    rules: 'required',\n  },\n  {\n    component: 'Input',\n    fieldName: 'functionName',\n    help: '用作类描述，例如 用户',\n    label: '生成功能名',\n    rules: 'required',\n  },\n  {\n    component: 'TreeSelect',\n    componentProps: {\n      allowClear: false,\n      getPopupContainer,\n    },\n    defaultValue: 0,\n    fieldName: 'parentMenuId',\n    label: '上级菜单',\n  },\n  {\n    component: 'RadioGroup',\n    componentProps: {\n      buttonStyle: 'solid',\n      options: [\n        { label: 'modal弹窗', value: 'modal' },\n        { label: 'drawer抽屉', value: 'drawer' },\n      ],\n      optionType: 'button',\n    },\n    help: '自定义功能, 需要后端支持',\n    defaultValue: 'modal',\n    fieldName: 'popupComponent',\n    label: '弹窗组件类型',\n  },\n  {\n    component: 'RadioGroup',\n    componentProps: {\n      buttonStyle: 'solid',\n      options: [\n        { label: 'useVbenForm', value: 'useForm' },\n        { label: 'antd原生表单', value: 'native' },\n      ],\n      optionType: 'button',\n    },\n    help: '自定义功能, 需要后端支持\\n复杂(布局, 联动等)表单建议用antd原生表单',\n    defaultValue: 'useForm',\n    fieldName: 'formComponent',\n    label: '生成表单类型',\n  },\n  {\n    component: 'RadioGroup',\n    componentProps: {\n      buttonStyle: 'solid',\n      options: [\n        { label: 'zip压缩包', value: '0' },\n        { label: '自定义路径', value: '1' },\n      ],\n      optionType: 'button',\n    },\n    defaultValue: '0',\n    fieldName: 'genType',\n    help: '默认为zip压缩包下载, 也可以自定义生成路径',\n    label: '生成代码方式',\n  },\n  {\n    component: 'Input',\n    defaultValue: '/',\n    dependencies: {\n      show: (model) => model.genType === '1',\n      triggerFields: ['genType'],\n    },\n    fieldName: 'genPath',\n    help: '输入绝对路径, 不支持\"./\"相对路径',\n    label: '代码生成路径',\n    rules: z\n      .string()\n      .regex(/^(?:[a-z]:)?(?:\\/|(?:\\\\|\\/)[^\\\\/:*?\"<>|\\r\\n]+)*(?:\\\\|\\/)?$/i, {\n        message: '请输入合法的路径',\n      }),\n  },\n  {\n    component: 'Textarea',\n    fieldName: 'remark',\n    formItemClass: 'col-span-2 items-baseline',\n    label: '备注',\n  },\n];\n"
  },
  {
    "path": "apps/web-antd/src/views/tool/gen/edit-steps/gen-config.vue",
    "content": "<script setup lang=\"ts\">\nimport type { Ref } from 'vue';\n\nimport type { VxeGridProps } from '#/adapter/vxe-table';\nimport type { GenInfo } from '#/api/tool/gen/model';\n\nimport { inject, onMounted, reactive } from 'vue';\n\nimport { useVbenVxeGrid } from '#/adapter/vxe-table';\nimport { dictOptionSelectList } from '#/api/system/dict/dict-type';\n\nimport { validRules, vxeTableColumns } from './gen-data';\n\n/**\n * 从父组件注入\n */\nconst genInfoData = inject('genInfoData') as Ref<GenInfo['info']>;\n\nconst dictOptions = reactive<{ label: string; value: string }[]>([\n  { label: '未设置', value: '' },\n]);\n\n/**\n * 加载字典下拉数据\n */\nonMounted(async () => {\n  const resp = await dictOptionSelectList();\n\n  const options = resp.map((dict) => ({\n    label: `${dict.dictName} | ${dict.dictType}`,\n    value: dict.dictType,\n  }));\n\n  dictOptions.push(...options);\n});\n\nconst gridOptions: VxeGridProps = {\n  columns: vxeTableColumns(dictOptions),\n  keepSource: true,\n  editConfig: { trigger: 'click', mode: 'cell', showStatus: true },\n  editRules: validRules,\n  rowConfig: {\n    keyField: 'id',\n    isCurrent: true, // 高亮当前行\n  },\n  columnConfig: {\n    resizable: true,\n  },\n  proxyConfig: {\n    enabled: true,\n  },\n  toolbarConfig: {\n    enabled: false,\n  },\n  height: 'auto',\n  pagerConfig: {\n    enabled: false,\n  },\n  data: genInfoData.value.columns,\n};\n\nconst [BasicTable, tableApi] = useVbenVxeGrid({ gridOptions });\n\n/**\n * 校验表格数据\n */\nasync function validateTable() {\n  const hasError = await tableApi.grid.validate();\n  return !hasError;\n}\n\n/**\n * 获取表格数据\n */\nfunction getTableRecords() {\n  return tableApi?.grid?.getData?.() ?? [];\n}\n\ndefineExpose({\n  validateTable,\n  getTableRecords,\n});\n</script>\n\n<template>\n  <div class=\"flex flex-col gap-[16px]\">\n    <div class=\"h-[calc(100vh-200px)] overflow-y-hidden\">\n      <BasicTable />\n    </div>\n  </div>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/tool/gen/edit-steps/gen-data.tsx",
    "content": "import type { Recordable } from '@vben/types';\n\nimport type { VxeGridProps } from '#/adapter/vxe-table';\n\nimport { Checkbox, Input, Select } from 'ant-design-vue';\n\nconst JavaTypes: string[] = [\n  'Long',\n  'String',\n  'Integer',\n  'Double',\n  'BigDecimal',\n  'Date',\n  'Boolean',\n  'LocalDate',\n  'LocalDateTime',\n];\n\nconst queryTypeOptions = [\n  { label: '=', value: 'EQ' },\n  { label: '!=', value: 'NE' },\n  { label: '>', value: 'GT' },\n  { label: '>=', value: 'GE' },\n  { label: '<', value: 'LT' },\n  { label: '<=', value: 'LE' },\n  { label: 'LIKE', value: 'LIKE' },\n  { label: 'BETWEEN', value: 'BETWEEN' },\n];\n\nconst componentsOptions = [\n  { label: '文本框', value: 'input' },\n  { label: '文本域', value: 'textarea' },\n  { label: '下拉框', value: 'select' },\n  { label: '单选框', value: 'radio' },\n  { label: '复选框', value: 'checkbox' },\n  { label: '日期控件', value: 'datetime' },\n  { label: '图片上传', value: 'imageUpload' },\n  { label: '文件上传', value: 'fileUpload' },\n  { label: '富文本', value: 'editor' },\n];\n\nfunction renderBooleanTag(row: Recordable<any>, field: string) {\n  const value = row[field] ? '是' : '否';\n  const className = row[field] ? 'text-green-500' : 'text-red-500';\n  return <span class={className}>{value}</span>;\n}\n\nfunction renderBooleanCheckbox(row: Recordable<any>, field: string) {\n  return <Checkbox v-model:checked={row[field]}></Checkbox>;\n}\n\nexport const validRules: VxeGridProps['editRules'] = {\n  columnComment: [{ required: true, message: '请输入' }],\n  javaField: [{ required: true, message: '请输入' }],\n};\n\n// 内部依赖的字典从外部通过函数传入\nexport const vxeTableColumns: (\n  dictOptions: { label: string; value: string }[],\n) => VxeGridProps['columns'] = (dictOptions) => [\n  {\n    title: '序号',\n    type: 'seq',\n    fixed: 'left',\n    width: '50',\n    align: 'center',\n  },\n  {\n    title: '字段列名',\n    field: 'columnName',\n    showOverflow: 'tooltip',\n    fixed: 'left',\n    minWidth: 150,\n  },\n  {\n    title: '字段描述',\n    field: 'columnComment',\n    minWidth: 150,\n    slots: {\n      edit: ({ row }) => {\n        return <Input v-model:value={row.columnComment}></Input>;\n      },\n    },\n    editRender: {},\n  },\n  {\n    title: 'db类型',\n    field: 'columnType',\n    minWidth: 120,\n    showOverflow: 'tooltip',\n  },\n  {\n    title: 'Java类型',\n    field: 'javaType',\n    minWidth: 150,\n    slots: {\n      edit: ({ row }) => {\n        const javaTypeOptions = JavaTypes.map((type) => ({\n          label: type,\n          value: type,\n        }));\n        return (\n          <Select\n            class=\"w-full\"\n            getPopupContainer={() => document.body}\n            options={javaTypeOptions}\n            v-model:value={row.javaType}\n          ></Select>\n        );\n      },\n    },\n    editRender: {},\n  },\n  {\n    title: 'Java属性名',\n    field: 'javaField',\n    minWidth: 150,\n    showOverflow: 'tooltip',\n    slots: {\n      edit: ({ row }) => {\n        return <Input v-model:value={row.javaField}></Input>;\n      },\n    },\n    editRender: {},\n  },\n  {\n    title: '插入',\n    field: 'insert',\n    minWidth: 80,\n    showOverflow: 'tooltip',\n    align: 'center',\n    slots: {\n      default: ({ row }) => {\n        return renderBooleanTag(row, 'insert');\n      },\n      edit: ({ row }) => {\n        return renderBooleanCheckbox(row, 'insert');\n      },\n    },\n    editRender: {},\n  },\n  {\n    title: '编辑',\n    field: 'edit',\n    showOverflow: 'tooltip',\n    align: 'center',\n    minWidth: 80,\n    slots: {\n      default: ({ row }) => {\n        return renderBooleanTag(row, 'edit');\n      },\n      edit: ({ row }) => {\n        return renderBooleanCheckbox(row, 'edit');\n      },\n    },\n    editRender: {},\n  },\n  {\n    title: '列表',\n    field: 'list',\n    showOverflow: 'tooltip',\n    align: 'center',\n    minWidth: 80,\n    slots: {\n      default: ({ row }) => {\n        return renderBooleanTag(row, 'list');\n      },\n      edit: ({ row }) => {\n        return renderBooleanCheckbox(row, 'list');\n      },\n    },\n    editRender: {},\n  },\n  {\n    title: '查询',\n    field: 'query',\n    showOverflow: 'tooltip',\n    align: 'center',\n    minWidth: 80,\n    slots: {\n      default: ({ row }) => {\n        return renderBooleanTag(row, 'query');\n      },\n      edit: ({ row }) => {\n        return renderBooleanCheckbox(row, 'query');\n      },\n    },\n    editRender: {},\n  },\n  {\n    title: '查询方式',\n    field: 'queryType',\n    showOverflow: 'tooltip',\n    align: 'center',\n    minWidth: 150,\n    slots: {\n      default: ({ row }) => {\n        const queryType = row.queryType;\n        const found = queryTypeOptions.find((item) => item.value === queryType);\n        if (found) {\n          return found.label;\n        }\n        return queryType;\n      },\n      edit: ({ row }) => {\n        return (\n          <Select\n            class=\"w-full\"\n            getPopupContainer={() => document.body}\n            options={queryTypeOptions}\n            v-model:value={row.queryType}\n          ></Select>\n        );\n      },\n    },\n    editRender: {},\n  },\n  {\n    title: '必填',\n    field: 'required',\n    showOverflow: 'tooltip',\n    align: 'center',\n    minWidth: 80,\n    slots: {\n      default: ({ row }) => {\n        return renderBooleanTag(row, 'required');\n      },\n      edit: ({ row }) => {\n        return renderBooleanCheckbox(row, 'required');\n      },\n    },\n    editRender: {},\n  },\n  {\n    title: '显示类型',\n    field: 'htmlType',\n    showOverflow: 'tooltip',\n    minWidth: 150,\n    align: 'center',\n    slots: {\n      default: ({ row }) => {\n        const htmlType = row.htmlType;\n        const found = componentsOptions.find((item) => item.value === htmlType);\n        if (found) {\n          return found.label;\n        }\n        return htmlType;\n      },\n      edit: ({ row }) => {\n        return (\n          <Select\n            class=\"w-full\"\n            getPopupContainer={() => document.body}\n            options={componentsOptions}\n            v-model:value={row.htmlType}\n          ></Select>\n        );\n      },\n    },\n    editRender: {},\n  },\n  {\n    title: '字典类型',\n    field: 'dictType',\n    showOverflow: 'tooltip',\n    minWidth: 230,\n    align: 'center',\n    titlePrefix: {\n      message: `仅'下拉框', '单选框', '复选框'支持字典类型`,\n    },\n    slots: {\n      default: ({ row }) => {\n        const dictType = row.dictType;\n        const found = dictOptions.find((item) => item.value === dictType);\n        if (found) {\n          return found.label;\n        }\n        return dictType;\n      },\n      edit: ({ row }) => {\n        // 清除的回调 需要设置为空字符串 否则不会提交\n        const onDeselect = () => {\n          row.dictType = '';\n        };\n        const disabled =\n          row.htmlType !== 'select' &&\n          row.htmlType !== 'radio' &&\n          row.htmlType !== 'checkbox';\n        return (\n          <Select\n            allowClear={true}\n            class=\"w-full\"\n            disabled={disabled}\n            getPopupContainer={() => document.body}\n            onDeselect={onDeselect}\n            options={dictOptions}\n            placeholder=\"请选择字典类型\"\n            v-model:value={row.dictType}\n          ></Select>\n        );\n      },\n    },\n    editRender: {},\n  },\n];\n"
  },
  {
    "path": "apps/web-antd/src/views/tool/gen/edit-steps/index.ts",
    "content": "export { default as BasicSetting } from './basic-setting.vue';\nexport { default as GenConfig } from './gen-config.vue';\n"
  },
  {
    "path": "apps/web-antd/src/views/tool/gen/editTable.vue",
    "content": "<!--\n后端版本>=5.4.0  这个从本地路由变为从后台返回\n-->\n<script setup lang=\"ts\">\nimport EditGenPage from './edit-gen.vue';\n</script>\n\n<template>\n  <EditGenPage />\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/tool/gen/index.vue",
    "content": "<script setup lang=\"ts\">\nimport type { VbenFormProps } from '@vben/common-ui';\nimport type { Recordable } from '@vben/types';\n\nimport type { VxeGridProps } from '#/adapter/vxe-table';\n\nimport { onMounted } from 'vue';\nimport { useRouter } from 'vue-router';\n\nimport { Page, useVbenModal } from '@vben/common-ui';\nimport { getVxePopupContainer } from '@vben/utils';\n\nimport { message, Modal, Popconfirm, Space } from 'ant-design-vue';\nimport dayjs from 'dayjs';\n\nimport { useVbenVxeGrid, vxeCheckboxChecked } from '#/adapter/vxe-table';\nimport {\n  batchGenCode,\n  generatedList,\n  genRemove,\n  genWithPath,\n  getDataSourceNames,\n  syncDb,\n} from '#/api/tool/gen';\nimport { downloadByData } from '#/utils/file/download';\n\nimport codePreviewModal from './code-preview-modal.vue';\nimport { columns, querySchema } from './data';\nimport tableImportModal from './table-import-modal.vue';\n\nconst formOptions: VbenFormProps = {\n  schema: querySchema(),\n  commonConfig: {\n    labelWidth: 80,\n    componentProps: {\n      allowClear: true,\n    },\n  },\n  wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4',\n  // 日期选择格式化\n  fieldMappingTime: [\n    [\n      'createTime',\n      ['params[beginTime]', 'params[endTime]'],\n      ['YYYY-MM-DD 00:00:00', 'YYYY-MM-DD 23:59:59'],\n    ],\n  ],\n};\n\nconst gridOptions: VxeGridProps = {\n  checkboxConfig: {\n    // 高亮\n    highlight: true,\n    // 翻页时保留选中状态\n    reserve: true,\n    // 点击行选中\n    trigger: 'row',\n  },\n  columns,\n  height: 'auto',\n  keepSource: true,\n  pagerConfig: {},\n  proxyConfig: {\n    ajax: {\n      query: async ({ page }, formValues = {}) => {\n        return await generatedList({\n          pageNum: page.currentPage,\n          pageSize: page.pageSize,\n          ...formValues,\n        });\n      },\n    },\n  },\n  rowConfig: {\n    keyField: 'tableId',\n  },\n  id: 'tool-gen-index',\n};\n\nconst [BasicTable, tableApi] = useVbenVxeGrid({\n  formOptions,\n  gridOptions,\n});\n\nonMounted(async () => {\n  // 获取数据源\n  const ret = await getDataSourceNames();\n  const dataSourceOptions = [{ label: '全部', value: '' }];\n  const transOptions = ret.map((item) => ({ label: item, value: item }));\n  dataSourceOptions.push(...transOptions);\n  // 更新selectOptions\n  tableApi.formApi.updateSchema([\n    {\n      fieldName: 'dataName',\n      componentProps: {\n        options: dataSourceOptions,\n      },\n    },\n  ]);\n});\n\nconst [CodePreviewModal, previewModalApi] = useVbenModal({\n  connectedComponent: codePreviewModal,\n});\n\nfunction handlePreview(record: Recordable<any>) {\n  previewModalApi.setData({ tableId: record.tableId });\n  previewModalApi.open();\n}\n\nconst router = useRouter();\nfunction handleEdit(record: Recordable<any>) {\n  router.push(`/tool/gen-edit/index/${record.tableId}`);\n}\n\nasync function handleSync(record: Recordable<any>) {\n  await syncDb(record.tableId);\n  await tableApi.query();\n}\n\n/**\n * 批量生成代码\n */\nasync function handleBatchGen() {\n  const rows = tableApi.grid.getCheckboxRecords();\n  const ids = rows.map((row: any) => row.tableId);\n  if (ids.length === 0) {\n    message.info('请选择需要生成代码的表');\n    return;\n  }\n  const hideLoading = message.loading('下载中...');\n  try {\n    const params = ids.join(',');\n    const data = await batchGenCode(params);\n    const timestamp = Date.now();\n    downloadByData(data, `批量代码生成_${timestamp}.zip`);\n  } finally {\n    hideLoading();\n  }\n}\n\nasync function handleDownload(record: Recordable<any>) {\n  const hideLoading = message.loading('加载中...');\n  try {\n    // 路径生成\n    if (record.genType === '1' && record.genPath) {\n      await genWithPath(record.tableId);\n      message.success(`生成成功: ${record.genPath}`);\n      return;\n    }\n    // zip生成\n    const blob = await batchGenCode(record.tableId);\n    const filename = `代码生成_${record.tableName}_${dayjs().valueOf()}.zip`;\n    downloadByData(blob, filename);\n  } catch (error) {\n    console.error(error);\n  } finally {\n    hideLoading();\n  }\n}\n\n/**\n * 删除\n * @param record\n */\nasync function handleDelete(record: Recordable<any>) {\n  await genRemove(record.tableId);\n  await tableApi.query();\n}\n\nfunction handleMultiDelete() {\n  const rows = tableApi.grid.getCheckboxRecords();\n  const ids = rows.map((row: any) => row.tableId);\n  Modal.confirm({\n    title: '提示',\n    okType: 'danger',\n    content: `确认删除选中的${ids.length}条记录吗？`,\n    onOk: async () => {\n      await genRemove(ids);\n      await tableApi.query();\n    },\n  });\n}\n\nconst [TableImportModal, tableImportModalApi] = useVbenModal({\n  connectedComponent: tableImportModal,\n});\n\nfunction handleImport() {\n  tableImportModalApi.open();\n}\n</script>\n\n<template>\n  <Page :auto-content-height=\"true\">\n    <BasicTable table-title=\"代码生成列表\">\n      <template #toolbar-tools>\n        <Space>\n          <a-button\n            :disabled=\"!vxeCheckboxChecked(tableApi)\"\n            danger\n            type=\"primary\"\n            v-access:code=\"['tool:gen:remove']\"\n            @click=\"handleMultiDelete\"\n          >\n            {{ $t('pages.common.delete') }}\n          </a-button>\n          <a-button\n            :disabled=\"!vxeCheckboxChecked(tableApi)\"\n            v-access:code=\"['tool:gen:code']\"\n            @click=\"handleBatchGen\"\n          >\n            {{ $t('pages.common.generate') }}\n          </a-button>\n          <a-button\n            type=\"primary\"\n            v-access:code=\"['tool:gen:import']\"\n            @click=\"handleImport\"\n          >\n            {{ $t('pages.common.import') }}\n          </a-button>\n        </Space>\n      </template>\n      <template #action=\"{ row }\">\n        <a-button\n          size=\"small\"\n          type=\"link\"\n          v-access:code=\"['tool:gen:preview']\"\n          @click.stop=\"handlePreview(row)\"\n        >\n          {{ $t('pages.common.preview') }}\n        </a-button>\n        <a-button\n          size=\"small\"\n          type=\"link\"\n          v-access:code=\"['tool:gen:edit']\"\n          @click.stop=\"handleEdit(row)\"\n        >\n          {{ $t('pages.common.edit') }}\n        </a-button>\n        <Popconfirm\n          :get-popup-container=\"getVxePopupContainer\"\n          :title=\"`确认同步[${row.tableName}]?`\"\n          placement=\"left\"\n          @confirm=\"handleSync(row)\"\n        >\n          <a-button\n            size=\"small\"\n            type=\"link\"\n            v-access:code=\"['tool:gen:edit']\"\n            @click.stop=\"\"\n          >\n            {{ $t('pages.common.sync') }}\n          </a-button>\n        </Popconfirm>\n        <a-button\n          size=\"small\"\n          type=\"link\"\n          v-access:code=\"['tool:gen:code']\"\n          @click.stop=\"handleDownload(row)\"\n        >\n          生成代码\n        </a-button>\n        <Popconfirm\n          :get-popup-container=\"getVxePopupContainer\"\n          :title=\"`确认删除[${row.tableName}]?`\"\n          placement=\"left\"\n          @confirm=\"handleDelete(row)\"\n        >\n          <a-button\n            danger\n            size=\"small\"\n            type=\"link\"\n            v-access:code=\"['tool:gen:remove']\"\n            @click.stop=\"\"\n          >\n            {{ $t('pages.common.delete') }}\n          </a-button>\n        </Popconfirm>\n      </template>\n    </BasicTable>\n    <CodePreviewModal />\n    <TableImportModal @reload=\"tableApi.query()\" />\n  </Page>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/tool/gen/table-import-modal.vue",
    "content": "<script setup lang=\"ts\">\nimport type { VbenFormProps } from '@vben/common-ui';\n\nimport type { VxeGridProps } from '#/adapter/vxe-table';\n\nimport { useVbenModal } from '@vben/common-ui';\n\nimport { useVbenVxeGrid } from '#/adapter/vxe-table';\nimport {\n  getDataSourceNames,\n  importTable,\n  readyToGenList,\n} from '#/api/tool/gen';\n\nconst emit = defineEmits<{ reload: [] }>();\n\nconst formOptions: VbenFormProps = {\n  schema: [\n    {\n      label: '数据源',\n      fieldName: 'dataName',\n      component: 'Select',\n      defaultValue: 'master',\n    },\n    {\n      label: '表名称',\n      fieldName: 'tableName',\n      component: 'Input',\n    },\n    {\n      label: '表描述',\n      fieldName: 'tableComment',\n      component: 'Input',\n    },\n  ],\n  commonConfig: {\n    labelWidth: 60,\n  },\n  showCollapseButton: false,\n  wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3',\n};\n\nconst gridOptions: VxeGridProps = {\n  checkboxConfig: {\n    highlight: true,\n    reserve: true,\n    trigger: 'row',\n  },\n  columns: [\n    {\n      type: 'checkbox',\n      width: 60,\n    },\n    {\n      title: '表名称',\n      field: 'tableName',\n      align: 'left',\n    },\n    {\n      title: '表描述',\n      field: 'tableComment',\n      align: 'left',\n    },\n    {\n      title: '创建时间',\n      field: 'createTime',\n    },\n    {\n      title: '更新时间',\n      field: 'updateTime',\n    },\n  ],\n  keepSource: true,\n  size: 'small',\n  minHeight: 400,\n  pagerConfig: {},\n  proxyConfig: {\n    ajax: {\n      query: async ({ page }, formValues = {}) => {\n        return await readyToGenList({\n          pageNum: page.currentPage,\n          pageSize: page.pageSize,\n          ...formValues,\n        });\n      },\n    },\n  },\n  rowConfig: {\n    keyField: 'tableName',\n  },\n  toolbarConfig: {\n    enabled: false,\n  },\n  id: 'import-table-modal',\n  cellClassName: 'cursor-pointer',\n};\n\nconst [BasicTable, tableApi] = useVbenVxeGrid({ formOptions, gridOptions });\n\nconst [BasicModal, modalApi] = useVbenModal({\n  onOpenChange: async (isOpen) => {\n    if (!isOpen) {\n      tableApi.grid.clearCheckboxRow();\n      return null;\n    }\n    const ret = await getDataSourceNames();\n    const dataSourceOptions = ret.map((item) => ({ label: item, value: item }));\n    tableApi.formApi.updateSchema([\n      {\n        fieldName: 'dataName',\n        componentProps: {\n          options: dataSourceOptions,\n        },\n      },\n    ]);\n  },\n  onConfirm: handleSubmit,\n});\n\nasync function handleSubmit() {\n  try {\n    const records = tableApi.grid.getCheckboxRecords();\n    const tables = records.map((item) => item.tableName);\n    if (tables.length === 0) {\n      modalApi.close();\n      return;\n    }\n    modalApi.modalLoading(true);\n    const { dataName } = await tableApi.formApi.getValues();\n    await importTable(tables.join(','), dataName);\n    emit('reload');\n    modalApi.close();\n  } catch (error) {\n    console.warn(error);\n  } finally {\n    modalApi.modalLoading(false);\n  }\n}\n</script>\n\n<template>\n  <BasicModal class=\"w-[800px]\" title=\"导入表\">\n    <BasicTable />\n  </BasicModal>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/workflow/category/category-modal.vue",
    "content": "<script setup lang=\"ts\">\nimport { computed, ref } from 'vue';\n\nimport { useVbenModal } from '@vben/common-ui';\nimport { $t } from '@vben/locales';\nimport {\n  addFullName,\n  cloneDeep,\n  getPopupContainer,\n  listToTree,\n} from '@vben/utils';\n\nimport { useVbenForm } from '#/adapter/form';\nimport {\n  categoryAdd,\n  categoryInfo,\n  categoryList,\n  categoryUpdate,\n} from '#/api/workflow/category';\nimport { defaultFormValueGetter, useBeforeCloseDiff } from '#/utils/popup';\n\nimport { modalSchema } from './data';\n\nconst emit = defineEmits<{ reload: [] }>();\n\nconst isUpdate = ref(false);\nconst title = computed(() => {\n  return isUpdate.value ? $t('pages.common.edit') : $t('pages.common.add');\n});\n\nconst [BasicForm, formApi] = useVbenForm({\n  commonConfig: {\n    // 默认占满两列\n    formItemClass: 'col-span-2',\n    // 默认label宽度 px\n    labelWidth: 80,\n    // 通用配置项 会影响到所有表单项\n    componentProps: {\n      class: 'w-full',\n    },\n  },\n  schema: modalSchema(),\n  showDefaultActions: false,\n  wrapperClass: 'grid-cols-2',\n});\n\nasync function setupCategorySelect() {\n  const listData = await categoryList();\n  const treeData = listToTree(listData, {\n    id: 'categoryId',\n    pid: 'parentId',\n  });\n  addFullName(treeData, 'categoryName', ' / ');\n  formApi.updateSchema([\n    {\n      fieldName: 'parentId',\n      componentProps: {\n        treeData,\n        treeLine: { showLeafIcon: false },\n        fieldNames: { label: 'categoryName', value: 'categoryId' },\n        treeDefaultExpandAll: true,\n        treeNodeLabelProp: 'fullName',\n        getPopupContainer,\n      },\n    },\n  ]);\n}\n\nconst { onBeforeClose, markInitialized, resetInitialized } = useBeforeCloseDiff(\n  {\n    initializedGetter: defaultFormValueGetter(formApi),\n    currentGetter: defaultFormValueGetter(formApi),\n  },\n);\n\nconst [BasicModal, modalApi] = useVbenModal({\n  fullscreenButton: false,\n  onBeforeClose,\n  onClosed: handleClosed,\n  onConfirm: handleConfirm,\n  onOpenChange: async (isOpen) => {\n    if (!isOpen) {\n      return null;\n    }\n    modalApi.modalLoading(true);\n\n    const { id, parentId } = modalApi.getData() as {\n      id?: number | string;\n      parentId?: number | string;\n    };\n    isUpdate.value = !!id;\n\n    if (isUpdate.value && id) {\n      const record = await categoryInfo(id);\n      await formApi.setValues(record);\n    }\n    if (parentId) {\n      await formApi.setValues({ parentId });\n    }\n    await setupCategorySelect();\n    await markInitialized();\n\n    modalApi.modalLoading(false);\n  },\n});\n\nasync function handleConfirm() {\n  try {\n    modalApi.lock(true);\n    const { valid } = await formApi.validate();\n    if (!valid) {\n      return;\n    }\n    // getValues获取为一个readonly的对象 需要修改必须先深拷贝一次\n    const data = cloneDeep(await formApi.getValues());\n    await (isUpdate.value ? categoryUpdate(data) : categoryAdd(data));\n    resetInitialized();\n    emit('reload');\n    modalApi.close();\n  } catch (error) {\n    console.error(error);\n  } finally {\n    modalApi.lock(false);\n  }\n}\n\nasync function handleClosed() {\n  await formApi.resetForm();\n  resetInitialized();\n}\n</script>\n\n<template>\n  <BasicModal :title=\"title\" class=\"min-h-[500px]\">\n    <BasicForm />\n  </BasicModal>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/workflow/category/data.ts",
    "content": "import type { FormSchemaGetter } from '#/adapter/form';\nimport type { VxeGridProps } from '#/adapter/vxe-table';\n\nexport const querySchema: FormSchemaGetter = () => [\n  {\n    fieldName: 'categoryName',\n    label: '分类名称',\n    component: 'Input',\n  },\n  {\n    fieldName: 'categoryCode',\n    label: '分类编码',\n    component: 'Input',\n  },\n];\n\nexport const columns: VxeGridProps['columns'] = [\n  {\n    field: 'categoryName',\n    title: '分类名称',\n    treeNode: true,\n  },\n  {\n    field: 'orderNum',\n    title: '排序',\n  },\n  {\n    field: 'createTime',\n    title: '创建时间',\n  },\n  {\n    field: 'action',\n    fixed: 'right',\n    slots: { default: 'action' },\n    title: '操作',\n    resizable: false,\n    width: 'auto',\n  },\n];\n\nexport const modalSchema: FormSchemaGetter = () => [\n  {\n    label: 'categoryId',\n    fieldName: 'categoryId',\n    component: 'Input',\n    dependencies: {\n      show: () => false,\n      triggerFields: [''],\n    },\n  },\n  {\n    fieldName: 'parentId',\n    label: '父级分类',\n    rules: 'required',\n    defaultValue: 100,\n    component: 'TreeSelect',\n  },\n  {\n    fieldName: 'categoryName',\n    label: '分类名称',\n    component: 'Input',\n    rules: 'required',\n  },\n  {\n    fieldName: 'orderNum',\n    label: '排序',\n    component: 'InputNumber',\n  },\n];\n"
  },
  {
    "path": "apps/web-antd/src/views/workflow/category/index.vue",
    "content": "<script setup lang=\"ts\">\nimport type { Recordable } from '@vben/types';\n\nimport { nextTick } from 'vue';\n\nimport { Page, useVbenModal, type VbenFormProps } from '@vben/common-ui';\nimport { getVxePopupContainer } from '@vben/utils';\n\nimport { Popconfirm, Space } from 'ant-design-vue';\n\nimport { useVbenVxeGrid, type VxeGridProps } from '#/adapter/vxe-table';\nimport { categoryList, categoryRemove } from '#/api/workflow/category';\n\nimport categoryModal from './category-modal.vue';\nimport { columns, querySchema } from './data';\n\nconst formOptions: VbenFormProps = {\n  commonConfig: {\n    labelWidth: 80,\n    componentProps: {\n      allowClear: true,\n    },\n  },\n  schema: querySchema(),\n  wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4',\n};\n\nconst gridOptions: VxeGridProps = {\n  columns,\n  height: 'auto',\n  keepSource: true,\n  pagerConfig: {\n    enabled: false,\n  },\n  proxyConfig: {\n    ajax: {\n      query: async (_, formValues = {}) => {\n        const resp = await categoryList({\n          ...formValues,\n        });\n        return { rows: resp };\n      },\n      // 默认请求接口后展开全部 不需要可以删除这段\n      querySuccess: () => {\n        nextTick(() => {\n          expandAll();\n        });\n      },\n    },\n  },\n  /**\n   * 虚拟滚动  默认关闭\n   */\n  scrollY: {\n    enabled: false,\n    gt: 0,\n  },\n  rowConfig: {\n    keyField: 'categoryId',\n  },\n  treeConfig: {\n    parentField: 'parentId',\n    rowField: 'categoryId',\n    transform: true,\n  },\n  // 表格全局唯一表示 保存列配置需要用到\n  id: 'workflow-category-index',\n};\n\nconst [BasicTable, tableApi] = useVbenVxeGrid({ formOptions, gridOptions });\nconst [CategoryModal, modalApi] = useVbenModal({\n  connectedComponent: categoryModal,\n});\n\nfunction handleAdd(row?: Recordable<any>) {\n  modalApi.setData({ parentId: row?.categoryId });\n  modalApi.open();\n}\n\nasync function handleEdit(row: Recordable<any>) {\n  modalApi.setData({ id: row.categoryId });\n  modalApi.open();\n}\n\nasync function handleDelete(row: Recordable<any>) {\n  await categoryRemove(row.categoryId);\n  await tableApi.query();\n}\n\nfunction expandAll() {\n  tableApi.grid?.setAllTreeExpand(true);\n}\n\nfunction collapseAll() {\n  tableApi.grid?.setAllTreeExpand(false);\n}\n</script>\n\n<template>\n  <Page :auto-content-height=\"true\">\n    <BasicTable table-title=\"流程分类列表\">\n      <template #toolbar-tools>\n        <Space>\n          <a-button @click=\"collapseAll\">\n            {{ $t('pages.common.collapse') }}\n          </a-button>\n          <a-button @click=\"expandAll\">\n            {{ $t('pages.common.expand') }}\n          </a-button>\n          <a-button\n            type=\"primary\"\n            v-access:code=\"['workflow:category:add']\"\n            @click=\"handleAdd\"\n          >\n            {{ $t('pages.common.add') }}\n          </a-button>\n        </Space>\n      </template>\n      <template #action=\"{ row }\">\n        <Space>\n          <ghost-button\n            v-access:code=\"['workflow:category:edit']\"\n            @click.stop=\"handleEdit(row)\"\n          >\n            {{ $t('pages.common.edit') }}\n          </ghost-button>\n          <ghost-button\n            class=\"btn-success\"\n            v-access:code=\"['workflow:category:edit']\"\n            @click.stop=\"handleAdd(row)\"\n          >\n            {{ $t('pages.common.add') }}\n          </ghost-button>\n          <Popconfirm\n            :get-popup-container=\"getVxePopupContainer\"\n            placement=\"left\"\n            title=\"确认删除？\"\n            @confirm=\"handleDelete(row)\"\n          >\n            <ghost-button\n              danger\n              v-access:code=\"['workflow:category:remove']\"\n              @click.stop=\"\"\n            >\n              {{ $t('pages.common.delete') }}\n            </ghost-button>\n          </Popconfirm>\n        </Space>\n      </template>\n    </BasicTable>\n    <CategoryModal @reload=\"tableApi.query()\" />\n  </Page>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/workflow/components/actions/flow-actions.vue",
    "content": "<script setup lang=\"ts\">\nimport type { ApprovalType } from '../type';\n\nimport type { User } from '#/api/core/user';\nimport type { TaskInfo } from '#/api/workflow/task/model';\n\nimport { computed, h } from 'vue';\nimport { useRouter } from 'vue-router';\n\nimport { useVbenModal } from '@vben/common-ui';\nimport { cn, getPopupContainer } from '@vben/utils';\n\nimport {\n  ArrowLeftOutlined,\n  CheckOutlined,\n  EditOutlined,\n  ExclamationCircleOutlined,\n  MenuOutlined,\n  RollbackOutlined,\n  UsergroupAddOutlined,\n  UsergroupDeleteOutlined,\n  UserOutlined,\n} from '@ant-design/icons-vue';\nimport { Dropdown, Menu, MenuItem, Modal, Space } from 'ant-design-vue';\n\nimport {\n  cancelProcessApply,\n  deleteByInstanceIds,\n} from '#/api/workflow/instance';\nimport {\n  taskOperation,\n  terminationTask,\n  updateAssignee,\n} from '#/api/workflow/task';\n\nimport { approvalModal, approvalRejectionModal, flowInterfereModal } from '..';\nimport { approveWithReasonModal } from '../helper';\nimport userSelectModal from '../user-select-modal.vue';\n\ninterface Props {\n  /**\n   * 行数据的taskInfo?\n   */\n  task?: TaskInfo;\n  /**\n   * 审批类型 根据不同类型显示按钮\n   */\n  type: ApprovalType;\n  /**\n   * 为审批类型时候 显示的按钮(按钮权限)\n   */\n  buttonPermissions: Record<string, boolean>;\n}\nconst props = defineProps<Props>();\n\nconst emit = defineEmits<{\n  reload: [];\n}>();\n\n// 是否显示 `其他` 按钮\nconst showButtonOther = computed(() => {\n  const moreCollections = new Set(['addSign', 'subSign', 'transfer', 'trust']);\n  return Object.keys(props.buttonPermissions).some(\n    (key) => moreCollections.has(key) && props.buttonPermissions[key],\n  );\n});\n\n// 进行中 可以撤销\nconst revocable = computed(() => props.task?.flowStatus === 'waiting');\nasync function handleCancel() {\n  Modal.confirm({\n    title: '提示',\n    content: '确定要撤销该申请吗？',\n    centered: true,\n    okButtonProps: { danger: true },\n    onOk: async () => {\n      await cancelProcessApply({\n        businessId: props.task!.businessId,\n        message: '申请人撤销流程！',\n      });\n      emit('reload');\n    },\n  });\n}\n\n/**\n * 是否可编辑/删除\n */\nconst editableAndRemoveable = computed(() => {\n  if (!props.task) {\n    return false;\n  }\n  return ['back', 'cancel', 'draft'].includes(props.task.flowStatus);\n});\n\nconst router = useRouter();\nfunction handleEdit() {\n  const path = props.task?.formPath;\n  if (path) {\n    router.push({ path, query: { id: props.task!.businessId } });\n  }\n}\n\nfunction handleRemove() {\n  Modal.confirm({\n    title: '提示',\n    content: '确定删除该申请吗？',\n    centered: true,\n    okButtonProps: { danger: true },\n    onOk: async () => {\n      await deleteByInstanceIds([props.task!.id]);\n      emit('reload');\n    },\n  });\n}\n\n/**\n * 审批驳回\n */\nconst [RejectionModal, rejectionModalApi] = useVbenModal({\n  connectedComponent: approvalRejectionModal,\n});\nfunction handleRejection() {\n  rejectionModalApi.setData({\n    taskId: props.task?.id,\n    definitionId: props.task?.definitionId,\n    nodeCode: props.task?.nodeCode,\n  });\n  rejectionModalApi.open();\n}\n\n/**\n * 审批终止\n */\nfunction handleTermination() {\n  approveWithReasonModal({\n    title: '审批终止',\n    description: '确定终止当前审批流程吗？',\n    onOk: async (reason) => {\n      await terminationTask({ taskId: props.task!.id, comment: reason });\n      emit('reload');\n    },\n  });\n}\n\n/**\n * 审批通过\n */\nconst [ApprovalModal, approvalModalApi] = useVbenModal({\n  connectedComponent: approvalModal,\n});\nfunction handleApproval() {\n  const { buttonPermissions } = props;\n  // 是否具有抄送权限\n  const copyPermission = buttonPermissions?.copy ?? false;\n  // 是否具有选人权限\n  const assignPermission = buttonPermissions?.pop ?? false;\n  approvalModalApi.setData({\n    taskId: props.task?.id,\n    copyPermission,\n    assignPermission,\n  });\n  approvalModalApi.open();\n}\n\n/**\n * 委托\n */\nconst [DelegationModal, delegationModalApi] = useVbenModal({\n  connectedComponent: userSelectModal,\n});\nfunction handleDelegation(userList: User[]) {\n  if (userList.length === 0) return;\n  const current = userList[0];\n  approveWithReasonModal({\n    title: '委托',\n    description: `确定委托给[${current?.nickName}]吗?`,\n    onOk: async (reason) => {\n      await taskOperation(\n        { taskId: props.task!.id, userId: current!.userId, message: reason },\n        'delegateTask',\n      );\n      emit('reload');\n    },\n  });\n}\n\n/**\n * 转办\n */\nconst [TransferModal, transferModalApi] = useVbenModal({\n  connectedComponent: userSelectModal,\n});\nfunction handleTransfer(userList: User[]) {\n  if (userList.length === 0) return;\n  const current = userList[0];\n  approveWithReasonModal({\n    title: '转办',\n    description: `确定转办给[${current?.nickName}]吗?`,\n    onOk: async (reason) => {\n      await taskOperation(\n        { taskId: props.task!.id, userId: current!.userId, message: reason },\n        'transferTask',\n      );\n      emit('reload');\n    },\n  });\n}\n\nconst [AddSignatureModal, addSignatureModalApi] = useVbenModal({\n  connectedComponent: userSelectModal,\n});\nfunction handleAddSignature(userList: User[]) {\n  if (userList.length === 0) return;\n  const userIds = userList.map((user) => user.userId);\n  Modal.confirm({\n    title: '提示',\n    content: '确认加签吗?',\n    centered: true,\n    onOk: async () => {\n      await taskOperation({ taskId: props.task!.id, userIds }, 'addSignature');\n      emit('reload');\n    },\n  });\n}\n\nconst [ReductionSignatureModal, reductionSignatureModalApi] = useVbenModal({\n  connectedComponent: userSelectModal,\n});\nfunction handleReductionSignature(userList: User[]) {\n  if (userList.length === 0) return;\n  const userIds = userList.map((user) => user.userId);\n  Modal.confirm({\n    title: '提示',\n    content: '确认减签吗?',\n    centered: true,\n    onOk: async () => {\n      await taskOperation(\n        { taskId: props.task!.id, userIds },\n        'reductionSignature',\n      );\n      emit('reload');\n    },\n  });\n}\n\n// 流程干预\nconst [FlowInterfereModal, flowInterfereModalApi] = useVbenModal({\n  connectedComponent: flowInterfereModal,\n});\nfunction handleFlowInterfere() {\n  flowInterfereModalApi.setData({ taskId: props.task?.id });\n  flowInterfereModalApi.open();\n}\n\n// 修改办理人\nconst [UpdateAssigneeModal, updateAssigneeModalApi] = useVbenModal({\n  connectedComponent: userSelectModal,\n});\nfunction handleUpdateAssignee(userList: User[]) {\n  if (userList.length === 0) return;\n  const current = userList[0];\n  if (!current) return;\n  Modal.confirm({\n    title: '修改办理人',\n    content: `确定修改办理人为${current?.nickName}吗?`,\n    centered: true,\n    onOk: async () => {\n      await updateAssignee([props.task!.id], current.userId);\n      emit('reload');\n    },\n  });\n}\n\n/**\n * 是否显示 加签/减签操作\n */\nconst showMultiActions = computed(() => {\n  if (!props.task) {\n    return false;\n  }\n  if (Number(props.task.nodeRatio) > 0) {\n    return true;\n  }\n  return false;\n});\n</script>\n\n<template>\n  <div\n    :class=\"\n      cn(\n        'absolute bottom-0 left-0',\n        'border-t-solid border-t-[1px]',\n        'bg-background w-full p-3',\n      )\n    \"\n  >\n    <div class=\"flex justify-end\">\n      <Space v-if=\"type === 'myself'\">\n        <a-button\n          v-if=\"revocable\"\n          danger\n          ghost\n          type=\"primary\"\n          :icon=\"h(RollbackOutlined)\"\n          @click=\"handleCancel\"\n        >\n          撤销申请\n        </a-button>\n        <a-button\n          type=\"primary\"\n          ghost\n          v-if=\"editableAndRemoveable\"\n          :icon=\"h(EditOutlined)\"\n          @click=\"handleEdit\"\n        >\n          重新编辑\n        </a-button>\n        <a-button\n          v-if=\"editableAndRemoveable\"\n          danger\n          ghost\n          type=\"primary\"\n          :icon=\"h(EditOutlined)\"\n          @click=\"handleRemove\"\n        >\n          删除\n        </a-button>\n      </Space>\n      <Space v-if=\"type === 'approve'\">\n        <a-button\n          type=\"primary\"\n          ghost\n          :icon=\"h(CheckOutlined)\"\n          @click=\"handleApproval\"\n        >\n          通过\n        </a-button>\n        <a-button\n          v-if=\"buttonPermissions?.termination\"\n          danger\n          ghost\n          type=\"primary\"\n          :icon=\"h(ExclamationCircleOutlined)\"\n          @click=\"handleTermination\"\n        >\n          终止\n        </a-button>\n        <a-button\n          v-if=\"buttonPermissions?.back\"\n          danger\n          ghost\n          type=\"primary\"\n          :icon=\"h(ArrowLeftOutlined)\"\n          @click=\"handleRejection\"\n        >\n          驳回\n        </a-button>\n        <Dropdown\n          :get-popup-container=\"getPopupContainer\"\n          placement=\"bottomRight\"\n        >\n          <template #overlay>\n            <Menu>\n              <MenuItem\n                v-if=\"buttonPermissions?.trust\"\n                key=\"1\"\n                @click=\"() => delegationModalApi.open()\"\n              >\n                <UserOutlined class=\"mr-2\" />委托\n              </MenuItem>\n              <MenuItem\n                v-if=\"buttonPermissions?.transfer\"\n                key=\"2\"\n                @click=\"() => transferModalApi.open()\"\n              >\n                <RollbackOutlined class=\"mr-2\" /> 转办\n              </MenuItem>\n              <MenuItem\n                v-if=\"showMultiActions && buttonPermissions?.addSign\"\n                key=\"3\"\n                @click=\"() => addSignatureModalApi.open()\"\n              >\n                <UsergroupAddOutlined class=\"mr-2\" /> 加签\n              </MenuItem>\n              <MenuItem\n                v-if=\"showMultiActions && buttonPermissions?.subSign\"\n                key=\"4\"\n                @click=\"() => reductionSignatureModalApi.open()\"\n              >\n                <UsergroupDeleteOutlined class=\"mr-2\" /> 减签\n              </MenuItem>\n            </Menu>\n          </template>\n          <a-button v-if=\"showButtonOther\" :icon=\"h(MenuOutlined)\">\n            其他\n          </a-button>\n        </Dropdown>\n        <ApprovalModal @complete=\"$emit('reload')\" />\n        <RejectionModal @complete=\"$emit('reload')\" />\n        <DelegationModal mode=\"single\" @finish=\"handleDelegation\" />\n        <TransferModal mode=\"single\" @finish=\"handleTransfer\" />\n        <AddSignatureModal mode=\"multiple\" @finish=\"handleAddSignature\" />\n        <ReductionSignatureModal\n          mode=\"multiple\"\n          @finish=\"handleReductionSignature\"\n        />\n      </Space>\n      <Space v-if=\"type === 'admin'\">\n        <a-button @click=\"handleFlowInterfere\"> 流程干预 </a-button>\n        <a-button @click=\"() => updateAssigneeModalApi.open()\">\n          修改办理人\n        </a-button>\n        <FlowInterfereModal @complete=\"$emit('reload')\" />\n        <UpdateAssigneeModal mode=\"single\" @finish=\"handleUpdateAssignee\" />\n      </Space>\n    </div>\n  </div>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/workflow/components/actions/index.ts",
    "content": "export { default as FlowActions } from './flow-actions.vue';\n"
  },
  {
    "path": "apps/web-antd/src/views/workflow/components/apply-modal.vue",
    "content": "<!-- 流程发起(启动)的弹窗 -->\n\n<script setup lang=\"ts\">\nimport type { CompleteTaskReqData } from '#/api/workflow/task/model';\n\nimport { useVbenModal } from '@vben/common-ui';\n\nimport { cloneDeep } from 'lodash-es';\n\nimport { useVbenForm } from '#/adapter/form';\nimport { completeTask, getTaskByTaskId } from '#/api/workflow/task';\n\nimport { CopyComponent } from '.';\n\ninterface Emits {\n  /**\n   * 完成\n   */\n  complete: [];\n  /**\n   * 取消 此时已经变成草稿状态了\n   */\n  cancel: [];\n}\n\nconst emit = defineEmits<Emits>();\n\ninterface ModalProps {\n  taskId: string;\n  taskVariables: Record<string, any>;\n  variables?: any; // 这个干啥的\n}\n\nconst [BasicModal, modalApi] = useVbenModal({\n  title: '流程发起',\n  fullscreenButton: false,\n  onConfirm: handleSubmit,\n  onCancel: () => {\n    emit('cancel');\n    modalApi.close();\n  },\n  async onOpenChange(isOpen) {\n    if (!isOpen) {\n      return null;\n    }\n    modalApi.modalLoading(true);\n\n    const { taskId } = modalApi.getData() as ModalProps;\n\n    // 查询是否有按钮权限\n    const resp = await getTaskByTaskId(taskId);\n    const buttonPermissions: Record<string, boolean> = {};\n    resp.buttonList.forEach((item) => {\n      buttonPermissions[item.code] = item.show;\n    });\n\n    // 是否具有抄送权限\n    const copyPermission = buttonPermissions?.copy ?? false;\n    formApi.updateSchema([\n      {\n        fieldName: 'flowCopyList',\n        dependencies: {\n          if: copyPermission,\n          triggerFields: [''],\n        },\n      },\n    ]);\n\n    modalApi.modalLoading(false);\n  },\n});\n\nconst [BasicForm, formApi] = useVbenForm({\n  commonConfig: {\n    // 默认占满两列\n    formItemClass: 'col-span-2',\n    // 默认label宽度 px\n    labelWidth: 100,\n    // 通用配置项 会影响到所有表单项\n    componentProps: {\n      class: 'w-full',\n    },\n  },\n  schema: [\n    {\n      fieldName: 'messageType',\n      component: 'CheckboxGroup',\n      componentProps: {\n        options: [\n          { label: '站内信', value: '1', disabled: true },\n          { label: '邮件', value: '2' },\n          { label: '短信', value: '3' },\n        ],\n      },\n      label: '通知方式',\n      defaultValue: ['1'],\n    },\n    {\n      fieldName: 'attachment',\n      component: 'FileUpload',\n      componentProps: {\n        maxCount: 10,\n        maxSize: 20,\n        accept: 'png, jpg, jpeg, doc, docx, xlsx, xls, ppt, pdf',\n      },\n      defaultValue: [],\n      label: '附件上传',\n      formItemClass: 'items-start',\n    },\n    {\n      fieldName: 'flowCopyList',\n      component: 'Input',\n      defaultValue: [],\n      label: '抄送人',\n      // 默认不显示\n      dependencies: {\n        if: false,\n        triggerFields: [''],\n      },\n    },\n  ],\n  showDefaultActions: false,\n  wrapperClass: 'grid-cols-2',\n});\n\nasync function handleSubmit() {\n  try {\n    modalApi.modalLoading(true);\n    const { messageType, flowCopyList, attachment } = cloneDeep(\n      await formApi.getValues(),\n    );\n    const { taskId, taskVariables, variables } =\n      modalApi.getData() as ModalProps;\n    // 需要转换数据 抄送人员\n    const flowCCList = (flowCopyList as Array<any>).map((item) => ({\n      userId: item.userId,\n      userName: item.nickName,\n    }));\n    const requestData = {\n      fileId: attachment.join(','),\n      messageType,\n      flowCopyList: flowCCList,\n      taskId,\n      taskVariables,\n      variables,\n    } as CompleteTaskReqData;\n    await completeTask(requestData);\n    modalApi.close();\n    emit('complete');\n  } catch (error) {\n    console.error(error);\n  } finally {\n    modalApi.modalLoading(false);\n  }\n}\n</script>\n\n<template>\n  <BasicModal>\n    <BasicForm>\n      <template #flowCopyList=\"slotProps\">\n        <CopyComponent v-model:user-list=\"slotProps.modelValue\" />\n      </template>\n    </BasicForm>\n  </BasicModal>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/workflow/components/approval-card.vue",
    "content": "<script setup lang=\"ts\">\nimport type { TaskInfo } from '#/api/workflow/task/model';\n\nimport { computed } from 'vue';\n\nimport { VbenAvatar } from '@vben/common-ui';\nimport { DictEnum } from '@vben/constants';\n\nimport { Descriptions, DescriptionsItem, Tooltip } from 'ant-design-vue';\n\nimport { renderDict } from '#/utils/render';\n\nimport { getDiffTimeString } from './helper';\n\ninterface Props extends TaskInfo {\n  active: boolean;\n}\n\nconst props = withDefaults(defineProps<{ info: Props; rowKey?: string }>(), {\n  rowKey: 'id',\n});\n\nconst emit = defineEmits<{ click: [string] }>();\n\n/**\n * TODO: 这里要优化 事件没有用到\n */\nfunction handleClick() {\n  const idKey = props.rowKey as keyof TaskInfo;\n  emit('click', props.info[idKey]);\n}\n\nconst diffUpdateTimeString = computed(() => {\n  return getDiffTimeString(props.info.updateTime);\n});\n</script>\n\n<template>\n  <div\n    :class=\"{\n      'border-primary': info.active,\n    }\"\n    class=\"cursor-pointer rounded-lg border-[1px] border-solid p-3 transition-shadow duration-300 ease-in-out hover:shadow-lg\"\n    @click.stop=\"handleClick\"\n  >\n    <Descriptions\n      :column=\"1\"\n      :title=\"info.businessTitle ?? info.flowName\"\n      size=\"middle\"\n    >\n      <template #extra>\n        <component\n          :is=\"renderDict(info.flowStatus, DictEnum.WF_BUSINESS_STATUS)\"\n        />\n      </template>\n      <DescriptionsItem label=\"当前任务\">\n        <div class=\"font-bold\">{{ info.nodeName }}</div>\n      </DescriptionsItem>\n      <DescriptionsItem label=\"提交时间\">\n        {{ info.createTime }}\n      </DescriptionsItem>\n      <!-- <DescriptionsItem label=\"更新时间\">\n        {{ info.updateTime }}\n      </DescriptionsItem> -->\n    </Descriptions>\n    <div class=\"flex w-full items-center justify-between text-[14px]\">\n      <div class=\"flex items-center gap-1 overflow-hidden whitespace-nowrap\">\n        <VbenAvatar\n          :alt=\"info?.createByName\"\n          class=\"bg-primary size-[24px] rounded-full text-[10px] text-white\"\n          src=\"\"\n        />\n        <span class=\"overflow-hidden text-ellipsis opacity-50\">\n          {{ info.createByName }}\n        </span>\n      </div>\n      <div class=\"text-nowrap opacity-50\">\n        <Tooltip placement=\"top\" :title=\"`更新时间: ${info.updateTime}`\">\n          <div class=\"flex items-center gap-1\">\n            <span class=\"icon-[mdi--clock-outline] size-[16px]\"></span>\n            <span>{{ diffUpdateTimeString }}前更新</span>\n          </div>\n        </Tooltip>\n      </div>\n    </div>\n  </div>\n</template>\n\n<style lang=\"scss\" scoped>\n:deep(.ant-descriptions .ant-descriptions-header) {\n  margin-bottom: 12px !important;\n}\n\n:deep(.ant-descriptions-item) {\n  padding-bottom: 8px !important;\n}\n</style>\n"
  },
  {
    "path": "apps/web-antd/src/views/workflow/components/approval-content.vue",
    "content": "<!-- 审批终止 Modal弹窗的content属性专用 用于填写审批意见 -->\n<script setup lang=\"ts\">\nimport { Textarea } from 'ant-design-vue';\n\ndefineOptions({\n  name: 'ApprovalContent',\n  inheritAttrs: false,\n});\n\ndefineProps<{ description: string; value: string }>();\n\ndefineEmits<{ 'update:value': [string] }>();\n</script>\n\n<template>\n  <div class=\"flex flex-col gap-2\">\n    <div>{{ description }}</div>\n    <Textarea\n      :allow-clear=\"true\"\n      :auto-size=\"true\"\n      :value=\"value\"\n      placeholder=\"审批意见(可选)\"\n      @change=\"(e) => $emit('update:value', e.target.value!)\"\n    />\n  </div>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/workflow/components/approval-details.vue",
    "content": "<!--\n审批详情\n动态渲染要显示的内容 需要再flowDescripionsMap先定义好组件\n-->\n<script setup lang=\"ts\">\nimport type { FlowComponentsMapMapKey } from '../register';\n\nimport type { FlowInfoResponse } from '#/api/workflow/instance/model';\nimport type { TaskInfo } from '#/api/workflow/task/model';\n\nimport { Divider } from 'ant-design-vue';\n\nimport { ApprovalTimeline } from '.';\nimport { flowComponentsMap } from '../register';\n\ndefineOptions({\n  name: 'ApprovalDetails',\n  inheritAttrs: false,\n});\n\ndefineProps<{\n  currentFlowInfo: FlowInfoResponse;\n  task: TaskInfo;\n}>();\n</script>\n\n<template>\n  <div>\n    <!--\n     动态渲染要显示的内容 需要再flowDescripionsMap先定义好组件\n     business-id为业务ID 必传\n    -->\n    <component\n      :is=\"flowComponentsMap[task.formPath as FlowComponentsMapMapKey]\"\n      :business-id=\"task.businessId\"\n    />\n    <Divider />\n    <ApprovalTimeline :list=\"currentFlowInfo.list\" />\n  </div>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/workflow/components/approval-modal.vue",
    "content": "<!-- 审批同意的弹窗 -->\n<script setup lang=\"ts\">\nimport type { User } from '#/api/system/user/model';\nimport type {\n  CompleteTaskReqData,\n  NextNodeInfo,\n} from '#/api/workflow/task/model';\n\nimport { ref } from 'vue';\n\nimport { useVbenModal } from '@vben/common-ui';\nimport { cloneDeep } from '@vben/utils';\n\nimport { message } from 'ant-design-vue';\nimport { omit } from 'lodash-es';\n\nimport { useVbenForm } from '#/adapter/form';\nimport { completeTask, getNextNodeList } from '#/api/workflow/task';\n\nimport { CopyComponent } from '.';\n\nconst emit = defineEmits<{ complete: [] }>();\n\nconst [BasicForm, formApi] = useVbenForm({\n  commonConfig: {\n    // 默认占满两列\n    formItemClass: 'col-span-2',\n    // 默认label宽度 px\n    labelWidth: 100,\n    // 通用配置项 会影响到所有表单项\n    componentProps: {\n      class: 'w-full',\n    },\n  },\n  schema: [\n    {\n      fieldName: 'taskId',\n      component: 'Input',\n      label: '任务ID',\n      dependencies: {\n        show: false,\n        triggerFields: [''],\n      },\n    },\n    {\n      fieldName: 'messageType',\n      component: 'CheckboxGroup',\n      componentProps: {\n        options: [\n          { label: '站内信', value: '1', disabled: true },\n          { label: '邮件', value: '2' },\n          { label: '短信', value: '3' },\n        ],\n      },\n      label: '通知方式',\n      defaultValue: ['1'],\n    },\n    {\n      fieldName: 'attachment',\n      component: 'FileUpload',\n      componentProps: {\n        maxCount: 10,\n        maxSize: 20,\n        accept: 'png, jpg, jpeg, doc, docx, xlsx, xls, ppt, pdf',\n      },\n      defaultValue: [],\n      label: '附件上传',\n      formItemClass: 'items-start',\n    },\n    {\n      fieldName: 'flowCopyList',\n      component: 'Input',\n      defaultValue: [],\n      label: '抄送人',\n    },\n    {\n      fieldName: 'assigneeMap',\n      component: 'Input',\n      label: '下一步审批人',\n    },\n    {\n      fieldName: 'message',\n      component: 'Textarea',\n      label: '审批意见',\n      formItemClass: 'items-start',\n    },\n  ],\n  showDefaultActions: false,\n  wrapperClass: 'grid-cols-2',\n});\n\ninterface ModalProps {\n  taskId: string;\n  // 是否具有抄送权限\n  copyPermission: boolean;\n  // 是有具有选人权限\n  assignPermission: boolean;\n}\n\n// 自定义添加选人属性 给组件v-for绑定\nconst nextNodeInfo = ref<(NextNodeInfo & { selectUserList: User[] })[]>([]);\nconst [BasicModal, modalApi] = useVbenModal({\n  title: '审批通过',\n  fullscreenButton: false,\n  class: 'min-h-[365px]',\n  onConfirm: handleSubmit,\n  async onOpenChange(isOpen) {\n    if (!isOpen) {\n      await formApi.resetForm();\n      return null;\n    }\n    modalApi.modalLoading(true);\n\n    const { taskId, copyPermission, assignPermission } =\n      modalApi.getData() as ModalProps;\n    // 是否显示抄送选择\n    formApi.updateSchema([\n      {\n        fieldName: 'flowCopyList',\n        dependencies: {\n          if: copyPermission,\n          triggerFields: [''],\n        },\n      },\n      {\n        fieldName: 'assigneeMap',\n        dependencies: {\n          if: assignPermission,\n          triggerFields: [''],\n        },\n      },\n    ]);\n\n    // 获取下一节点名称\n    if (assignPermission) {\n      const resp = await getNextNodeList({ taskId });\n      nextNodeInfo.value = resp.map((item) => ({\n        ...item,\n        // 用于给组件绑定\n        selectUserList: [],\n      }));\n    }\n\n    await formApi.setFieldValue('taskId', taskId);\n\n    modalApi.modalLoading(false);\n  },\n});\n\nasync function handleSubmit() {\n  try {\n    modalApi.modalLoading(true);\n    const { valid } = await formApi.validate();\n    if (!valid) {\n      return;\n    }\n    const data = cloneDeep(await formApi.getValues());\n    // 需要转换数据 抄送人员\n    const flowCopyList = (data.flowCopyList as Array<any>).map((item) => ({\n      userId: item.userId,\n      userName: item.nickName,\n    }));\n    const requestData = {\n      ...omit(data, ['attachment']),\n      fileId: data.attachment.join(','),\n      taskVariables: {},\n      variables: {},\n      flowCopyList,\n    } as CompleteTaskReqData;\n\n    // 选人\n    if (modalApi.getData()?.assignPermission) {\n      // 判断是否选中\n      for (const item of nextNodeInfo.value) {\n        if (item.selectUserList.length === 0) {\n          message.warn(`未选择节点[${item.nodeName}]审批人`);\n          return;\n        }\n      }\n\n      const assigneeMap: { [key: string]: string } = {};\n      nextNodeInfo.value.forEach((item) => {\n        assigneeMap[item.nodeCode] = item.selectUserList\n          .map((u) => u.userId)\n          .join(',');\n      });\n      requestData.assigneeMap = assigneeMap;\n    }\n\n    await completeTask(requestData);\n    modalApi.close();\n    emit('complete');\n  } catch (error) {\n    console.error(error);\n  } finally {\n    modalApi.modalLoading(false);\n  }\n}\n</script>\n\n<template>\n  <BasicModal>\n    <BasicForm>\n      <template #flowCopyList=\"slotProps\">\n        <CopyComponent v-model:user-list=\"slotProps.modelValue\" />\n      </template>\n      <template #assigneeMap>\n        <div\n          v-for=\"item in nextNodeInfo\"\n          :key=\"item.nodeCode\"\n          class=\"flex items-center gap-2\"\n        >\n          <template v-if=\"item.permissionFlag\">\n            <span class=\"opacity-70\">{{ item.nodeName }}</span>\n            <CopyComponent\n              :allow-user-ids=\"item.permissionFlag\"\n              v-model:user-list=\"item.selectUserList\"\n            />\n          </template>\n          <template v-else>\n            <span class=\"text-red-500\">没有权限, 请联系管理员</span>\n          </template>\n        </div>\n      </template>\n    </BasicForm>\n  </BasicModal>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/workflow/components/approval-panel.vue",
    "content": "<!--\nTODO: 优化项\n会先加载流程信息 再加载业务表单信息\n-->\n<script setup lang=\"ts\">\nimport type { ApprovalType } from './type';\n\nimport type { FlowInfoResponse } from '#/api/workflow/instance/model';\nimport type { TaskInfo } from '#/api/workflow/task/model';\n\nimport { computed, ref, watch } from 'vue';\n\nimport { Fallback, VbenAvatar } from '@vben/common-ui';\nimport { DictEnum } from '@vben/constants';\nimport { cn } from '@vben/utils';\n\nimport { CopyOutlined } from '@ant-design/icons-vue';\nimport { useClipboard } from '@vueuse/core';\nimport { Card, Divider, message, TabPane, Tabs } from 'ant-design-vue';\n\nimport { flowInfo } from '#/api/workflow/instance';\nimport { getTaskByTaskId } from '#/api/workflow/task';\nimport { renderDict } from '#/utils/render';\n\nimport { FlowActions } from './actions';\nimport ApprovalDetails from './approval-details.vue';\nimport FlowPreview from './flow-preview.vue';\n\ndefineOptions({\n  name: 'ApprovalPanel',\n  inheritAttrs: false,\n});\n\nconst props = defineProps<Props>();\n\n/**\n * 下面按钮点击后会触发的事件\n */\ndefineEmits<{ reload: [] }>();\n\ninterface Props {\n  /**\n   * 行数据(list)的info\n   */\n  task?: TaskInfo;\n  /**\n   * 审批类型\n   */\n  type: ApprovalType;\n}\n\n/**\n * 目前的作用只为了获取按钮权限 因为list接口(行数据)获取为空\n */\nconst onlyForBtnPermissionTask = ref<TaskInfo>();\n/**\n * 按钮权限\n */\nconst buttonPermissions = computed(() => {\n  const record: Record<string, boolean> = {};\n  if (!onlyForBtnPermissionTask.value) {\n    return record;\n  }\n  onlyForBtnPermissionTask.value.buttonList.forEach((item) => {\n    record[item.code] = item.show;\n  });\n  return record;\n});\n\nconst showFooter = computed(() => {\n  if (props.type === 'readonly') {\n    return false;\n  }\n  // 我发起的 && [已完成, 已作废] 不显示\n  if (\n    props.type === 'myself' &&\n    ['finish', 'invalid'].includes(props.task?.flowStatus ?? '')\n  ) {\n    return false;\n  }\n  return true;\n});\n\nconst currentFlowInfo = ref<FlowInfoResponse>();\n/**\n * card的loading状态\n */\nconst loading = ref(false);\n\nasync function handleLoadInfo(task: TaskInfo | undefined) {\n  if (!task) {\n    return null;\n  }\n  try {\n    loading.value = true;\n\n    /**\n     * 不为审批不需要调用`getTaskByTaskId`接口\n     */\n    if (props.type !== 'approve') {\n      const flowResp = await flowInfo(task.businessId);\n      currentFlowInfo.value = flowResp;\n      return;\n    }\n\n    /**\n     * getTaskByTaskId主要为了获取按钮权限 目前没有其他功能\n     * 行数据(即props.task)获取的是没有按钮权限的\n     */\n    const [flowResp, taskResp] = await Promise.all([\n      flowInfo(task.businessId),\n      getTaskByTaskId(task.id),\n    ]);\n\n    currentFlowInfo.value = flowResp;\n    onlyForBtnPermissionTask.value = taskResp;\n  } catch (error) {\n    console.error(error);\n  } finally {\n    loading.value = false;\n  }\n}\n\nwatch(() => props.task, handleLoadInfo);\n\n/**\n * 不加legacy在本地开发没有问题\n * 打包后在一些设备会无法复制 使用legacy来保证兼容性\n */\nconst { copy } = useClipboard({ legacy: true });\nasync function handleCopy(text: string) {\n  await copy(text);\n  message.success('复制成功');\n}\n</script>\n\n<template>\n  <div :class=\"cn('thin-scrollbar', 'flex flex-1 overflow-y-hidden')\">\n    <Card\n      v-if=\"task\"\n      :body-style=\"{ overflowY: 'auto', height: '100%' }\"\n      :loading=\"loading\"\n      class=\"thin-scrollbar flex-1 overflow-y-hidden\"\n      size=\"small\"\n    >\n      <template #title>\n        <div class=\"flex items-center gap-2\">\n          <div>编号: {{ task.id }}</div>\n          <CopyOutlined class=\"cursor-pointer\" @click=\"handleCopy(task.id)\" />\n        </div>\n      </template>\n\n      <template #extra>\n        <a-button size=\"small\" @click=\"() => handleLoadInfo(task)\">\n          <div class=\"flex items-center justify-center\">\n            <span class=\"icon-[material-symbols--refresh] size-24px\"></span>\n          </div>\n        </a-button>\n      </template>\n\n      <div class=\"flex flex-col gap-5 p-4\">\n        <div class=\"flex flex-col gap-3\">\n          <div class=\"flex items-center gap-2\">\n            <div class=\"text-2xl font-bold\">\n              {{ task.businessTitle ?? task.flowName }}\n            </div>\n            <div>\n              <component\n                :is=\"renderDict(task.flowStatus, DictEnum.WF_BUSINESS_STATUS)\"\n              />\n            </div>\n          </div>\n\n          <div class=\"flex items-center gap-2\">\n            <VbenAvatar\n              :alt=\"task?.createByName ?? ''\"\n              class=\"bg-primary size-[28px] rounded-full text-white\"\n              src=\"\"\n            />\n\n            <span>{{ task.createByName }}</span>\n\n            <div class=\"flex items-center opacity-50\">\n              <div class=\"flex items-center gap-1\">\n                <span class=\"icon-[bxs--category-alt] size-[16px]\"></span>\n                流程分类: {{ task.categoryName }}\n              </div>\n\n              <Divider type=\"vertical\" />\n\n              <div class=\"flex items-center gap-1\">\n                <span class=\"icon-[mdi--clock-outline] size-[16px]\"></span>\n                提交时间: {{ task.createTime }}\n              </div>\n            </div>\n          </div>\n        </div>\n\n        <Tabs v-if=\"currentFlowInfo\" class=\"flex-1\">\n          <TabPane key=\"1\" tab=\"审批详情\">\n            <ApprovalDetails\n              :current-flow-info=\"currentFlowInfo\"\n              :task=\"task\"\n            />\n          </TabPane>\n\n          <TabPane key=\"2\" tab=\"审批流程图\">\n            <FlowPreview :instance-id=\"currentFlowInfo.instanceId\" />\n          </TabPane>\n        </Tabs>\n      </div>\n\n      <!-- 固定底部 占位高度 -->\n      <div class=\"h-[58px]\"></div>\n      <FlowActions\n        v-if=\"showFooter\"\n        :type=\"type\"\n        :task=\"task\"\n        :button-permissions=\"buttonPermissions\"\n        @reload=\"$emit('reload')\"\n      />\n    </Card>\n\n    <slot v-else name=\"empty\">\n      <Fallback title=\"点击左侧选择\" />\n    </slot>\n  </div>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/workflow/components/approval-rejection-modal.vue",
    "content": "<!-- 审批驳回窗口 -->\n<script setup lang=\"ts\">\nimport { useVbenModal } from '@vben/common-ui';\nimport { cloneDeep, getPopupContainer } from '@vben/utils';\n\nimport { useVbenForm } from '#/adapter/form';\nimport { backProcess, getBackTaskNode } from '#/api/workflow/task';\n\nconst emit = defineEmits<{ complete: [] }>();\n\nconst [BasicForm, formApi] = useVbenForm({\n  commonConfig: {\n    // 默认占满两列\n    formItemClass: 'col-span-2',\n    // 默认label宽度 px\n    labelWidth: 100,\n    // 通用配置项 会影响到所有表单项\n    componentProps: {\n      class: 'w-full',\n    },\n  },\n  schema: [\n    {\n      fieldName: 'taskId',\n      component: 'Input',\n      label: '任务ID',\n      dependencies: {\n        show: false,\n        triggerFields: [''],\n      },\n    },\n    {\n      fieldName: 'messageType',\n      component: 'CheckboxGroup',\n      componentProps: {\n        options: [\n          { label: '站内信', value: '1', disabled: true },\n          { label: '邮件', value: '2' },\n          { label: '短信', value: '3' },\n        ],\n      },\n      label: '通知方式',\n      defaultValue: ['1'],\n    },\n    {\n      fieldName: 'nodeCode',\n      component: 'Select',\n      componentProps: {\n        getPopupContainer,\n      },\n      label: '驳回节点',\n    },\n    {\n      fieldName: 'attachment',\n      component: 'FileUpload',\n      componentProps: {\n        maxCount: 10,\n        maxSize: 20,\n        accept: 'png, jpg, jpeg, doc, docx, xlsx, xls, ppt, pdf',\n      },\n      defaultValue: [],\n      label: '附件上传',\n      formItemClass: 'items-start',\n    },\n    {\n      fieldName: 'message',\n      component: 'Textarea',\n      label: '审批意见',\n      formItemClass: 'items-start',\n    },\n  ],\n  showDefaultActions: false,\n  wrapperClass: 'grid-cols-2',\n});\n\ninterface ModalProps {\n  taskId: string;\n  definitionId: string;\n  nodeCode: string;\n}\n\nconst [BasicModal, modalApi] = useVbenModal({\n  title: '审批驳回',\n  fullscreenButton: false,\n  class: 'min-h-[365px]',\n  onConfirm: handleSubmit,\n  async onOpenChange(isOpen) {\n    if (!isOpen) {\n      await formApi.resetForm();\n      return null;\n    }\n    modalApi.modalLoading(true);\n\n    const { taskId, nodeCode } = modalApi.getData() as ModalProps;\n    await formApi.setFieldValue('taskId', taskId);\n\n    const resp = await getBackTaskNode(taskId, nodeCode);\n    const options = resp.map((item) => ({\n      label: item.nodeName,\n      value: item.nodeCode,\n    }));\n    formApi.updateSchema([\n      {\n        fieldName: 'nodeCode',\n        componentProps: {\n          options,\n        },\n      },\n    ]);\n    // 默认选中第一个节点\n    if (options.length > 0) {\n      formApi.setFieldValue('nodeCode', options[0]?.value);\n    }\n\n    modalApi.modalLoading(false);\n  },\n});\n\nasync function handleSubmit() {\n  try {\n    modalApi.modalLoading(true);\n    const { valid } = await formApi.validate();\n    if (!valid) {\n      return;\n    }\n    const data = cloneDeep(await formApi.getValues());\n    // 附件join\n    data.fileId = data.attachment?.join?.(',');\n    // 取消attachment参数的传递\n    data.attachment = undefined;\n    await backProcess(data);\n    modalApi.close();\n    emit('complete');\n  } catch (error) {\n    console.error(error);\n  } finally {\n    modalApi.modalLoading(false);\n  }\n}\n</script>\n\n<template>\n  <BasicModal>\n    <BasicForm />\n  </BasicModal>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/workflow/components/approval-timeline-item.vue",
    "content": "<script setup lang=\"ts\">\nimport type { Flow } from '#/api/workflow/instance/model';\n\nimport { computed, h, onMounted, ref } from 'vue';\n\nimport { VbenAvatar } from '@vben/common-ui';\nimport { DictEnum } from '@vben/constants';\nimport { cn } from '@vben/utils';\n\nimport {\n  MessageOutlined,\n  UsergroupAddOutlined,\n  UserOutlined,\n} from '@ant-design/icons-vue';\nimport { Avatar, TimelineItem } from 'ant-design-vue';\n\nimport { ossInfo } from '#/api/system/oss';\nimport { renderDict } from '#/utils/render';\n\ndefineOptions({\n  name: 'ApprovalTimelineItem',\n});\n\nconst props = defineProps<{ item: Flow }>();\n\ninterface AttachmentInfo {\n  ossId: string;\n  url: string;\n  name: string;\n}\n\n/**\n * 处理附件信息\n */\nconst attachmentInfo = ref<AttachmentInfo[]>([]);\nonMounted(async () => {\n  if (!props.item.ext) {\n    return null;\n  }\n  const resp = await ossInfo(props.item.ext.split(','));\n  attachmentInfo.value = resp.map((item) => ({\n    ossId: item.ossId,\n    url: item.url,\n    name: item.originalName,\n  }));\n});\n\nconst isMultiplePerson = computed(\n  () => props.item.approver?.split(',').length > 1,\n);\n</script>\n\n<template>\n  <TimelineItem>\n    <template #dot>\n      <div class=\"relative rounded-full border\">\n        <Avatar\n          class=\"bg-primary-400\"\n          v-if=\"isMultiplePerson\"\n          :size=\"36\"\n          :icon=\"h(UsergroupAddOutlined)\"\n        />\n        <VbenAvatar\n          v-else\n          :alt=\"item?.approveName ?? 'unknown'\"\n          class=\"bg-primary size-[36px] rounded-full text-white\"\n          src=\"\"\n        />\n        <div\n          :class=\"\n            cn(\n              'absolute bottom-0 right-[-2px]',\n              'size-[12px] rounded-full bg-green-500',\n              'border-[2px] border-white',\n            )\n          \"\n        ></div>\n      </div>\n    </template>\n    <div class=\"mb-5 ml-2 flex flex-col gap-1\">\n      <div class=\"flex items-center gap-1\">\n        <div class=\"font-bold\">{{ item.nodeName }}</div>\n        <component :is=\"renderDict(item.flowStatus, DictEnum.WF_TASK_STATUS)\" />\n      </div>\n\n      <div :class=\"cn('mt-2 flex flex-wrap gap-2')\" v-if=\"isMultiplePerson\">\n        <!-- 如果昵称中带, 这里的处理是不准确的 -->\n        <div\n          :class=\"cn('bg-foreground/5 flex items-center rounded-full', 'p-1')\"\n          v-for=\"(name, index) in item.approveName.split(',')\"\n          :key=\"index\"\n        >\n          <Avatar\n            class=\"bg-primary-400 flex items-center justify-center\"\n            :size=\"24\"\n            :icon=\"h(UserOutlined)\"\n          />\n          <span class=\"px-1\">{{ name }}</span>\n        </div>\n      </div>\n      <div v-else>{{ item.approveName }}</div>\n\n      <div>{{ item.updateTime }}</div>\n      <div\n        v-if=\"item.message\"\n        class=\"rounded-lg border px-3 py-1\"\n        :class=\"cn('flex gap-2')\"\n      >\n        <MessageOutlined />\n        <div class=\"text-foreground/75 break-all\">{{ item.message }}</div>\n      </div>\n      <div v-if=\"attachmentInfo.length > 0\" class=\"flex flex-wrap gap-2\">\n        <!-- 这里下载的文件名不是原始文件名 -->\n        <a\n          v-for=\"attachment in attachmentInfo\"\n          :key=\"attachment.ossId\"\n          :href=\"attachment.url\"\n          class=\"text-primary\"\n          target=\"_blank\"\n        >\n          <div class=\"flex items-center gap-1\">\n            <span class=\"icon-[mingcute--attachment-line] size-[18px]\"></span>\n            <span>{{ attachment.name }}</span>\n          </div>\n        </a>\n      </div>\n    </div>\n  </TimelineItem>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/workflow/components/approval-timeline.vue",
    "content": "<script setup lang=\"ts\">\nimport type { Flow } from '#/api/workflow/instance/model';\n\nimport { Empty, Timeline } from 'ant-design-vue';\n\nimport ApprovalTimelineItem from './approval-timeline-item.vue';\n\ninterface Props {\n  list: Flow[];\n}\n\ndefineProps<Props>();\n</script>\n\n<template>\n  <Timeline v-if=\"list.length > 0\">\n    <ApprovalTimelineItem v-for=\"item in list\" :key=\"item.id\" :item=\"item\" />\n  </Timeline>\n  <Empty v-else />\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/workflow/components/copy-component.vue",
    "content": "<!--抄送组件-->\n<script setup lang=\"ts\">\nimport type { PropType } from 'vue';\n\nimport type { User } from '#/api/system/user/model';\n\nimport { computed } from 'vue';\n\nimport { useVbenModal, VbenAvatar } from '@vben/common-ui';\n\nimport { Avatar, AvatarGroup, Tooltip } from 'ant-design-vue';\n\nimport { userSelectModal } from '.';\n\ndefineOptions({\n  name: 'CopyComponent',\n  inheritAttrs: false,\n});\n\nconst props = withDefaults(\n  defineProps<{ allowUserIds?: string; ellipseNumber?: number }>(),\n  {\n    /**\n     * 最大显示的头像数量 超过显示为省略号头像\n     */\n    ellipseNumber: 3,\n    /**\n     * 允许选择允许选择的人员ID 会当做参数拼接在uselist接口\n     */\n    allowUserIds: '',\n  },\n);\n\nconst emit = defineEmits<{ cancel: []; finish: [User[]] }>();\n\nconst [UserSelectModal, modalApi] = useVbenModal({\n  connectedComponent: userSelectModal,\n});\n\nconst userListModel = defineModel('userList', {\n  type: Array as PropType<User[]>,\n  default: () => [],\n});\n\nfunction handleOpen() {\n  modalApi.setData({ userList: userListModel.value });\n  modalApi.open();\n}\n\nfunction handleFinish(userList: User[]) {\n  // 清空 直接赋值[]会丢失响应性\n  userListModel.value.splice(0);\n  userListModel.value.push(...userList);\n  emit('finish', userList);\n}\n\nconst displayedList = computed(() => {\n  return userListModel.value.slice(0, props.ellipseNumber);\n});\n</script>\n\n<template>\n  <div class=\"flex items-center gap-2\">\n    <AvatarGroup v-if=\"userListModel.length > 0\">\n      <Tooltip\n        v-for=\"user in displayedList\"\n        :key=\"user.userId\"\n        :title=\"user.nickName\"\n        placement=\"top\"\n      >\n        <div>\n          <VbenAvatar\n            :alt=\"user?.nickName ?? ''\"\n            class=\"bg-primary size-[36px] cursor-pointer rounded-full border text-white\"\n            src=\"\"\n          />\n        </div>\n      </Tooltip>\n      <Tooltip\n        :title=\"`等${userListModel.length - props.ellipseNumber}人`\"\n        placement=\"top\"\n      >\n        <Avatar\n          v-if=\"userListModel.length > ellipseNumber\"\n          class=\"flex size-[36px] cursor-pointer items-center justify-center rounded-full border bg-[gray] text-white\"\n        >\n          +{{ userListModel.length - props.ellipseNumber }}\n        </Avatar>\n      </Tooltip>\n    </AvatarGroup>\n    <a-button size=\"small\" @click=\"handleOpen\">选择人员</a-button>\n    <UserSelectModal\n      :allow-user-ids=\"allowUserIds\"\n      @cancel=\"$emit('cancel')\"\n      @finish=\"handleFinish\"\n    />\n  </div>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/workflow/components/flow-designer.vue",
    "content": "<script setup lang=\"ts\">\nimport { useRoute, useRouter } from 'vue-router';\n\nimport { useAppConfig, useTabs } from '@vben/hooks';\nimport { stringify } from '@vben/request';\nimport { useAccessStore } from '@vben/stores';\n\nimport { useEventListener } from '@vueuse/core';\n\ndefineOptions({ name: 'FlowDesigner' });\n\nconst route = useRoute();\nconst definitionId = route.query.definitionId as string;\n// const disabled = route.query.disabled === 'true';\n\nconst { clientId } = useAppConfig(import.meta.env, import.meta.env.PROD);\n\nconst accessStore = useAccessStore();\nconst params = {\n  Authorization: `Bearer ${accessStore.accessToken}`,\n  id: definitionId,\n  clientid: clientId,\n  onlyDesignShow: true,\n};\n\n/**\n * iframe设计器的地址\n */\nconst url = `${import.meta.env.VITE_GLOB_API_URL}/warm-flow-ui/index.html?${stringify(params)}`;\n\nconst { closeCurrentTab } = useTabs();\nconst router = useRouter();\n\nfunction messageHandler(event: MessageEvent) {\n  switch (event.data.method) {\n    case 'close': {\n      // 关闭当前tab\n      closeCurrentTab();\n      // 跳转到流程定义列表\n      router.push('/workflow/processDefinition');\n      break;\n    }\n  }\n}\n\n// iframe监听组件内设计器保存事件\nuseEventListener('message', messageHandler);\n</script>\n\n<template>\n  <div class=\"size-full\">\n    <iframe :src=\"url\" class=\"size-full\"></iframe>\n  </div>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/workflow/components/flow-info-modal.vue",
    "content": "<!-- 弹窗查看流程信息 -->\n<script setup lang=\"ts\">\nimport type { TaskInfo } from '#/api/workflow/task/model';\n\nimport { ref } from 'vue';\n\nimport { useVbenModal } from '@vben/common-ui';\n\nimport { Spin } from 'ant-design-vue';\n\nimport { getTaskByBusinessId } from '#/api/workflow/instance';\n\nimport { ApprovalPanel } from '.';\n\ninterface ModalProps {\n  businessId: string;\n}\n\nconst taskInfo = ref<TaskInfo>();\n\nconst [BasicModal, modalApi] = useVbenModal({\n  title: '流程信息',\n  class: 'w-[1000px]',\n  footer: false,\n  onClosed: () => {\n    taskInfo.value = undefined;\n  },\n  onOpenChange: async (isOpen) => {\n    if (!isOpen) {\n      return null;\n    }\n    const { businessId } = modalApi.getData() as ModalProps;\n    const taskResp = await getTaskByBusinessId(businessId);\n    taskInfo.value = taskResp;\n  },\n});\n</script>\n\n<template>\n  <BasicModal>\n    <ApprovalPanel :task=\"taskInfo\" type=\"readonly\">\n      <template #empty>\n        <Spin\n          class=\"flex h-[200px] w-full items-center justify-center\"\n          size=\"large\"\n        />\n      </template>\n    </ApprovalPanel>\n  </BasicModal>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/workflow/components/flow-interfere-modal.vue",
    "content": "<script setup lang=\"ts\">\nimport type { User } from '#/api/system/user/model';\nimport type { TaskInfo } from '#/api/workflow/task/model';\n\nimport { computed, ref } from 'vue';\n\nimport { useVbenModal } from '@vben/common-ui';\n\nimport { Descriptions, DescriptionsItem, Modal } from 'ant-design-vue';\n\nimport {\n  getTaskByTaskId,\n  taskOperation,\n  terminationTask,\n} from '#/api/workflow/task';\n\nimport { userSelectModal } from '.';\n\nconst emit = defineEmits<{ complete: [] }>();\n\nconst taskInfo = ref<TaskInfo>();\n\n/**\n * 是否显示 加签/减签操作\n */\nconst showMultiActions = computed(() => {\n  if (!taskInfo.value) {\n    return false;\n  }\n  if (Number(taskInfo.value.nodeRatio) > 0) {\n    return true;\n  }\n  return false;\n});\n\nconst [BasicModal, modalApi] = useVbenModal({\n  title: '流程干预',\n  class: 'w-[800px]',\n  fullscreenButton: false,\n  async onOpenChange(isOpen) {\n    if (!isOpen) {\n      return null;\n    }\n    const { taskId } = modalApi.getData() as { taskId: string };\n    taskInfo.value = await getTaskByTaskId(taskId);\n  },\n});\n\n/**\n * 转办\n */\nconst [TransferModal, transferModalApi] = useVbenModal({\n  connectedComponent: userSelectModal,\n});\nfunction handleTransfer(userList: User[]) {\n  if (userList.length === 0 || !taskInfo.value) return;\n  const current = userList[0];\n  Modal.confirm({\n    title: '转办',\n    content: `确定转办给${current?.nickName}吗?`,\n    centered: true,\n    onOk: async () => {\n      await taskOperation(\n        { taskId: taskInfo.value!.id, userId: current!.userId },\n        'transferTask',\n      );\n      emit('complete');\n    },\n  });\n}\n\n/**\n * 审批终止\n */\nfunction handleTermination() {\n  if (!taskInfo.value) {\n    return;\n  }\n  Modal.confirm({\n    title: '审批终止',\n    content: '确定终止当前审批流程吗？',\n    centered: true,\n    okButtonProps: { danger: true },\n    onOk: async () => {\n      await terminationTask({ taskId: taskInfo.value!.id });\n      emit('complete');\n    },\n  });\n}\n\nconst [AddSignatureModal, addSignatureModalApi] = useVbenModal({\n  connectedComponent: userSelectModal,\n});\nfunction handleAddSignature(userList: User[]) {\n  if (userList.length === 0 || !taskInfo.value) return;\n  const userIds = userList.map((user) => user.userId);\n  Modal.confirm({\n    title: '提示',\n    content: '确认加签吗?',\n    centered: true,\n    onOk: async () => {\n      await taskOperation(\n        { taskId: taskInfo.value!.id, userIds },\n        'addSignature',\n      );\n      emit('complete');\n    },\n  });\n}\n\nconst [ReductionSignatureModal, reductionSignatureModalApi] = useVbenModal({\n  connectedComponent: userSelectModal,\n});\nfunction handleReductionSignature(userList: User[]) {\n  if (userList.length === 0 || !taskInfo.value) return;\n  const userIds = userList.map((user) => user.userId);\n  Modal.confirm({\n    title: '提示',\n    content: '确认减签吗?',\n    centered: true,\n    onOk: async () => {\n      await taskOperation(\n        { taskId: taskInfo.value!.id, userIds },\n        'reductionSignature',\n      );\n      emit('complete');\n    },\n  });\n}\n</script>\n\n<template>\n  <BasicModal>\n    <Descriptions v-if=\"taskInfo\" :column=\"2\" bordered size=\"small\">\n      <DescriptionsItem label=\"任务名称\">\n        {{ taskInfo.nodeName }}\n      </DescriptionsItem>\n      <DescriptionsItem label=\"节点编码\">\n        {{ taskInfo.nodeCode }}\n      </DescriptionsItem>\n      <DescriptionsItem label=\"开始时间\">\n        {{ taskInfo.createTime }}\n      </DescriptionsItem>\n      <DescriptionsItem label=\"流程实例ID\">\n        {{ taskInfo.instanceId }}\n      </DescriptionsItem>\n      <DescriptionsItem label=\"版本号\">\n        {{ taskInfo.version }}\n      </DescriptionsItem>\n      <DescriptionsItem label=\"业务ID\">\n        {{ taskInfo.businessId }}\n      </DescriptionsItem>\n    </Descriptions>\n    <TransferModal mode=\"single\" @finish=\"handleTransfer\" />\n    <AddSignatureModal mode=\"multiple\" @finish=\"handleAddSignature\" />\n    <ReductionSignatureModal\n      mode=\"multiple\"\n      @finish=\"handleReductionSignature\"\n    />\n    <template #footer>\n      <template v-if=\"showMultiActions\">\n        <a-button @click=\"() => addSignatureModalApi.open()\">加签</a-button>\n        <a-button @click=\"() => reductionSignatureModalApi.open()\">\n          减签\n        </a-button>\n      </template>\n      <a-button @click=\"() => transferModalApi.open()\">转办</a-button>\n      <a-button danger type=\"primary\" @click=\"handleTermination\">终止</a-button>\n    </template>\n  </BasicModal>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/workflow/components/flow-preview.vue",
    "content": "<!-- 流程图预览组件 -->\n\n<script setup lang=\"ts\">\nimport { useAppConfig } from '@vben/hooks';\nimport { stringify } from '@vben/request';\nimport { useAccessStore } from '@vben/stores';\n\nimport { useWarmflowIframe } from './hook';\n\ndefineOptions({ name: 'FlowPreview' });\n\nconst props = defineProps<Props>();\n\ninterface Props {\n  /**\n   * 流程实例ID\n   */\n  instanceId: string;\n}\n\nconst { clientId } = useAppConfig(import.meta.env, import.meta.env.PROD);\n\nconst accessStore = useAccessStore();\nconst params = {\n  Authorization: `Bearer ${accessStore.accessToken}`,\n  id: props.instanceId,\n  clientid: clientId,\n  type: 'FlowChart',\n};\n\n/**\n * iframe地址\n * 后端地址 + 固定flow地址拼接\n */\nconst url = `${import.meta.env.VITE_GLOB_API_URL}/warm-flow-ui/index.html?${stringify(params)}`;\n\nconst { iframeRef } = useWarmflowIframe();\n</script>\n\n<template>\n  <iframe\n    ref=\"iframeRef\"\n    :src=\"url\"\n    class=\"h-[600px] w-full rounded-[6px] border\"\n  ></iframe>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/workflow/components/helper.tsx",
    "content": "import { defineComponent, h, ref } from 'vue';\n\nimport { Modal } from 'ant-design-vue';\nimport dayjs from 'dayjs';\nimport duration from 'dayjs/plugin/duration';\nimport relativeTime from 'dayjs/plugin/relativeTime';\n\nimport ApprovalContent from './approval-content.vue';\n\nexport interface ApproveWithReasonModalProps {\n  title: string;\n  description: string;\n  onOk: (reason: string) => void;\n}\n\n/**\n * 带审批意见的confirm\n * @param props props\n */\nexport function approveWithReasonModal(props: ApproveWithReasonModalProps) {\n  const { onOk, title, description } = props;\n  const content = ref('');\n  Modal.confirm({\n    title,\n    content: h(\n      defineComponent({\n        setup() {\n          return () =>\n            h(ApprovalContent, {\n              description,\n              value: content.value,\n              'onUpdate:value': (v) => (content.value = v),\n            });\n        },\n      }),\n    ),\n    centered: true,\n    okButtonProps: { danger: true },\n    onOk: () => onOk(content.value),\n  });\n}\n\ndayjs.extend(duration);\ndayjs.extend(relativeTime);\n/**\n * 计算相差的时间\n * @param dateTime 时间字符串\n * @returns 相差的时间\n */\nexport function getDiffTimeString(dateTime: string) {\n  // 计算相差秒数\n  const diffSeconds = dayjs().diff(dayjs(dateTime), 'second');\n  /**\n   * 转为时间显示(x月 x天)\n   * https://dayjs.fenxianglu.cn/category/duration.html#%E4%BA%BA%E6%80%A7%E5%8C%96\n   *\n   */\n  const diffText = dayjs.duration(diffSeconds, 'seconds').humanize();\n  return diffText;\n}\n"
  },
  {
    "path": "apps/web-antd/src/views/workflow/components/hook.ts",
    "content": "import { onMounted, useTemplateRef, watch } from 'vue';\n\nimport { usePreferences } from '@vben/preferences';\n\n/**\n * warmflow ref相关操作\n * @returns hook\n */\nexport function useWarmflowIframe() {\n  const iframeRef = useTemplateRef<HTMLIFrameElement>('iframeRef');\n  const { isDark } = usePreferences();\n\n  async function iframeLoadEvent() {\n    /**\n     * TODO: 这里可以优化 因为拿不到内部vue的mount状态\n     */\n    await new Promise((resolve) => setTimeout(resolve, 500));\n    const theme = isDark.value ? 'theme-dark' : 'theme-light';\n    iframeRef.value?.contentWindow?.postMessage({ type: theme });\n  }\n\n  onMounted(() => {\n    /**\n     * load只是iframe加载完 而非vue加载完\n     */\n    iframeRef.value?.addEventListener('load', iframeLoadEvent);\n  });\n\n  // onBeforeUnmount(() => {\n  //   iframeRef.value?.removeEventListener('load', iframeLoadEvent);\n  // });\n\n  // 监听主题切换 通知iframe切换\n  watch(isDark, (dark) => {\n    if (!iframeRef.value) {\n      return;\n    }\n    const theme = dark ? 'theme-dark' : 'theme-light';\n    iframeRef.value.contentWindow?.postMessage({ type: theme });\n  });\n\n  return { iframeRef };\n}\n"
  },
  {
    "path": "apps/web-antd/src/views/workflow/components/index.ts",
    "content": "export { default as applyModal } from './apply-modal.vue';\nexport { default as ApprovalCard } from './approval-card.vue';\n/**\n * 审批同意\n */\nexport { default as approvalModal } from './approval-modal.vue';\nexport { default as ApprovalPanel } from './approval-panel.vue';\n/**\n * 审批驳回\n */\nexport { default as approvalRejectionModal } from './approval-rejection-modal.vue';\nexport { default as ApprovalTimeline } from './approval-timeline.vue';\n/**\n * 选择抄送人\n */\nexport { default as CopyComponent } from './copy-component.vue';\n\n/**\n * 详情信息 modal\n */\nexport { default as flowInfoModal } from './flow-info-modal.vue';\n/**\n * 流程干预 modal\n */\nexport { default as flowInterfereModal } from './flow-interfere-modal.vue';\n\n/**\n * 选人 支持单选/多选\n */\nexport { default as userSelectModal } from './user-select-modal.vue';\n"
  },
  {
    "path": "apps/web-antd/src/views/workflow/components/type.d.ts",
    "content": "export {};\n/**\n * myself 我发起的\n * readonly 只读 只用于查看\n * approve 审批(我的待办)\n * admin 流程监控 - 待办任务使用\n */\nexport type ApprovalType = 'admin' | 'approve' | 'myself' | 'readonly';\n"
  },
  {
    "path": "apps/web-antd/src/views/workflow/components/user-select-modal.vue",
    "content": "<!-- eslint-disable no-use-before-define -->\n<script setup lang=\"ts\">\nimport type { VbenFormProps } from '@vben/common-ui';\n\nimport type { VxeGridProps } from '#/adapter/vxe-table';\nimport type { User } from '#/api';\n\nimport { ref } from 'vue';\n\nimport { useVbenModal, VbenAvatar } from '@vben/common-ui';\n\nimport { useVbenVxeGrid } from '#/adapter/vxe-table';\nimport { userList } from '#/api/system/user';\nimport DeptTree from '#/views/system/user/dept-tree.vue';\n\ndefineOptions({\n  name: 'UserSelectModal',\n  inheritAttrs: false,\n});\n\nconst props = withDefaults(\n  defineProps<{ allowUserIds?: string; mode?: 'multiple' | 'single' }>(),\n  {\n    mode: 'multiple',\n    /**\n     * 允许选择允许选择的人员ID 会当做参数拼接在uselist接口\n     */\n    allowUserIds: '',\n  },\n);\n\nconst emit = defineEmits<{\n  /**\n   * 取消的事件\n   */\n  cancel: [];\n  /**\n   * 选择完成的事件\n   */\n  finish: [User[]];\n}>();\n\nconst [BasicModal, modalApi] = useVbenModal({\n  title: '选择人员',\n  class: 'w-[1060px]',\n  fullscreenButton: false,\n  onClosed: () => emit('cancel'),\n  onConfirm: handleSubmit,\n  async onOpened() {\n    const { userList = [] } = modalApi.getData() as { userList: User[] };\n    // 暂时只处理多选 目前并没有单选的情况\n    if (props.mode === 'multiple') {\n      // 左边选中\n      await tableApi.grid.setCheckboxRow(userList, true);\n      // 右边赋值\n      await rightTableApi.grid.loadData(userList);\n    }\n  },\n});\n\n// 左边部门用\nconst selectDeptId = ref<string[]>([]);\nconst formOptions: VbenFormProps = {\n  schema: [\n    {\n      component: 'Input',\n      fieldName: 'userName',\n      label: '用户账号',\n      hideLabel: true,\n      componentProps: {\n        placeholder: '请输入账号',\n      },\n    },\n  ],\n  commonConfig: {\n    labelWidth: 80,\n    componentProps: {\n      allowClear: true,\n    },\n  },\n  wrapperClass: 'grid-cols-2',\n  handleReset: async () => {\n    selectDeptId.value = [];\n    const { formApi, reload } = tableApi;\n    await formApi.resetForm();\n    const formValues = formApi.form.values;\n    formApi.setLatestSubmissionValues(formValues);\n    await reload(formValues);\n  },\n};\n\nconst gridOptions: VxeGridProps = {\n  checkboxConfig: {\n    // 翻页时保留选中状态\n    reserve: true,\n    // 点击行选中\n    trigger: 'row',\n  },\n  radioConfig: {\n    trigger: 'row',\n    strict: true,\n  },\n  columns: [\n    {\n      type: props.mode === 'single' ? 'radio' : 'checkbox',\n      width: 60,\n      resizable: false,\n    },\n    {\n      field: 'userName',\n      title: '用户',\n      headerAlign: 'left',\n      resizable: false,\n      slots: {\n        default: 'user',\n      },\n    },\n  ],\n  height: 'auto',\n  keepSource: true,\n  pagerConfig: {\n    layouts: ['PrevPage', 'Number', 'NextPage', 'Sizes', 'Total'],\n  },\n  proxyConfig: {\n    ajax: {\n      query: async ({ page }, formValues = {}) => {\n        // 部门树选择处理\n        if (selectDeptId.value.length === 1) {\n          formValues.deptId = selectDeptId.value[0];\n        } else {\n          Reflect.deleteProperty(formValues, 'deptId');\n        }\n\n        // 加载完毕需要设置选中的行\n        if (props.mode === 'multiple') {\n          const records = rightTableApi.grid.getData();\n          await tableApi.grid.setCheckboxRow(records, true);\n        }\n        if (props.mode === 'single') {\n          const records = rightTableApi.grid.getData();\n          if (records.length === 1) {\n            await tableApi.grid.setRadioRow(records[0]);\n          }\n        }\n\n        const params: any = {\n          pageNum: page.currentPage,\n          pageSize: page.pageSize,\n          ...formValues,\n        };\n        // 添加参数\n        if (props.allowUserIds) {\n          params.userIds = props.allowUserIds;\n        }\n\n        return await userList(params);\n      },\n    },\n  },\n  rowConfig: {\n    keyField: 'userId',\n  },\n  toolbarConfig: {\n    enabled: false,\n  },\n  showOverflow: false,\n};\n\nconst [BasicTable, tableApi] = useVbenVxeGrid({\n  formOptions,\n  gridOptions,\n  gridEvents: {\n    // 需要控制不同的事件 radio也会触发checkbox事件\n    checkboxChange: checkBoxEvent,\n    checkboxAll: checkBoxEvent,\n    radioChange: radioEvent,\n  },\n});\n\nfunction checkBoxEvent() {\n  if (props.mode !== 'multiple') {\n    return;\n  }\n  /**\n   * 给右边表格赋值\n   * records拿到的是当前页的选中数据\n   * reserveRecords拿到的是其他页选中的数据\n   */\n  const records = tableApi.grid.getCheckboxRecords();\n  const reserveRecords = tableApi.grid.getCheckboxReserveRecords();\n  const realRecords = [...records, ...reserveRecords];\n  rightTableApi.grid.loadData(realRecords);\n}\n\nfunction radioEvent() {\n  if (props.mode !== 'single') {\n    return;\n  }\n  // 给右边表格赋值\n  const records = tableApi.grid.getRadioRecord();\n  rightTableApi.grid.loadData([records]);\n}\n\nconst rightGridOptions: VxeGridProps = {\n  checkboxConfig: {},\n  columns: [\n    {\n      field: 'nickName',\n      title: '昵称',\n      width: 200,\n      resizable: false,\n      slots: {\n        default: 'user',\n      },\n    },\n    {\n      field: 'action',\n      title: '操作',\n      width: 120,\n      slots: { default: 'action' },\n    },\n  ],\n  height: 'auto',\n  keepSource: true,\n  pagerConfig: {\n    enabled: false,\n  },\n  proxyConfig: {\n    enabled: false,\n  },\n  rowConfig: {\n    keyField: 'userId',\n  },\n  toolbarConfig: {\n    enabled: false,\n  },\n  showOverflow: false,\n};\n\nconst [RightBasicTable, rightTableApi] = useVbenVxeGrid({\n  gridOptions: rightGridOptions,\n});\n\nasync function handleRemoveItem(row: any) {\n  if (props.mode === 'multiple') {\n    await tableApi.grid.setCheckboxRow(row, false);\n  }\n  if (props.mode === 'single') {\n    await tableApi.grid.clearRadioRow();\n  }\n  const data = rightTableApi.grid.getData();\n  await rightTableApi.grid.loadData(data.filter((item) => item !== row));\n  // 这个方法有问题\n  // await rightTableApi.grid.remove(row);\n}\n\nfunction handleRemoveAll() {\n  if (props.mode === 'multiple') {\n    tableApi.grid.clearCheckboxRow();\n    tableApi.grid.clearCheckboxReserve();\n  }\n  if (props.mode === 'single') {\n    tableApi.grid.clearRadioRow();\n  }\n  rightTableApi.grid.loadData([]);\n}\n\nasync function handleDeptQuery() {\n  await tableApi.reload();\n  // 重置后恢复 保存勾选的数据\n  const records = rightTableApi.grid.getData();\n  if (props.mode === 'multiple') {\n    tableApi?.grid.setCheckboxRow(records, true);\n  }\n  if (props.mode === 'single' && records.length === 1) {\n    tableApi.grid.setRadioRow(records[0]);\n  }\n}\n\nfunction handleSubmit() {\n  const records = rightTableApi.grid.getData();\n  console.log(records);\n  emit('finish', records);\n  modalApi.close();\n}\n</script>\n\n<template>\n  <BasicModal>\n    <div class=\"flex min-h-[600px]\">\n      <DeptTree\n        v-model:select-dept-id=\"selectDeptId\"\n        :show-search=\"false\"\n        class=\"w-[230px]\"\n        @reload=\"() => tableApi.reload()\"\n        @select=\"handleDeptQuery\"\n      />\n      <div class=\"h-[600px] w-[450px]\">\n        <BasicTable>\n          <template #user=\"{ row }\">\n            <div class=\"flex items-center gap-2\">\n              <VbenAvatar\n                :alt=\"row.nickName\"\n                :src=\"row.avatar ?? ''\"\n                :class=\"{ 'bg-primary': !row.avatar }\"\n                class=\"size-[32px] rounded-full text-white\"\n              />\n              <div class=\"flex flex-col items-baseline text-[12px]\">\n                <div>{{ row.nickName }}</div>\n                <div class=\"opacity-50\">\n                  {{ row.phonenumber || '暂无手机号' }}\n                </div>\n              </div>\n            </div>\n          </template>\n        </BasicTable>\n      </div>\n      <div class=\"flex h-[600px] flex-col\">\n        <div class=\"flex w-full px-4\">\n          <div class=\"flex w-full items-center justify-between\">\n            <div>已选中人员</div>\n            <div>\n              <a-button size=\"small\" @click=\"handleRemoveAll\">\n                清空选中\n              </a-button>\n            </div>\n          </div>\n        </div>\n        <RightBasicTable id=\"user-select-right-table\">\n          <template #user=\"{ row }\">\n            <div class=\"flex items-center gap-2 overflow-hidden\">\n              <VbenAvatar\n                :alt=\"row.nickName\"\n                :src=\"row.avatar ?? ''\"\n                :class=\"{ 'bg-primary': !row.avatar }\"\n                class=\"size-[32px] rounded-full text-white\"\n              />\n              <div class=\"flex flex-col items-baseline text-[12px]\">\n                <div class=\"overflow-ellipsis whitespace-nowrap\">\n                  {{ row.nickName }}\n                </div>\n                <div class=\"opacity-50\">\n                  {{ row.phonenumber || '暂无手机号' }}\n                </div>\n              </div>\n            </div>\n          </template>\n          <template #action=\"{ row }\">\n            <a-button size=\"small\" @click=\"handleRemoveItem(row)\">\n              移除\n            </a-button>\n          </template>\n        </RightBasicTable>\n      </div>\n    </div>\n  </BasicModal>\n</template>\n\n<style scoped>\n:deep(div.vben-link) {\n  display: none;\n}\n\n:deep(.vxe-body--row) {\n  cursor: pointer;\n}\n</style>\n\n<style lang=\"scss\">\n/**\n默认显示右边的滚动条 防止出现滚动条被挤压\n*/\n#user-select-right-table {\n  div.vxe-table--body-wrapper.body--wrapper {\n    overflow: scroll;\n  }\n}\n</style>\n"
  },
  {
    "path": "apps/web-antd/src/views/workflow/leave/api/index.ts",
    "content": "import type { LeaveForm, LeaveQuery, LeaveVO } from './model';\n\nimport type { ID, IDS, PageResult } from '#/api/common';\n\nimport { commonExport } from '#/api/helper';\nimport { requestClient } from '#/api/request';\n\n/**\n * 查询请假申请列表\n * @param params\n * @returns 请假申请列表\n */\nexport function leaveList(params?: LeaveQuery) {\n  return requestClient.get<PageResult<LeaveVO>>('/workflow/leave/list', {\n    params,\n  });\n}\n\n/**\n * 导出请假申请列表\n * @param params\n * @returns 请假申请列表\n */\nexport function leaveExport(params?: LeaveQuery) {\n  return commonExport('/workflow/leave/export', params ?? {});\n}\n\n/**\n * 查询请假申请详情\n * @param id id\n * @returns 请假申请详情\n */\nexport function leaveInfo(id: ID) {\n  return requestClient.get<LeaveVO>(`/workflow/leave/${id}`);\n}\n\n/**\n * 新增请假申请\n * @param data\n * @returns void\n */\nexport function leaveAdd(data: LeaveForm) {\n  return requestClient.postWithMsg<LeaveVO>('/workflow/leave', data);\n}\n\n/**\n * 更新请假申请\n * @param data\n * @returns void\n */\nexport function leaveUpdate(data: LeaveForm) {\n  return requestClient.putWithMsg<LeaveVO>('/workflow/leave', data);\n}\n\n/**\n * 删除请假申请\n * @param id id\n * @returns void\n */\nexport function leaveRemove(id: ID | IDS) {\n  return requestClient.deleteWithMsg<void>(`/workflow/leave/${id}`);\n}\n\n/**\n * 提交 & 发起流程(后端发起)\n * @param data data\n * @returns void\n */\nexport function submitAndStartWorkflow(data: LeaveForm) {\n  return requestClient.postWithMsg<void>(\n    '/workflow/leave/submitAndFlowStart',\n    data,\n  );\n}\n"
  },
  {
    "path": "apps/web-antd/src/views/workflow/leave/api/model.d.ts",
    "content": "import type { BaseEntity, PageQuery } from '#/api/common';\n\nexport interface LeaveVO {\n  /**\n   * 主键\n   */\n  id: number | string;\n\n  /**\n   * 请假类型\n   */\n  leaveType: string;\n\n  /**\n   * 开始时间\n   */\n  startDate: string;\n\n  /**\n   * 结束时间\n   */\n  endDate: string;\n\n  /**\n   * 请假天数\n   */\n  leaveDays: number;\n\n  /**\n   * 请假原因\n   */\n  remark: string;\n\n  /**\n   *\n   */\n  status: string;\n  applyCode?: string;\n}\n\nexport interface LeaveForm extends BaseEntity {\n  /**\n   * 主键\n   */\n  id?: number | string;\n\n  /**\n   * 请假类型\n   */\n  leaveType?: string;\n\n  /**\n   * 开始时间\n   */\n  startDate?: string;\n\n  /**\n   * 结束时间\n   */\n  endDate?: string;\n\n  /**\n   * 请假天数\n   */\n  leaveDays?: number;\n\n  /**\n   * 请假原因\n   */\n  remark?: string;\n\n  /**\n   *\n   */\n  status?: string;\n}\n\nexport interface LeaveQuery extends PageQuery {\n  /**\n   * 请假类型\n   */\n  leaveType?: string;\n\n  /**\n   * 开始时间\n   */\n  startDate?: string;\n\n  /**\n   * 结束时间\n   */\n  endDate?: string;\n\n  /**\n   * 请假天数\n   */\n  leaveDays?: number;\n\n  /**\n   *\n   */\n  status?: string;\n\n  /**\n   * 日期范围参数\n   */\n  params?: any;\n}\n"
  },
  {
    "path": "apps/web-antd/src/views/workflow/leave/data.tsx",
    "content": "import type { FormSchemaGetter, VbenFormSchema } from '#/adapter/form';\nimport type { VxeGridProps } from '#/adapter/vxe-table';\n\nimport { DictEnum } from '@vben/constants';\nimport { getPopupContainer } from '@vben/utils';\n\nimport dayjs from 'dayjs';\n\nimport { OptionsTag } from '#/components/table';\nimport { renderDict } from '#/utils/render';\n\nexport const leaveTypeOptions = [\n  { label: '病假 😷', value: '1' },\n  { label: '事假 🖥', value: '2' },\n  { label: '年假 🏝', value: '3' },\n  { label: '婚假 💒', value: '4' },\n  { label: '产假 🤰', value: '5' },\n  { label: '其他 🤔', value: '7' },\n];\n\nexport const leaveFlowOptions = [\n  { label: '请假流程-普通', value: 'leave1' },\n  { label: '请假流程-排他网关', value: 'leave2' },\n  { label: '请假流程-并行网关', value: 'leave3' },\n  { label: '请假流程-会签', value: 'leave4' },\n  { label: '请假申请-并行会签网关', value: 'leave5' },\n  { label: '请假申请-排他并行网关', value: 'leave6' },\n];\n\nexport const querySchema: FormSchemaGetter = () => [\n  {\n    component: 'InputNumber',\n    componentProps: {\n      min: 1,\n    },\n    fieldName: 'startLeaveDays',\n    label: '请假天数',\n  },\n  {\n    component: 'InputNumber',\n    componentProps: {\n      min: 1,\n    },\n    fieldName: 'endLeaveDays',\n    label: '至',\n    labelClass: 'justify-center',\n  },\n];\n\nexport const columns: VxeGridProps['columns'] = [\n  { type: 'checkbox', width: 60 },\n  {\n    title: '请假类型',\n    field: 'leaveType',\n    slots: {\n      default: ({ row }) => {\n        return <OptionsTag options={leaveTypeOptions} value={row.leaveType} />;\n      },\n    },\n  },\n  {\n    title: '开始时间',\n    field: 'startDate',\n    formatter: ({ cellValue }) => dayjs(cellValue).format('YYYY-MM-DD'),\n  },\n  {\n    title: '结束时间',\n    field: 'endDate',\n    formatter: ({ cellValue }) => dayjs(cellValue).format('YYYY-MM-DD'),\n  },\n  {\n    title: '请假天数',\n    field: 'leaveDays',\n    formatter: ({ cellValue }) => `${cellValue}天`,\n  },\n  {\n    title: '请假原因',\n    field: 'remark',\n  },\n  {\n    title: '流程状态',\n    field: 'status',\n    slots: {\n      default: ({ row }) => {\n        return renderDict(row.status, DictEnum.WF_BUSINESS_STATUS);\n      },\n    },\n  },\n  {\n    field: 'action',\n    fixed: 'right',\n    slots: { default: 'action' },\n    title: '操作',\n    resizable: false,\n    width: 'auto',\n  },\n];\n\nexport const formSchema: () => VbenFormSchema[] = () => [\n  {\n    label: '主键',\n    fieldName: 'id',\n    component: 'Input',\n    dependencies: {\n      show: () => false,\n      triggerFields: [''],\n    },\n  },\n  {\n    label: '流程类型',\n    fieldName: 'flowType',\n    component: 'Select',\n    help: '这里仅仅为了发起流程方便, 实际不应该包含此字段',\n    componentProps: {\n      options: leaveFlowOptions,\n      getPopupContainer,\n    },\n    defaultValue: 'leave1',\n    rules: 'selectRequired',\n    dependencies: {\n      triggerFields: [''],\n    },\n  },\n  {\n    label: '发起类型',\n    fieldName: 'type',\n    component: 'Select',\n    help: '这里仅仅为了测试, 实际不应该包含此字段',\n    componentProps: {\n      options: [\n        {\n          label: '前端发起 (可选审批人, 选抄送人, 上传附件)',\n          value: 'frontend',\n        },\n        {\n          label: '后端发起 (自行编写后端逻辑, 由后端发起流程)',\n          value: 'backend',\n        },\n      ],\n      getPopupContainer,\n    },\n    defaultValue: 'frontend',\n  },\n  {\n    label: '请假类型',\n    fieldName: 'leaveType',\n    component: 'Select',\n    componentProps: {\n      options: leaveTypeOptions,\n      getPopupContainer,\n    },\n    rules: 'selectRequired',\n  },\n  {\n    label: '开始时间',\n    fieldName: 'dateRange',\n    component: 'RangePicker',\n    componentProps(model) {\n      return {\n        format: 'YYYY-MM-DD',\n        valueFormat: 'YYYY-MM-DD HH:mm:ss',\n        onChange: (dates: [string, string]) => {\n          if (!dates) {\n            model.leaveDays = null;\n            return;\n          }\n          const [start, end] = dates;\n          const leaveDays = dayjs(end).diff(dayjs(start), 'day') + 1;\n          model.leaveDays = leaveDays;\n        },\n      };\n    },\n    rules: 'required',\n  },\n  {\n    label: '请假天数',\n    fieldName: 'leaveDays',\n    component: 'Input',\n    componentProps: {\n      disabled: true,\n    },\n    // rules: 'required',\n  },\n  {\n    label: '请假原因',\n    fieldName: 'remark',\n    component: 'Textarea',\n  },\n];\n"
  },
  {
    "path": "apps/web-antd/src/views/workflow/leave/hook.ts",
    "content": "import { onActivated, onMounted, ref } from 'vue';\n\nimport { createGlobalState } from '@vueuse/core';\n\nexport function useRouteIdEdit(callback: (id: string) => void, timeout = 500) {\n  const { businessId } = useQueryId();\n  function openEditFromRouteId() {\n    const id = businessId.value;\n    if (!id) {\n      return;\n    }\n    setTimeout(() => {\n      // 回调\n      callback?.(id);\n      // 执行完 清理id\n      businessId.value = '';\n    }, timeout);\n  }\n\n  onMounted(openEditFromRouteId);\n  onActivated(openEditFromRouteId);\n}\n\n/**\n * 用来存储业务ID 传值\n */\nexport const useQueryId = createGlobalState(() => {\n  const businessId = ref('');\n\n  return {\n    businessId,\n  };\n});\n"
  },
  {
    "path": "apps/web-antd/src/views/workflow/leave/index.vue",
    "content": "<script setup lang=\"ts\">\nimport type { VbenFormProps } from '@vben/common-ui';\n\nimport type { LeaveForm } from './api/model';\n\nimport type { VxeGridProps } from '#/adapter/vxe-table';\n\nimport { Page, useVbenDrawer, useVbenModal } from '@vben/common-ui';\nimport { getVxePopupContainer } from '@vben/utils';\n\nimport { Modal, Popconfirm, Space } from 'ant-design-vue';\n\nimport { useVbenVxeGrid, vxeCheckboxChecked } from '#/adapter/vxe-table';\nimport { cancelProcessApply } from '#/api/workflow/instance';\nimport { commonDownloadExcel } from '#/utils/file/download';\n\nimport { applyModal, flowInfoModal } from '../components';\nimport { leaveExport, leaveList, leaveRemove } from './api';\nimport { columns, querySchema } from './data';\nimport { useRouteIdEdit } from './hook';\nimport leaveDrawer from './leave-drawer.vue';\n\nconst formOptions: VbenFormProps = {\n  commonConfig: {\n    labelWidth: 80,\n    componentProps: {\n      allowClear: true,\n    },\n  },\n  schema: querySchema(),\n  wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4',\n};\n\nconst gridOptions: VxeGridProps = {\n  checkboxConfig: {\n    // 高亮\n    highlight: true,\n    // 翻页时保留选中状态\n    reserve: true,\n    // 选中 需要根据状态判断\n    checkMethod: ({ row }) => ['back', 'cancel', 'draft'].includes(row.status),\n  },\n  columns,\n  height: 'auto',\n  keepSource: true,\n  pagerConfig: {},\n  proxyConfig: {\n    ajax: {\n      query: async ({ page }, formValues = {}) => {\n        return await leaveList({\n          pageNum: page.currentPage,\n          pageSize: page.pageSize,\n          ...formValues,\n        });\n      },\n    },\n  },\n  rowConfig: {\n    keyField: 'id',\n  },\n  // 表格全局唯一表示 保存列配置需要用到\n  id: 'workflow-leave-index',\n  cellClassName: ({ row }) => {\n    // 草稿状态 可点击\n    if (row.status !== 'draft') {\n      return 'cursor-pointer';\n    }\n  },\n};\n\nconst [BasicTable, tableApi] = useVbenVxeGrid({\n  formOptions,\n  gridOptions,\n  gridEvents: {\n    cellClick: ({ row, column }) => {\n      // 草稿状态 不做处理\n      // 操作列 不做处理\n      if (row.status === 'draft' || column.field === 'action') {\n        return;\n      }\n      // 查看详情\n      handleInfo(row);\n    },\n  },\n});\n\nconst [ApplyModal, applyModalApi] = useVbenModal({\n  connectedComponent: applyModal,\n});\nconst [LeaveDrawer, leaveDrawerApi] = useVbenDrawer({\n  connectedComponent: leaveDrawer,\n});\n\nfunction handleAdd() {\n  leaveDrawerApi.setData({ applyModalApi }).open();\n}\n\nasync function handleEdit(row: Required<LeaveForm>) {\n  leaveDrawerApi.setData({ id: row.id, applyModalApi }).open();\n}\n\nuseRouteIdEdit((id) => {\n  // 打开编辑\n  leaveDrawerApi.setData({ id, applyModalApi }).open();\n});\n\nasync function handleCompleteOrCancel() {\n  leaveDrawerApi.close();\n  tableApi.query();\n}\n\nasync function handleDelete(row: Required<LeaveForm>) {\n  await leaveRemove(row.id);\n  await tableApi.query();\n}\n\nasync function handleRevoke(row: Required<LeaveForm>) {\n  await cancelProcessApply({\n    businessId: row.id,\n    message: '申请人撤销流程！',\n  });\n  await tableApi.query();\n}\n\nfunction handleMultiDelete() {\n  const rows = tableApi.grid.getCheckboxRecords();\n  const ids = rows.map((row: Required<LeaveForm>) => row.id);\n  Modal.confirm({\n    title: '提示',\n    okType: 'danger',\n    content: `确认删除选中的${ids.length}条记录吗？`,\n    onOk: async () => {\n      await leaveRemove(ids);\n      await tableApi.query();\n    },\n  });\n}\n\nfunction handleDownloadExcel() {\n  commonDownloadExcel(\n    leaveExport,\n    '请假申请数据',\n    tableApi.formApi.form.values,\n    {\n      fieldMappingTime: formOptions.fieldMappingTime,\n    },\n  );\n}\nconst [FlowInfoModal, flowInfoModalApi] = useVbenModal({\n  connectedComponent: flowInfoModal,\n});\n\nfunction handleInfo(row: Required<LeaveForm>) {\n  flowInfoModalApi.setData({ businessId: row.id });\n  flowInfoModalApi.open();\n}\n</script>\n\n<template>\n  <Page :auto-content-height=\"true\">\n    <BasicTable table-title=\"请假申请列表\">\n      <template #toolbar-tools>\n        <Space>\n          <a-button\n            v-access:code=\"['workflow:leave:export']\"\n            @click=\"handleDownloadExcel\"\n          >\n            {{ $t('pages.common.export') }}\n          </a-button>\n          <a-button\n            :disabled=\"!vxeCheckboxChecked(tableApi)\"\n            danger\n            type=\"primary\"\n            v-access:code=\"['workflow:leave:remove']\"\n            @click=\"handleMultiDelete\"\n          >\n            {{ $t('pages.common.delete') }}\n          </a-button>\n          <a-button\n            type=\"primary\"\n            v-access:code=\"['workflow:leave:add']\"\n            @click=\"handleAdd\"\n          >\n            {{ $t('pages.common.add') }}\n          </a-button>\n        </Space>\n      </template>\n      <template #action=\"{ row }\">\n        <a-button\n          size=\"small\"\n          type=\"link\"\n          :disabled=\"!['draft', 'cancel', 'back'].includes(row.status)\"\n          v-access:code=\"['workflow:leave:edit']\"\n          @click.stop=\"handleEdit(row)\"\n        >\n          {{ $t('pages.common.edit') }}\n        </a-button>\n        <Popconfirm\n          :get-popup-container=\"getVxePopupContainer\"\n          placement=\"left\"\n          title=\"确认撤销？\"\n          :disabled=\"!['waiting'].includes(row.status)\"\n          @confirm.stop=\"handleRevoke(row)\"\n          @cancel.stop=\"\"\n        >\n          <a-button\n            size=\"small\"\n            type=\"link\"\n            :disabled=\"!['waiting'].includes(row.status)\"\n            v-access:code=\"['workflow:leave:edit']\"\n            @click.stop=\"\"\n          >\n            撤销\n          </a-button>\n        </Popconfirm>\n        <Popconfirm\n          :get-popup-container=\"getVxePopupContainer\"\n          placement=\"left\"\n          title=\"确认删除？\"\n          :disabled=\"!['draft', 'cancel', 'back'].includes(row.status)\"\n          @confirm.stop=\"handleDelete(row)\"\n          @cancel.stop=\"\"\n        >\n          <a-button\n            size=\"small\"\n            type=\"link\"\n            :disabled=\"!['draft', 'cancel', 'back'].includes(row.status)\"\n            danger\n            v-access:code=\"['workflow:leave:remove']\"\n            @click.stop=\"\"\n          >\n            {{ $t('pages.common.delete') }}\n          </a-button>\n        </Popconfirm>\n      </template>\n    </BasicTable>\n    <FlowInfoModal />\n    <ApplyModal\n      @complete=\"handleCompleteOrCancel\"\n      @cancel=\"handleCompleteOrCancel\"\n    />\n    <LeaveDrawer @reload=\"() => tableApi.query()\" />\n  </Page>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/workflow/leave/leave-description.vue",
    "content": "<script setup lang=\"ts\">\nimport type { LeaveVO } from '../leave/api/model';\n\nimport { computed, onMounted, shallowRef } from 'vue';\n\nimport { Descriptions, DescriptionsItem, Skeleton } from 'ant-design-vue';\nimport dayjs from 'dayjs';\n\nimport { leaveInfo } from './api';\nimport { leaveTypeOptions } from './data';\n\ndefineOptions({\n  name: 'LeaveDescription',\n  inheritAttrs: false,\n});\n\nconst props = defineProps<{ businessId: number | string }>();\n\nconst data = shallowRef<LeaveVO>();\nonMounted(async () => {\n  const resp = await leaveInfo(props.businessId);\n  data.value = resp;\n});\n\nconst leaveType = computed(() => {\n  return (\n    leaveTypeOptions.find((item) => item.value === data.value?.leaveType)\n      ?.label ?? '未知'\n  );\n});\n\nfunction formatDate(date: string) {\n  return dayjs(date).format('YYYY-MM-DD');\n}\n</script>\n\n<template>\n  <div class=\"rounded-[6px] border p-2\">\n    <Descriptions v-if=\"data\" :column=\"1\" size=\"middle\">\n      <DescriptionsItem label=\"请假类型\">\n        {{ leaveType }}\n      </DescriptionsItem>\n      <DescriptionsItem label=\"请假时间\">\n        {{ formatDate(data.startDate) }} - {{ formatDate(data.endDate) }}\n      </DescriptionsItem>\n      <DescriptionsItem label=\"请假时长\">\n        {{ data.leaveDays }}天\n      </DescriptionsItem>\n      <DescriptionsItem label=\"请假原因\">\n        {{ data.remark || '无' }}\n      </DescriptionsItem>\n    </Descriptions>\n\n    <Skeleton active v-else />\n  </div>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/workflow/leave/leave-drawer.vue",
    "content": "<script setup lang=\"ts\">\nimport type { ExtendedModalApi } from '@vben/common-ui';\n\nimport type { StartWorkFlowReqData } from '#/api/workflow/task/model';\n\nimport { computed, ref, shallowRef } from 'vue';\n\nimport { useVbenDrawer } from '@vben/common-ui';\nimport { $t } from '@vben/locales';\nimport { cloneDeep } from '@vben/utils';\n\nimport dayjs from 'dayjs';\nimport { omit } from 'lodash-es';\n\nimport { useVbenForm } from '#/adapter/form';\nimport { startWorkFlow } from '#/api/workflow/task';\n\nimport {\n  leaveAdd,\n  leaveInfo,\n  leaveUpdate,\n  submitAndStartWorkflow,\n} from './api';\nimport { formSchema } from './data';\n\nconst emit = defineEmits<{ reload: [] }>();\n\nconst isUpdate = ref(false);\nconst title = computed(() => {\n  return isUpdate.value ? $t('pages.common.edit') : $t('pages.common.add');\n});\n\nconst [BasicForm, formApi] = useVbenForm({\n  layout: 'vertical',\n  commonConfig: {\n    formItemClass: 'col-span-2',\n    componentProps: {\n      class: 'w-full',\n    },\n    labelWidth: 100,\n  },\n  schema: formSchema(),\n  showDefaultActions: false,\n  wrapperClass: 'grid-cols-2',\n});\n\nconst modalApi = shallowRef<ExtendedModalApi | null>(null);\n\nconst [BasicDrawer, drawerApi] = useVbenDrawer({\n  closeOnClickModal: false,\n  onClosed: handleClosed,\n  onConfirm: handleStartWorkFlow,\n  async onOpenChange(isOpen) {\n    if (!isOpen) {\n      return null;\n    }\n    drawerApi.drawerLoading(true);\n\n    const { id, applyModalApi } = drawerApi.getData() as {\n      applyModalApi: ExtendedModalApi;\n      id?: number | string;\n    };\n    modalApi.value = applyModalApi;\n    isUpdate.value = !!id;\n    // 赋值\n    if (isUpdate.value && id) {\n      const resp = await leaveInfo(id);\n      await formApi.setValues(resp);\n      const dateRange = [dayjs(resp.startDate), dayjs(resp.endDate)];\n      await formApi.setFieldValue('dateRange', dateRange);\n    }\n\n    drawerApi.drawerLoading(false);\n  },\n});\n\nasync function handleClosed() {\n  await formApi.resetForm();\n}\n\n/**\n * 获取已经处理好的表单参数\n */\nasync function getFormData() {\n  const { valid } = await formApi.validate();\n  if (!valid) {\n    throw new Error('表单验证失败');\n  }\n  let data = cloneDeep(await formApi.getValues()) as any;\n  data = omit(data, 'flowType', 'type');\n  // 处理日期\n  data.startDate = dayjs(data.dateRange[0]).format('YYYY-MM-DD HH:mm:ss');\n  data.endDate = dayjs(data.dateRange[1]).format('YYYY-MM-DD HH:mm:ss');\n  return data;\n}\n\n/**\n * 暂存/提交 提取通用逻辑\n */\nasync function handleSaveOrUpdate() {\n  const data = await getFormData();\n  return await (isUpdate.value ? leaveUpdate(data) : leaveAdd(data));\n}\n\n/**\n * 暂存 草稿状态\n */\nasync function handleTempSave() {\n  try {\n    await handleSaveOrUpdate();\n    emit('reload');\n    drawerApi.close();\n  } catch (error) {\n    console.error(error);\n  }\n}\n\n/**\n * 保存业务 & 发起流程\n */\nasync function handleStartWorkFlow() {\n  drawerApi.lock(true);\n  try {\n    const { valid } = await formApi.validate();\n    if (!valid) {\n      return;\n    }\n    // 获取发起类型\n    const { type } = await formApi.getValues();\n    /**\n     * 这里只是demo 实际只会用到一种\n     */\n    switch (type) {\n      // 后端发起流程\n      case 'backend': {\n        const data = await getFormData();\n        await submitAndStartWorkflow(data);\n        emit('reload');\n        drawerApi.close();\n        break;\n      }\n      // 前端发起流程\n      case 'frontend': {\n        // 保存业务\n        const leaveResp = await handleSaveOrUpdate();\n        // 启动流程\n        const taskVariables = {\n          leaveDays: leaveResp!.leaveDays,\n          userList: ['1', '3', '4'],\n        };\n        const formValues = await formApi.getValues();\n        const flowCode = formValues?.flowType ?? 'leave1';\n        const startWorkFlowData: StartWorkFlowReqData = {\n          businessId: leaveResp!.id,\n          flowCode,\n          variables: taskVariables,\n          flowInstanceBizExtBo: {\n            businessTitle: '请假申请 - 自定义标题',\n            businessCode: leaveResp!.applyCode,\n          },\n        };\n        const { taskId } = await startWorkFlow(startWorkFlowData);\n        // 打开窗口\n        modalApi.value?.setData({\n          taskId,\n          taskVariables,\n          variables: {},\n        });\n        modalApi.value?.open();\n        break;\n      }\n    }\n    emit('reload');\n    drawerApi.close();\n  } catch (error) {\n    console.error(error);\n  } finally {\n    drawerApi.lock(false);\n  }\n}\n</script>\n\n<template>\n  <BasicDrawer :title=\"title\" class=\"w-[600px]\">\n    <BasicForm />\n    <template #center-footer>\n      <a-button @click=\"handleTempSave\">暂存</a-button>\n    </template>\n  </BasicDrawer>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/workflow/leave/leave-form.vue",
    "content": "<!--\n这个文件用不上  已经更改交互为drawer\n-->\n\n<script setup lang=\"ts\">\nimport type { StartWorkFlowReqData } from '#/api/workflow/task/model';\n\nimport { onMounted, ref } from 'vue';\nimport { useRoute, useRouter } from 'vue-router';\n\nimport { useVbenModal } from '@vben/common-ui';\nimport { useTabs } from '@vben/hooks';\n\nimport { Card, Spin } from 'ant-design-vue';\nimport dayjs from 'dayjs';\nimport { cloneDeep, omit } from 'lodash-es';\n\nimport { useVbenForm } from '#/adapter/form';\nimport { startWorkFlow } from '#/api/workflow/task';\n\nimport { applyModal } from '../components';\nimport {\n  leaveAdd,\n  leaveInfo,\n  leaveUpdate,\n  submitAndStartWorkflow,\n} from './api';\nimport { formSchema } from './data';\n\nconst route = useRoute();\nconst id = route.query?.id as string;\n\nconst [BasicForm, formApi] = useVbenForm({\n  commonConfig: {\n    // 默认占满两列\n    formItemClass: 'col-span-2',\n    // 默认label宽度 px\n    labelWidth: 100,\n    // 通用配置项 会影响到所有表单项\n    componentProps: {\n      class: 'w-full',\n    },\n  },\n  schema: formSchema(),\n  showDefaultActions: false,\n  wrapperClass: 'grid-cols-2',\n});\n\nconst loading = ref(false);\nonMounted(async () => {\n  // 只读 获取信息赋值\n  if (id) {\n    loading.value = true;\n\n    const resp = await leaveInfo(id);\n    await formApi.setValues(resp);\n    const dateRange = [dayjs(resp.startDate), dayjs(resp.endDate)];\n    await formApi.setFieldValue('dateRange', dateRange);\n\n    loading.value = false;\n  }\n});\n\nconst router = useRouter();\n\n/**\n * 获取已经处理好的表单参数\n */\nasync function getFormData() {\n  let data = cloneDeep(await formApi.getValues()) as any;\n  data = omit(data, 'flowType', 'type');\n  // 处理日期\n  data.startDate = dayjs(data.dateRange[0]).format('YYYY-MM-DD HH:mm:ss');\n  data.endDate = dayjs(data.dateRange[1]).format('YYYY-MM-DD HH:mm:ss');\n  return data;\n}\n\n/**\n * 暂存/提交 提取通用逻辑\n */\nasync function handleSaveOrUpdate() {\n  const data = await getFormData();\n  if (id) {\n    data.id = id;\n    return await leaveUpdate(data);\n  } else {\n    return await leaveAdd(data);\n  }\n}\n\nconst [ApplyModal, applyModalApi] = useVbenModal({\n  connectedComponent: applyModal,\n});\n/**\n * 暂存 草稿状态\n */\nasync function handleTempSave() {\n  try {\n    await handleSaveOrUpdate();\n    router.push('/demo/leave');\n  } catch (error) {\n    console.error(error);\n  }\n}\n\n/**\n * 保存业务 & 发起流程\n */\nasync function handleStartWorkFlow() {\n  loading.value = true;\n  try {\n    const { valid } = await formApi.validate();\n    if (!valid) {\n      return;\n    }\n    // 获取发起类型\n    const { type } = await formApi.getValues();\n    /**\n     * 这里只是demo 实际只会用到一种\n     */\n    switch (type) {\n      // 后端发起流程\n      case 'backend': {\n        const data = await getFormData();\n        await submitAndStartWorkflow(data);\n        await handleCompleteOrCancel();\n        break;\n      }\n      // 前端发起流程\n      case 'frontend': {\n        // 保存业务\n        const leaveResp = await handleSaveOrUpdate();\n        // 启动流程\n        const taskVariables = {\n          leaveDays: leaveResp!.leaveDays,\n          userList: ['1', '3', '4'],\n        };\n        const formValues = await formApi.getValues();\n        const flowCode = formValues?.flowType ?? 'leave1';\n        const startWorkFlowData: StartWorkFlowReqData = {\n          businessId: leaveResp!.id,\n          flowCode,\n          variables: taskVariables,\n          flowInstanceBizExtBo: {\n            businessTitle: '请假申请 - 自定义标题',\n            businessCode: leaveResp!.applyCode,\n          },\n        };\n        const { taskId } = await startWorkFlow(startWorkFlowData);\n        // 打开窗口\n        applyModalApi.setData({\n          taskId,\n          taskVariables,\n          variables: {},\n        });\n        applyModalApi.open();\n        break;\n      }\n    }\n  } catch (error) {\n    console.error(error);\n  } finally {\n    loading.value = false;\n  }\n}\n\nconst { closeCurrentTab } = useTabs();\n\n/**\n * 通用提交/取消回调\n *\n * 提交后点击取消 这时候已经变成草稿状态了\n * 每次点击都会生成新记录 直接跳转回列表\n */\nasync function handleCompleteOrCancel() {\n  formApi.resetForm();\n  await closeCurrentTab();\n  router.push('/demo/leave');\n}\n</script>\n\n<template>\n  <Spin :spinning=\"loading\">\n    <Card>\n      <BasicForm />\n      <div class=\"flex justify-end gap-2\">\n        <a-button @click=\"handleTempSave\">暂存</a-button>\n        <a-button type=\"primary\" @click=\"handleStartWorkFlow\">提交</a-button>\n      </div>\n      <ApplyModal\n        :modal-api=\"applyModalApi\"\n        @complete=\"handleCompleteOrCancel\"\n        @cancel=\"handleCompleteOrCancel\"\n      />\n    </Card>\n  </Spin>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/workflow/leave/leaveEdit.vue",
    "content": "<!--\n后端版本>=5.4.0  这个从本地路由变为从后台返回\n未修改文件名 而是新加了这个文件\n-->\n<script setup lang=\"ts\">\nimport { onMounted } from 'vue';\nimport { useRoute, useRouter } from 'vue-router';\n\nimport { useTabs } from '@vben/hooks';\n\nimport { Spin } from 'ant-design-vue';\n\nimport { useQueryId } from './hook';\n\nconst router = useRouter();\nconst route = useRoute();\nconst id = route.query.id as string;\n\n/**\n * 从我的任务 -> 点击重新编辑会跳转到这里\n * 相当于一个中转 因为我的任务无法获取到列表页的路径(与ele交互不同)\n *\n * 为什么不使用路由的query来实现?\n * 因为刷新后参数不会丢失 且tab存的也是全路径 切换也不会丢失 这不符合预期\n * 可以通过window.history.replaceState来删除query参数 但是tab切换还是会保留\n */\nconst { closeCurrentTab } = useTabs();\nconst { businessId } = useQueryId();\nonMounted(async () => {\n  await closeCurrentTab();\n  if (id) {\n    // 设置业务ID 存储在内存\n    businessId.value = id;\n    router.push({ path: '/demo/leave' });\n  }\n});\n</script>\n\n<template>\n  <div>\n    <Spin :spinning=\"true\" />\n  </div>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/workflow/processDefinition/category-tree.vue",
    "content": "<script setup lang=\"ts\">\nimport type { PropType } from 'vue';\n\nimport type { CategoryTree } from '#/api/workflow/category/model';\n\nimport { onMounted, ref } from 'vue';\n\nimport { SyncOutlined } from '@ant-design/icons-vue';\nimport { InputSearch, Skeleton, Tree } from 'ant-design-vue';\n\nimport { categoryTree } from '#/api/workflow/category';\n\ndefineOptions({ inheritAttrs: false });\n\nconst emit = defineEmits<{\n  /**\n   * 点击刷新按钮的事件\n   */\n  reload: [];\n  /**\n   * 点击节点的事件\n   */\n  select: [];\n}>();\n\nconst selectCode = defineModel('selectCode', {\n  required: true,\n  type: Array as PropType<number[] | string[]>,\n});\n\nconst searchValue = defineModel('searchValue', {\n  type: String,\n  default: '',\n});\n\nconst categoryTreeArray = ref<CategoryTree[]>([]);\n/** 骨架屏加载 */\nconst showTreeSkeleton = ref<boolean>(true);\n\nasync function loadTree() {\n  showTreeSkeleton.value = true;\n  searchValue.value = '';\n  selectCode.value = [];\n\n  const treeData = await categoryTree();\n\n  categoryTreeArray.value = treeData;\n  showTreeSkeleton.value = false;\n}\n\nasync function handleReload() {\n  await loadTree();\n  emit('reload');\n}\n\nonMounted(loadTree);\n</script>\n\n<template>\n  <div :class=\"$attrs.class\">\n    <Skeleton\n      :loading=\"showTreeSkeleton\"\n      :paragraph=\"{ rows: 8 }\"\n      active\n      class=\"p-[8px]\"\n    >\n      <div\n        class=\"bg-background flex h-full flex-col overflow-y-auto rounded-lg\"\n      >\n        <!-- 固定在顶部 必须加上bg-background背景色 否则会产生'穿透'效果 -->\n        <div class=\"bg-background z-100 sticky left-0 top-0 p-[8px]\">\n          <InputSearch\n            v-model:value=\"searchValue\"\n            :placeholder=\"$t('pages.common.search')\"\n            size=\"small\"\n            allow-clear\n          >\n            <template #enterButton>\n              <a-button @click=\"handleReload\">\n                <SyncOutlined class=\"text-primary\" />\n              </a-button>\n            </template>\n          </InputSearch>\n        </div>\n        <div class=\"h-full overflow-x-hidden px-[8px]\">\n          <Tree\n            v-bind=\"$attrs\"\n            v-if=\"categoryTreeArray.length > 0\"\n            v-model:selected-keys=\"selectCode\"\n            :class=\"$attrs.class\"\n            :field-names=\"{ title: 'label', key: 'id' }\"\n            :show-line=\"{ showLeafIcon: false }\"\n            :tree-data=\"categoryTreeArray\"\n            :virtual=\"false\"\n            default-expand-all\n            @select=\"$emit('select')\"\n          >\n            <template #title=\"{ label }\">\n              <span v-if=\"label.includes(searchValue)\">\n                {{ label.substring(0, label.indexOf(searchValue)) }}\n                <span class=\"text-primary\">{{ searchValue }}</span>\n                {{\n                  label.substring(\n                    label.indexOf(searchValue) + searchValue.length,\n                  )\n                }}\n              </span>\n              <span v-else>{{ label }}</span>\n            </template>\n          </Tree>\n        </div>\n      </div>\n    </Skeleton>\n  </div>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/workflow/processDefinition/constant.ts",
    "content": "import { optionsToEnum } from '@vben/utils';\n\nexport const activityStatusOptions = [\n  {\n    label: '激活',\n    value: 1,\n    color: 'success',\n    enumName: 'Active',\n  },\n  {\n    label: '挂起',\n    value: 0,\n    color: 'error',\n    enumName: 'Suspended',\n  },\n] as const;\n\nexport const ActivityStatusEnum = optionsToEnum(activityStatusOptions);\n\nexport const publishStatusOptions = [\n  {\n    label: '已发布',\n    value: 1,\n    color: 'success',\n  },\n  {\n    label: '未发布',\n    value: 0,\n    color: 'warning',\n  },\n  {\n    label: '失效',\n    value: 9,\n    color: 'error',\n  },\n];\n"
  },
  {
    "path": "apps/web-antd/src/views/workflow/processDefinition/data.tsx",
    "content": "import type { FormSchemaGetter } from '#/adapter/form';\nimport type { VxeGridProps } from '#/adapter/vxe-table';\n\nimport { OptionsTag } from '#/components/table';\n\nimport { publishStatusOptions } from './constant';\n\nexport const designerModeOptions = [\n  {\n    label: '经典模式',\n    value: 'CLASSICS',\n  },\n  {\n    label: '仿钉钉模式',\n    value: 'MIMIC',\n  },\n];\n\nexport const querySchema: FormSchemaGetter = () => [\n  {\n    component: 'Input',\n    fieldName: 'flowName',\n    label: '流程名称',\n  },\n  {\n    component: 'Input',\n    fieldName: 'flowCode',\n    label: '流程code',\n  },\n];\n\nexport const columns: VxeGridProps['columns'] = [\n  { type: 'checkbox', width: 60 },\n  {\n    field: 'flowName',\n    title: '流程名称',\n    minWidth: 150,\n  },\n  {\n    field: 'flowCode',\n    title: '流程code',\n    minWidth: 150,\n  },\n  // {\n  //   field: 'modelValue',\n  //   title: '设计器模式',\n  //   minWidth: 150,\n  // },\n  {\n    field: 'version',\n    title: '版本号',\n    minWidth: 80,\n    formatter: ({ cellValue }) => `V${cellValue}.0`,\n  },\n  {\n    field: 'activityStatus',\n    title: '激活状态',\n    minWidth: 100,\n    slots: {\n      default: 'activityStatus',\n    },\n  },\n  {\n    field: 'isPublish',\n    title: '发布状态',\n    minWidth: 100,\n    slots: {\n      default: ({ row }) => {\n        const cellValue = row.isPublish;\n        return (\n          <OptionsTag options={publishStatusOptions as any} value={cellValue} />\n        );\n      },\n    },\n  },\n  {\n    field: 'action',\n    fixed: 'right',\n    slots: { default: 'action' },\n    title: '操作',\n    resizable: false,\n    width: 'auto',\n  },\n];\n\nexport const modalSchema: FormSchemaGetter = () => [\n  {\n    component: 'Input',\n    dependencies: {\n      show: () => false,\n      triggerFields: [''],\n    },\n    fieldName: 'id',\n  },\n  {\n    component: 'TreeSelect',\n    fieldName: 'category',\n    label: '流程分类',\n    rules: 'selectRequired',\n  },\n  {\n    component: 'Input',\n    fieldName: 'flowCode',\n    label: '流程code',\n    rules: 'required',\n  },\n  {\n    component: 'Input',\n    fieldName: 'flowName',\n    label: '流程名称',\n    rules: 'required',\n  },\n  {\n    component: 'RadioGroup',\n    fieldName: 'modelValue',\n    label: '设计器模式',\n    componentProps: {\n      options: designerModeOptions,\n      optionType: 'button',\n      buttonStyle: 'solid',\n    },\n    defaultValue: 'CLASSICS',\n    rules: 'selectRequired',\n  },\n  {\n    component: 'Input',\n    fieldName: 'formPath',\n    label: '表单路径',\n    rules: 'required',\n  },\n];\n"
  },
  {
    "path": "apps/web-antd/src/views/workflow/processDefinition/design.vue",
    "content": "<!--\n后端版本>=5.4.0  这个从本地路由变为从后台返回\n未修改文件名 而是新加了这个文件\n-->\n<script setup lang=\"ts\">\nimport FlowDesignerPage from '../components/flow-designer.vue';\n</script>\n\n<template>\n  <FlowDesignerPage />\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/workflow/processDefinition/index.vue",
    "content": "<!-- eslint-disable no-use-before-define -->\n<script setup lang=\"ts\">\nimport type { RadioChangeEvent } from 'ant-design-vue';\n\nimport type { VbenFormProps } from '@vben/common-ui';\nimport type { Recordable } from '@vben/types';\n\nimport type { VxeGridProps } from '#/adapter/vxe-table';\n\nimport { computed, ref } from 'vue';\nimport { useRouter } from 'vue-router';\n\nimport { Page, useVbenModal } from '@vben/common-ui';\nimport { $t } from '@vben/locales';\nimport { getVxePopupContainer } from '@vben/utils';\n\nimport {\n  message,\n  Modal,\n  Popconfirm,\n  RadioGroup,\n  Space,\n  Switch,\n} from 'ant-design-vue';\n\nimport { useVbenVxeGrid, vxeCheckboxChecked } from '#/adapter/vxe-table';\nimport {\n  unPublishList,\n  workflowDefinitionActive,\n  workflowDefinitionCopy,\n  workflowDefinitionDelete,\n  workflowDefinitionExport,\n  workflowDefinitionList,\n  workflowDefinitionPublish,\n} from '#/api/workflow/definition';\nimport { downloadByData } from '#/utils/file/download';\n\nimport CategoryTree from './category-tree.vue';\nimport { columns, querySchema } from './data';\nimport processDefinitionDeployModal from './process-definition-deploy-modal.vue';\nimport processDefinitionModal from './process-definition-modal.vue';\n\n// 左边部门用\nconst selectedCode = ref<number[] | string[]>([]);\n\nconst formOptions: VbenFormProps = {\n  schema: querySchema(),\n  commonConfig: {\n    labelWidth: 80,\n    componentProps: {\n      allowClear: true,\n    },\n  },\n  wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4',\n  handleReset: async () => {\n    selectedCode.value = [];\n    const { formApi, reload } = tableApi;\n    await formApi.resetForm();\n    const formValues = formApi.form.values;\n    formApi.setLatestSubmissionValues(formValues);\n    await reload(formValues);\n  },\n};\n\nconst gridOptions: VxeGridProps = {\n  checkboxConfig: {\n    // 高亮\n    highlight: true,\n    // 翻页时保留选中状态\n    reserve: true,\n    // 点击行选中\n    trigger: 'default',\n  },\n  columns,\n  height: 'auto',\n  keepSource: true,\n  pagerConfig: {},\n  proxyConfig: {\n    ajax: {\n      query: async ({ page }, formValues = {}) => {\n        // 部门树选择处理\n        if (selectedCode.value.length === 1) {\n          formValues.category = selectedCode.value[0];\n        } else {\n          Reflect.deleteProperty(formValues, 'category');\n        }\n\n        return await currentTableApi.value({\n          pageNum: page.currentPage,\n          pageSize: page.pageSize,\n          ...formValues,\n        });\n      },\n    },\n  },\n  headerCellConfig: {\n    height: 44,\n  },\n  cellConfig: {\n    height: 100,\n  },\n  rowConfig: {\n    keyField: 'id',\n  },\n  id: 'workflow-definition-index',\n};\n\nconst [BasicTable, tableApi] = useVbenVxeGrid({\n  formOptions,\n  gridOptions,\n});\n\n// 左边的切换\nconst statusOptions = [\n  { label: '已发布流程', value: 1 },\n  { label: '未发布流程', value: 0 },\n];\nconst currentStatus = ref(1);\nconst currentTableApi = computed(() => {\n  if (currentStatus.value === 1) {\n    return workflowDefinitionList;\n  }\n  return unPublishList;\n});\nasync function handleStatusChange(e: RadioChangeEvent) {\n  currentStatus.value = e.target.value as number;\n  await tableApi.reload();\n}\n\nasync function handleDelete(row: Recordable<any>) {\n  await workflowDefinitionDelete(row.id);\n  await tableApi.query();\n}\n\nfunction handleMultiDelete() {\n  const rows = tableApi.grid.getCheckboxRecords();\n  const ids = rows.map((row: any) => row.id);\n  Modal.confirm({\n    title: '提示',\n    okType: 'danger',\n    content: `确认删除选中的${ids.length}条记录吗？`,\n    onOk: async () => {\n      await workflowDefinitionDelete(ids);\n      await tableApi.query();\n    },\n  });\n}\n\nconst router = useRouter();\n/**\n * 流程设计/预览\n * @param row row\n * @param _disabled true为预览，false为设计\n */\nfunction handleDesign(row: any, _disabled: boolean) {\n  router.push({\n    path: '/workflow/design/index',\n    query: { definitionId: row.id },\n  });\n}\n\n/**\n * 激活/挂起流程\n * @param row row\n */\nasync function handleActive(row: any, status: boolean | number | string) {\n  const lastStatus = status === 1 ? 0 : 1;\n  try {\n    await workflowDefinitionActive(row.id, !!status);\n    await tableApi.query();\n  } catch (error) {\n    row.activityStatus = lastStatus;\n    console.error(error);\n  }\n}\n\n/**\n * 发布流程\n * @param row row\n */\nasync function handlePublish(row: any) {\n  await workflowDefinitionPublish(row.id);\n  await tableApi.query();\n}\n\n/**\n * 复制流程\n * @param row row\n */\nasync function handleCopy(row: any) {\n  await workflowDefinitionCopy(row.id);\n  // 跳转到未发布流程tab\n  currentStatus.value = 0;\n  await tableApi.reload();\n}\n\nconst [ProcessDefinitionModal, modalApi] = useVbenModal({\n  connectedComponent: processDefinitionModal,\n});\n\n/**\n * 新增流程\n */\nfunction handleAdd() {\n  modalApi.setData({});\n  modalApi.open();\n}\n\n/**\n * 编辑流程\n */\nfunction handleEdit(row: any) {\n  modalApi.setData({ id: row.id });\n  modalApi.open();\n}\n\n/**\n * 导出xml\n * @param row row\n */\nasync function handleExportXml(row: any) {\n  const hideLoading = message.loading($t('pages.common.downloadLoading'), 0);\n  try {\n    const blob = await workflowDefinitionExport(row.id);\n    downloadByData(blob, `${row.flowName}-${Date.now()}.json`);\n  } catch (error) {\n    console.error(error);\n  } finally {\n    hideLoading();\n  }\n}\n\nconst [ProcessDefinitionDeployModal, deployModalApi] = useVbenModal({\n  connectedComponent: processDefinitionDeployModal,\n});\n\n/**\n * 部署流程xml\n */\nfunction handleDeploy() {\n  if (selectedCode.value.length === 0) {\n    message.warning('请先选择流程分类');\n    return;\n  }\n  const selectedCategory = selectedCode.value[0];\n  if (selectedCategory === 0) {\n    message.warning('不可选择根目录进行部署, 请选择子分类');\n    return;\n  }\n  deployModalApi.setData({ category: selectedCategory });\n  deployModalApi.open();\n}\n\n// 部署流程json\nasync function handleDeploySuccess() {\n  // 跳转到未发布\n  currentStatus.value = 0;\n  await tableApi.reload();\n}\n\n// 新增完成需要跳转到未发布\nasync function handleReload(type: 'add' | 'update') {\n  if (type === 'add') {\n    currentStatus.value = 0;\n  }\n  await tableApi.reload();\n}\n</script>\n\n<template>\n  <Page :auto-content-height=\"true\">\n    <div class=\"flex h-full gap-[8px]\">\n      <CategoryTree\n        v-model:select-code=\"selectedCode\"\n        class=\"w-[260px]\"\n        @reload=\"() => tableApi.reload()\"\n        @select=\"() => tableApi.reload()\"\n      />\n      <BasicTable class=\"flex-1 overflow-hidden\">\n        <template #toolbar-actions>\n          <RadioGroup\n            v-model:value=\"currentStatus\"\n            :options=\"statusOptions\"\n            button-style=\"solid\"\n            option-type=\"button\"\n            @change=\"handleStatusChange\"\n          />\n        </template>\n        <template #toolbar-tools>\n          <Space>\n            <a-button\n              :disabled=\"!vxeCheckboxChecked(tableApi)\"\n              danger\n              type=\"primary\"\n              v-access:code=\"['system:user:remove']\"\n              @click=\"handleMultiDelete\"\n            >\n              {{ $t('pages.common.delete') }}\n            </a-button>\n            <a-button v-access:code=\"['system:user:add']\" @click=\"handleDeploy\">\n              部署\n            </a-button>\n            <a-button\n              type=\"primary\"\n              v-access:code=\"['system:user:add']\"\n              @click=\"handleAdd\"\n            >\n              {{ $t('pages.common.add') }}\n            </a-button>\n          </Space>\n        </template>\n        <template #activityStatus=\"{ row }\">\n          <Switch\n            v-model:checked=\"row.activityStatus\"\n            :checked-value=\"1\"\n            :unchecked-value=\"0\"\n            checked-children=\"激活\"\n            un-checked-children=\"挂起\"\n            @change=\"(status) => handleActive(row, status)\"\n          />\n        </template>\n        <template #action=\"{ row }\">\n          <div class=\"flex flex-col gap-1\">\n            <div>\n              <a-button size=\"small\" type=\"link\" @click=\"handleEdit(row)\">\n                编辑信息\n              </a-button>\n              <Popconfirm\n                :get-popup-container=\"getVxePopupContainer\"\n                placement=\"left\"\n                title=\"确认删除？\"\n                @confirm=\"handleDelete(row)\"\n              >\n                <a-button danger size=\"small\" type=\"link\" @click.stop=\"\">\n                  删除流程\n                </a-button>\n              </Popconfirm>\n            </div>\n            <div>\n              <a-button\n                size=\"small\"\n                type=\"link\"\n                @click=\"handleDesign(row, !!row.isPublish)\"\n              >\n                {{ row.isPublish ? '查看流程' : '设计流程' }}\n              </a-button>\n              <Popconfirm\n                :get-popup-container=\"getVxePopupContainer\"\n                :title=\"`确认发布流程[${row.flowName}]?`\"\n                placement=\"left\"\n                @confirm=\"handlePublish(row)\"\n              >\n                <a-button v-if=\"!row.isPublish\" size=\"small\" type=\"link\">\n                  发布流程\n                </a-button>\n              </Popconfirm>\n            </div>\n            <div>\n              <Popconfirm\n                :get-popup-container=\"getVxePopupContainer\"\n                :title=\"`确认复制流程[${row.flowName}]?`\"\n                placement=\"left\"\n                @confirm=\"handleCopy(row)\"\n              >\n                <a-button size=\"small\" type=\"link\"> 复制流程 </a-button>\n              </Popconfirm>\n              <a-button size=\"small\" type=\"link\" @click=\"handleExportXml(row)\">\n                导出流程\n              </a-button>\n            </div>\n          </div>\n        </template>\n      </BasicTable>\n    </div>\n    <ProcessDefinitionModal @reload=\"handleReload\" />\n    <ProcessDefinitionDeployModal @reload=\"handleDeploySuccess\" />\n  </Page>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/workflow/processDefinition/process-definition-deploy-modal.vue",
    "content": "<script setup lang=\"ts\">\nimport type { UploadFile } from 'ant-design-vue/es/upload/interface';\n\nimport { ref } from 'vue';\n\nimport { useVbenModal } from '@vben/common-ui';\nimport { InBoxIcon } from '@vben/icons';\n\nimport { Upload } from 'ant-design-vue';\n\nimport { workflowDefinitionImport } from '#/api/workflow/definition';\n\nconst emit = defineEmits<{ reload: [] }>();\n\nconst UploadDragger = Upload.Dragger;\n\nconst [BasicModal, modalApi] = useVbenModal({\n  onCancel: handleCancel,\n  onConfirm: handleSubmit,\n});\n\nconst fileList = ref<UploadFile[]>([]);\n\nasync function handleSubmit() {\n  try {\n    modalApi.modalLoading(true);\n    if (fileList.value.length !== 1) {\n      handleCancel();\n      return;\n    }\n    const data = {\n      file: fileList.value[0]!.originFileObj as Blob,\n      category: modalApi.getData().category,\n    };\n    await workflowDefinitionImport(data);\n    emit('reload');\n    handleCancel();\n  } catch (error) {\n    console.warn(error);\n    modalApi.close();\n  } finally {\n    modalApi.modalLoading(false);\n  }\n}\n\nfunction handleCancel() {\n  modalApi.close();\n  fileList.value = [];\n}\n</script>\n\n<template>\n  <BasicModal\n    :close-on-click-modal=\"false\"\n    :fullscreen-button=\"false\"\n    title=\"流程部署\"\n  >\n    <!-- z-index不设置会遮挡模板下载loading -->\n    <!-- 手动处理 而不是放入文件就上传 -->\n    <UploadDragger\n      v-model:file-list=\"fileList\"\n      :before-upload=\"() => false\"\n      :max-count=\"1\"\n      :show-upload-list=\"true\"\n      accept=\"application/json\"\n    >\n      <p class=\"ant-upload-drag-icon flex items-center justify-center\">\n        <InBoxIcon class=\"text-primary size-[48px]\" />\n      </p>\n      <p class=\"ant-upload-text\">点击或者拖拽到此处上传[json]文件</p>\n    </UploadDragger>\n  </BasicModal>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/workflow/processDefinition/process-definition-modal.vue",
    "content": "<script setup lang=\"ts\">\nimport { computed, ref } from 'vue';\n\nimport { useVbenModal } from '@vben/common-ui';\nimport { $t } from '@vben/locales';\nimport { addFullName, cloneDeep, getPopupContainer } from '@vben/utils';\n\nimport { useVbenForm } from '#/adapter/form';\nimport { categoryTree } from '#/api/workflow/category';\nimport {\n  workflowDefinitionAdd,\n  workflowDefinitionInfo,\n  workflowDefinitionUpdate,\n} from '#/api/workflow/definition';\nimport { defaultFormValueGetter, useBeforeCloseDiff } from '#/utils/popup';\n\nimport { modalSchema } from './data';\n\nconst emit = defineEmits<{ reload: [type: 'add' | 'update'] }>();\n\nconst isUpdate = ref(false);\nconst title = computed(() => {\n  return isUpdate.value ? $t('pages.common.edit') : $t('pages.common.add');\n});\n\nconst [BasicForm, formApi] = useVbenForm({\n  commonConfig: {\n    componentProps: {\n      class: 'w-full',\n    },\n    formItemClass: 'col-span-2',\n    labelWidth: 90,\n  },\n  schema: modalSchema(),\n  showDefaultActions: false,\n  wrapperClass: 'grid-cols-2',\n});\n\nasync function setupCategorySelect() {\n  // menu\n  const tree = await categoryTree();\n  addFullName(tree, 'label', ' / ');\n\n  formApi.updateSchema([\n    {\n      componentProps: {\n        fieldNames: {\n          label: 'label',\n          value: 'id',\n        },\n        getPopupContainer,\n        // 设置弹窗滚动高度 默认256\n        listHeight: 300,\n        showSearch: true,\n        treeData: tree,\n        treeDefaultExpandAll: true,\n        // 默认展开的树节点\n        // treeDefaultExpandedKeys: [0],\n        treeLine: { showLeafIcon: false },\n        // 筛选的字段\n        treeNodeFilterProp: 'label',\n        treeNodeLabelProp: 'fullName',\n      },\n      fieldName: 'category',\n    },\n  ]);\n}\n\nconst { onBeforeClose, markInitialized, resetInitialized } = useBeforeCloseDiff(\n  {\n    initializedGetter: defaultFormValueGetter(formApi),\n    currentGetter: defaultFormValueGetter(formApi),\n  },\n);\n\nconst [BasicDrawer, modalApi] = useVbenModal({\n  onBeforeClose,\n  onClosed: handleClosed,\n  onConfirm: handleConfirm,\n  async onOpenChange(isOpen) {\n    if (!isOpen) {\n      return null;\n    }\n    modalApi.modalLoading(true);\n\n    const { id } = modalApi.getData() as { id?: number | string };\n    isUpdate.value = !!id;\n\n    // 加载分类树选择\n    await setupCategorySelect();\n    if (isUpdate.value && id) {\n      const record = await workflowDefinitionInfo(id);\n      await formApi.setValues(record);\n    }\n    await markInitialized();\n\n    modalApi.modalLoading(false);\n  },\n});\n\nasync function handleConfirm() {\n  try {\n    modalApi.lock(true);\n    const { valid } = await formApi.validate();\n    if (!valid) {\n      return;\n    }\n    const data = cloneDeep(await formApi.getValues());\n    if (isUpdate.value) {\n      await workflowDefinitionUpdate(data);\n      emit('reload', 'update');\n    } else {\n      await workflowDefinitionAdd(data);\n      emit('reload', 'add');\n    }\n    resetInitialized();\n    modalApi.close();\n  } catch (error) {\n    console.error(error);\n  } finally {\n    modalApi.lock(false);\n  }\n}\n\nasync function handleClosed() {\n  await formApi.resetForm();\n  resetInitialized();\n}\n</script>\n\n<template>\n  <BasicDrawer :fullscreen-button=\"false\" :title=\"title\" class=\"w-[550px]\">\n    <div class=\"min-h-[400px]\">\n      <BasicForm />\n    </div>\n  </BasicDrawer>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/workflow/processInstance/data.tsx",
    "content": "import type { FormSchemaGetter } from '#/adapter/form';\nimport type { VxeGridProps } from '#/adapter/vxe-table';\n\nimport { DictEnum } from '@vben/constants';\n\nimport { OptionsTag } from '#/components/table';\nimport { renderDict } from '#/utils/render';\n\nimport { activityStatusOptions } from '../processDefinition/constant';\n\nexport const querySchema: FormSchemaGetter = () => [\n  {\n    component: 'Input',\n    label: '任务名称',\n    fieldName: 'nodeName',\n  },\n  {\n    component: 'Input',\n    label: '流程名称',\n    fieldName: 'flowName',\n  },\n  {\n    component: 'Input',\n    label: '流程编码',\n    fieldName: 'flowCode',\n  },\n];\n\nexport const columns: VxeGridProps['columns'] = [\n  { type: 'checkbox', width: 60 },\n  {\n    field: 'flowName',\n    title: '流程名称',\n    minWidth: 150,\n  },\n  {\n    field: 'nodeName',\n    title: '任务名称',\n    minWidth: 150,\n  },\n  {\n    field: 'flowCode',\n    title: '流程编码',\n    minWidth: 150,\n  },\n  {\n    field: 'createByName',\n    title: '申请人',\n    minWidth: 150,\n  },\n  {\n    field: 'version',\n    title: '版本号',\n    minWidth: 150,\n    formatter: ({ cellValue }) => `V${cellValue}.0`,\n  },\n  {\n    field: 'activityStatus',\n    title: '状态',\n    minWidth: 100,\n    slots: {\n      default: ({ row }) => {\n        const cellValue = row.activityStatus;\n        return (\n          <OptionsTag\n            options={activityStatusOptions as any}\n            value={cellValue}\n          />\n        );\n      },\n    },\n  },\n  {\n    field: 'flowStatus',\n    title: '流程状态',\n    minWidth: 100,\n    slots: {\n      default: ({ row }) => {\n        return renderDict(row.flowStatus, DictEnum.WF_BUSINESS_STATUS);\n      },\n    },\n  },\n  {\n    field: 'action',\n    fixed: 'right',\n    slots: { default: 'action' },\n    title: '操作',\n    resizable: false,\n    width: 200,\n  },\n];\n"
  },
  {
    "path": "apps/web-antd/src/views/workflow/processInstance/index.vue",
    "content": "<script setup lang=\"ts\">\nimport type { RadioChangeEvent } from 'ant-design-vue';\n\nimport type { VbenFormProps } from '@vben/common-ui';\nimport type { Recordable } from '@vben/types';\n\nimport type { VxeGridProps } from '#/adapter/vxe-table';\n\nimport { ref } from 'vue';\n\nimport { Page, useVbenModal } from '@vben/common-ui';\nimport { $t } from '@vben/locales';\nimport { getVxePopupContainer } from '@vben/utils';\n\nimport { Modal, Popconfirm, RadioGroup, Space } from 'ant-design-vue';\n\nimport { useVbenVxeGrid, vxeCheckboxChecked } from '#/adapter/vxe-table';\nimport {\n  deleteByInstanceIds,\n  pageByFinish,\n  pageByRunning,\n} from '#/api/workflow/instance';\nimport CategoryTree from '#/views/workflow/processDefinition/category-tree.vue';\n\nimport { flowInfoModal } from '../components';\nimport { columns, querySchema } from './data';\nimport instanceInvalidModal from './instance-invalid-modal.vue';\nimport instanceVariableModal from './instance-variable-modal.vue';\n\n// 左边分类用\nconst selectedCode = ref<number[] | string[]>([]);\n\nconst formOptions: VbenFormProps = {\n  schema: querySchema(),\n  commonConfig: {\n    labelWidth: 80,\n    componentProps: {\n      allowClear: true,\n    },\n  },\n  wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4',\n  handleReset: async () => {\n    selectedCode.value = [];\n\n    const { formApi, reload } = tableApi;\n    await formApi.resetForm();\n    const formValues = formApi.form.values;\n    formApi.setLatestSubmissionValues(formValues);\n    await reload(formValues);\n  },\n};\n\nconst typeOptions = [\n  { label: '运行中', value: 'process_running' },\n  { label: '已完成', value: 'process_completed' },\n];\nlet currentTypeApi = pageByRunning;\nconst currentType = ref('process_running');\nasync function handleTypeChange(e: RadioChangeEvent) {\n  const { value } = e.target;\n  switch (value) {\n    case 'process_completed': {\n      currentTypeApi = pageByFinish;\n      break;\n    }\n    case 'process_running': {\n      currentTypeApi = pageByRunning;\n      break;\n    }\n  }\n\n  await tableApi.reload();\n}\n\nconst gridOptions: VxeGridProps = {\n  checkboxConfig: {\n    // 高亮\n    highlight: true,\n    // 翻页时保留选中状态\n    reserve: true,\n    // 点击行选中\n    trigger: 'default',\n  },\n  columns,\n  height: 'auto',\n  keepSource: true,\n  pagerConfig: {},\n  proxyConfig: {\n    ajax: {\n      query: async ({ page }, formValues = {}) => {\n        // 部门树选择处理\n        if (selectedCode.value.length === 1) {\n          formValues.category = selectedCode.value[0];\n        } else {\n          Reflect.deleteProperty(formValues, 'category');\n        }\n\n        return await currentTypeApi({\n          pageNum: page.currentPage,\n          pageSize: page.pageSize,\n          ...formValues,\n        });\n      },\n    },\n  },\n  headerCellConfig: {\n    height: 44,\n  },\n  cellConfig: {\n    height: 66,\n  },\n  rowConfig: {\n    keyField: 'id',\n  },\n  id: 'workflow-definition-index',\n};\n\nconst [BasicTable, tableApi] = useVbenVxeGrid({\n  formOptions,\n  gridOptions,\n});\n\nconst [InstanceInvalidModal, instanceInvalidModalApi] = useVbenModal({\n  connectedComponent: instanceInvalidModal,\n});\nasync function handleInvalid(row: Recordable<any>) {\n  instanceInvalidModalApi.setData({ id: row.id });\n  instanceInvalidModalApi.open();\n}\n\nasync function handleDelete(row: Recordable<any>) {\n  await deleteByInstanceIds(row.id);\n  await tableApi.query();\n}\n\nfunction handleMultiDelete() {\n  const rows = tableApi.grid.getCheckboxRecords();\n  const ids = rows.map((row: any) => row.id);\n  Modal.confirm({\n    title: '提示',\n    okType: 'danger',\n    content: `确认删除选中的${ids.length}条记录吗？`,\n    onOk: async () => {\n      await deleteByInstanceIds(ids);\n      await tableApi.query();\n    },\n  });\n}\nconst [InstanceVariableModal, instanceVariableModalApi] = useVbenModal({\n  connectedComponent: instanceVariableModal,\n});\nfunction handleVariable(row: Recordable<any>) {\n  instanceVariableModalApi.setData({ instanceId: row.id });\n  instanceVariableModalApi.open();\n}\n\nconst [FlowInfoModal, flowInfoModalApi] = useVbenModal({\n  connectedComponent: flowInfoModal,\n});\nfunction handleInfo(row: any) {\n  console.log(row);\n  flowInfoModalApi.setData({ businessId: row.businessId });\n  flowInfoModalApi.open();\n}\n</script>\n\n<template>\n  <Page :auto-content-height=\"true\">\n    <div class=\"flex h-full gap-[8px]\">\n      <CategoryTree\n        v-model:select-code=\"selectedCode\"\n        class=\"w-[260px]\"\n        @reload=\"() => tableApi.reload()\"\n        @select=\"() => tableApi.reload()\"\n      />\n      <BasicTable class=\"flex-1 overflow-hidden\">\n        <template #toolbar-actions>\n          <RadioGroup\n            v-model:value=\"currentType\"\n            :options=\"typeOptions\"\n            button-style=\"solid\"\n            option-type=\"button\"\n            @change=\"handleTypeChange\"\n          />\n        </template>\n        <template #toolbar-tools>\n          <Space>\n            <a-button\n              :disabled=\"!vxeCheckboxChecked(tableApi)\"\n              danger\n              type=\"primary\"\n              v-access:code=\"['system:user:remove']\"\n              @click=\"handleMultiDelete\"\n            >\n              {{ $t('pages.common.delete') }}\n            </a-button>\n          </Space>\n        </template>\n        <template #action=\"{ row }\">\n          <div class=\"flex flex-col\">\n            <div v-if=\"currentType === 'process_running'\">\n              <a-button\n                danger\n                size=\"small\"\n                type=\"link\"\n                @click.stop=\"handleInvalid(row)\"\n              >\n                作废流程\n              </a-button>\n              <Popconfirm\n                :get-popup-container=\"getVxePopupContainer\"\n                placement=\"left\"\n                title=\"确认删除？\"\n                @confirm=\"handleDelete(row)\"\n              >\n                <a-button danger size=\"small\" type=\"link\" @click.stop=\"\">\n                  删除流程\n                </a-button>\n              </Popconfirm>\n            </div>\n            <div>\n              <a-button size=\"small\" type=\"link\" @click.stop=\"handleInfo(row)\">\n                流程预览\n              </a-button>\n              <a-button\n                size=\"small\"\n                type=\"link\"\n                @click.stop=\"handleVariable(row)\"\n              >\n                变量查看\n              </a-button>\n            </div>\n          </div>\n        </template>\n      </BasicTable>\n    </div>\n    <InstanceInvalidModal @reload=\"() => tableApi.reload()\" />\n    <InstanceVariableModal />\n    <FlowInfoModal />\n  </Page>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/workflow/processInstance/instance-invalid-modal.vue",
    "content": "<script setup lang=\"ts\">\nimport { useVbenModal } from '@vben/common-ui';\n\nimport { cloneDeep } from 'lodash-es';\n\nimport { useVbenForm } from '#/adapter/form';\nimport { workflowInstanceInvalid } from '#/api/workflow/instance';\n\nconst emit = defineEmits<{ reload: [] }>();\n\nconst [BasicModal, modalApi] = useVbenModal({\n  onConfirm: handleSubmit,\n  onCancel: handleCancel,\n  fullscreenButton: false,\n  title: '作废原因',\n});\n\nconst [BasicForm, formApi] = useVbenForm({\n  commonConfig: {\n    formItemClass: 'col-span-2',\n    componentProps: {\n      class: 'w-full',\n    },\n    labelWidth: 80,\n  },\n  layout: 'vertical',\n  schema: [\n    {\n      fieldName: 'comment',\n      label: '作废原因',\n      component: 'Textarea',\n    },\n  ],\n  showDefaultActions: false,\n  wrapperClass: 'grid-cols-2',\n});\n\nasync function handleCancel() {\n  modalApi.close();\n  await formApi.resetForm();\n}\n\nasync function handleSubmit() {\n  try {\n    modalApi.modalLoading(true);\n    const { valid } = await formApi.validate();\n    if (!valid) {\n      return;\n    }\n    const data = cloneDeep(await formApi.getValues());\n    data.id = modalApi.getData().id;\n    await workflowInstanceInvalid(data as any);\n    emit('reload');\n    handleCancel();\n  } catch (error) {\n    console.error(error);\n  } finally {\n    modalApi.modalLoading(false);\n  }\n}\n</script>\n\n<template>\n  <BasicModal>\n    <BasicForm />\n  </BasicModal>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/workflow/processInstance/instance-variable-modal.vue",
    "content": "<script setup lang=\"tsx\">\nimport { ref } from 'vue';\n\nimport { JsonPreview, useVbenModal } from '@vben/common-ui';\nimport { cn, getPopupContainer } from '@vben/utils';\n\nimport { message, Modal, Tag } from 'ant-design-vue';\n\nimport { useVbenForm } from '#/adapter/form';\nimport { instanceVariable, updateFlowVariable } from '#/api/workflow/instance';\n\ninterface ModalData {\n  /**\n   * 变量 json字符串\n   */\n  record: string;\n  instanceId: string;\n}\n\nconst data = ref({});\nconst [BasicModal, modalApi] = useVbenModal({\n  title: '流程变量',\n  fullscreenButton: false,\n  footer: false,\n  onOpenChange: async (visible) => {\n    if (!visible) {\n      data.value = {};\n      return null;\n    }\n    modalApi.modalLoading(true);\n\n    await loadData();\n\n    modalApi.modalLoading(false);\n  },\n});\n\nconst fieldTypeColors = {\n  string: 'cyan',\n  number: 'blue',\n  boolean: 'orange',\n  object: 'purple',\n};\nfunction getFieldTypeColor(fieldType: string) {\n  return (\n    fieldTypeColors[fieldType as keyof typeof fieldTypeColors] ?? 'default'\n  );\n}\n\nasync function loadData() {\n  const { instanceId } = modalApi.getData() as ModalData;\n  const resp = await instanceVariable(instanceId);\n  const jsonObj = JSON.parse(resp.variable);\n  data.value = jsonObj;\n\n  // 表单\n  const objEntry = Object.entries(jsonObj);\n\n  interface OptionsType {\n    label: string;\n    value: string;\n    fieldType: string;\n  }\n\n  formApi.updateSchema([\n    {\n      fieldName: 'key',\n      componentProps: {\n        options: objEntry.map(\n          ([key, value]) =>\n            ({\n              label: key,\n              value: key,\n              fieldType: typeof value,\n            }) as OptionsType,\n        ),\n      },\n      renderComponentContent: () => ({\n        option: (option: OptionsType) => (\n          <div>\n            {option.label}\n            <Tag class=\"ml-1\" color={getFieldTypeColor(option.fieldType)}>\n              {option.fieldType}\n            </Tag>\n          </div>\n        ),\n      }),\n    },\n  ]);\n}\n\nconst [Form, formApi] = useVbenForm({\n  commonConfig: {\n    componentProps: {\n      class: 'w-full',\n      allowClear: true,\n    },\n    labelWidth: 80,\n  },\n  schema: [\n    {\n      fieldName: 'key',\n      component: 'Select',\n      label: '变量名称',\n      rules: 'selectRequired',\n      componentProps: {\n        getPopupContainer,\n      },\n    },\n    {\n      fieldName: 'valueType',\n      component: 'Select',\n      label: '变量类型',\n      rules: 'selectRequired',\n      componentProps: {\n        getPopupContainer,\n        options: [\n          {\n            label: 'string',\n            value: 'string',\n          },\n          {\n            label: 'boolean | number | object (使用JSON.parse)',\n            value: 'object',\n          },\n        ],\n      },\n    },\n    {\n      fieldName: 'value',\n      component: 'Input',\n      label: '变量值',\n      rules: 'required',\n    },\n  ],\n  resetButtonOptions: {\n    show: false,\n  },\n  submitButtonOptions: {\n    content: '修改',\n  },\n  handleSubmit: async (values) => {\n    console.log(values);\n    Modal.confirm({\n      title: '修改流程变量',\n      content: '确认修改流程变量吗？',\n      centered: true,\n      okButtonProps: {\n        danger: true,\n      },\n      onOk: async () => {\n        await handleSubmit(values);\n      },\n    });\n  },\n});\n\nasync function handleSubmit(values: any) {\n  try {\n    modalApi.lock(true);\n\n    const { instanceId } = modalApi.getData() as ModalData;\n\n    let transformValue = values.value;\n    if (values.valueType !== 'string') {\n      try {\n        transformValue = JSON.parse(values.value);\n      } catch (error) {\n        console.error(error);\n        if (error instanceof Error) {\n          message.error(error.message);\n        }\n        throw error;\n      }\n    }\n\n    // 修改\n    const requestData = {\n      instanceId,\n      key: values.key,\n      value: transformValue,\n    };\n    await updateFlowVariable(requestData);\n    await formApi.resetForm();\n\n    // 查询修改后的\n    const resp = await instanceVariable(instanceId);\n    const jsonObj = JSON.parse(resp.variable);\n    data.value = jsonObj;\n  } catch (error) {\n    console.error(error);\n  } finally {\n    modalApi.lock(false);\n  }\n}\n</script>\n\n<template>\n  <BasicModal>\n    <div\n      :class=\"cn('min-h-[400px] overflow-y-auto border', 'rounded-[4px] p-2')\"\n    >\n      <JsonPreview :data=\"data\" />\n    </div>\n    <div class=\"mt-2 break-all text-sm font-medium text-orange-500\">\n      需要支持变量类型需要更改后端代码(原版只支持string类型)\n      <div>\n        ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/bo/FlowVariableBo.java\n      </div>\n      将value的类型改为Object才能使用\n    </div>\n    <Form class=\"mt-2\" />\n  </BasicModal>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/workflow/register.ts",
    "content": "import { defineAsyncComponent, markRaw } from 'vue';\n\n/**\n * 这里定义流程描述组件\n */\n\nconst LeaveDescription = defineAsyncComponent(\n  () => import('#/views/workflow/leave/leave-description.vue'),\n);\n\n/**\n * key为流程的路径(task.formPath) value为要显示的组件\n */\nexport const flowComponentsMap = {\n  /**\n   * 请假申请 详情\n   */\n  '/workflow/leaveEdit/index': markRaw(LeaveDescription),\n};\n\nexport type FlowComponentsMapMapKey = keyof typeof flowComponentsMap;\n"
  },
  {
    "path": "apps/web-antd/src/views/workflow/spel/common.ts",
    "content": "export function generateSpel(data: {\n  componentName?: string;\n  methodName?: string;\n  methodParams?: string;\n}) {\n  const { componentName, methodName, methodParams } = data;\n  if (!componentName || !methodName) {\n    return '-';\n  }\n\n  const params = methodParams ? methodParams.split(',') : [];\n  const methodParamsText = params.map((item) => `#${item}`).join(',');\n\n  return `#{@${componentName}.${methodName}(${methodParamsText})}`;\n}\n"
  },
  {
    "path": "apps/web-antd/src/views/workflow/spel/data.tsx",
    "content": "import type { FormSchemaGetter } from '#/adapter/form';\nimport type { VxeGridProps } from '#/adapter/vxe-table';\n\nimport { DictEnum } from '@vben/constants';\n\nimport { getDictOptions } from '#/utils/dict';\nimport { renderDict } from '#/utils/render';\n\nexport const querySchema: FormSchemaGetter = () => [\n  {\n    component: 'Input',\n    fieldName: 'componentName',\n    label: '组件名称',\n  },\n  {\n    component: 'Input',\n    fieldName: 'methodName',\n    label: '方法名称',\n  },\n];\n\nexport const columns: VxeGridProps['columns'] = [\n  { type: 'checkbox', width: 60 },\n  {\n    title: '组件名称',\n    field: 'componentName',\n    formatter: ({ cellValue }) => cellValue ?? '-',\n  },\n  {\n    title: '方法名称',\n    field: 'methodName',\n    formatter: ({ cellValue }) => cellValue ?? '-',\n  },\n  {\n    title: '参数名称',\n    field: 'methodParams',\n  },\n  {\n    title: 'Spel表达式',\n    field: 'viewSpel',\n  },\n  {\n    title: '状态',\n    field: 'status',\n    width: 120,\n    slots: {\n      default: ({ row }) => {\n        return renderDict(row.status, DictEnum.SYS_NORMAL_DISABLE);\n      },\n    },\n  },\n  {\n    title: '备注',\n    field: 'remark',\n  },\n  {\n    title: '创建时间',\n    field: 'createTime',\n  },\n  {\n    field: 'action',\n    fixed: 'right',\n    slots: { default: 'action' },\n    title: '操作',\n    resizable: false,\n    width: 'auto',\n  },\n];\n\nexport const drawerSchema: FormSchemaGetter = () => [\n  {\n    component: 'Input',\n    dependencies: {\n      show: () => false,\n      triggerFields: [''],\n    },\n    fieldName: 'id',\n    label: 'id',\n  },\n  {\n    component: 'Input',\n    fieldName: 'componentName',\n    label: '组件名称',\n    rules: 'required',\n  },\n  {\n    component: 'Input',\n    fieldName: 'methodName',\n    label: '方法名称',\n    rules: 'required',\n  },\n  {\n    component: 'Input',\n    fieldName: 'methodParams',\n    label: '参数名称',\n    // rules: 'required',\n  },\n  {\n    component: 'Input',\n    fieldName: 'viewSpel',\n    label: 'Spel表达式',\n    // rules: 'required',\n  },\n  {\n    component: 'RadioGroup',\n    componentProps: {\n      buttonStyle: 'solid',\n      options: getDictOptions(DictEnum.SYS_NORMAL_DISABLE),\n      optionType: 'button',\n    },\n    defaultValue: '0',\n    fieldName: 'status',\n    label: '状态',\n    rules: 'required',\n  },\n  {\n    component: 'Textarea',\n    fieldName: 'remark',\n    formItemClass: 'items-start',\n    label: '备注',\n  },\n];\n"
  },
  {
    "path": "apps/web-antd/src/views/workflow/spel/index.vue",
    "content": "<script setup lang=\"ts\">\nimport type { VbenFormProps } from '@vben/common-ui';\n\nimport type { VxeGridProps } from '#/adapter/vxe-table';\nimport type { Spel } from '#/api/workflow/spel/model';\n\nimport { Page, useVbenDrawer } from '@vben/common-ui';\nimport { getVxePopupContainer } from '@vben/utils';\n\nimport { Modal, Popconfirm, Space } from 'ant-design-vue';\n\nimport { useVbenVxeGrid, vxeCheckboxChecked } from '#/adapter/vxe-table';\nimport { configRemove } from '#/api/system/config';\nimport { spelList } from '#/api/workflow/spel';\n\nimport { columns, querySchema } from './data';\nimport spelDrawer from './spel-drawer.vue';\n\nconst formOptions: VbenFormProps = {\n  commonConfig: {\n    labelWidth: 80,\n    componentProps: {\n      allowClear: true,\n    },\n  },\n  schema: querySchema(),\n  wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4',\n};\n\nconst gridOptions: VxeGridProps = {\n  checkboxConfig: {\n    // 高亮\n    highlight: true,\n    // 翻页时保留选中状态\n    reserve: true,\n  },\n  columns,\n  height: 'auto',\n  keepSource: true,\n  pagerConfig: {},\n  proxyConfig: {\n    ajax: {\n      query: async ({ page }, formValues = {}) => {\n        return await spelList({\n          pageNum: page.currentPage,\n          pageSize: page.pageSize,\n          ...formValues,\n        });\n      },\n    },\n  },\n  rowConfig: {\n    keyField: 'id',\n  },\n  id: 'workflow-spel-index',\n  showOverflow: false,\n};\n\nconst [BasicTable, tableApi] = useVbenVxeGrid({\n  formOptions,\n  gridOptions,\n});\nconst [SpelDrawer, drawerApi] = useVbenDrawer({\n  connectedComponent: spelDrawer,\n});\n\nfunction handleAdd() {\n  drawerApi.setData({});\n  drawerApi.open();\n}\n\nasync function handleEdit(record: Spel) {\n  drawerApi.setData({ id: record.id });\n  drawerApi.open();\n}\n\nasync function handleDelete(row: Spel) {\n  await configRemove([row.id]);\n  await tableApi.query();\n}\n\nfunction handleMultiDelete() {\n  const rows = tableApi.grid.getCheckboxRecords();\n  const ids = rows.map((row: Spel) => row.id);\n  Modal.confirm({\n    title: '提示',\n    okType: 'danger',\n    content: `确认删除选中的${ids.length}条记录吗？`,\n    onOk: async () => {\n      await configRemove(ids);\n      await tableApi.query();\n    },\n  });\n}\n</script>\n\n<template>\n  <Page :auto-content-height=\"true\">\n    <BasicTable table-title=\"流程表达式列表\">\n      <template #toolbar-tools>\n        <Space>\n          <a-button\n            :disabled=\"!vxeCheckboxChecked(tableApi)\"\n            danger\n            type=\"primary\"\n            v-access:code=\"['system:config:remove']\"\n            @click=\"handleMultiDelete\"\n          >\n            {{ $t('pages.common.delete') }}\n          </a-button>\n          <a-button\n            type=\"primary\"\n            v-access:code=\"['system:config:add']\"\n            @click=\"handleAdd\"\n          >\n            {{ $t('pages.common.add') }}\n          </a-button>\n        </Space>\n      </template>\n      <template #action=\"{ row }\">\n        <Space>\n          <ghost-button\n            v-access:code=\"['system:config:edit']\"\n            @click.stop=\"handleEdit(row)\"\n          >\n            {{ $t('pages.common.edit') }}\n          </ghost-button>\n          <Popconfirm\n            :get-popup-container=\"getVxePopupContainer\"\n            placement=\"left\"\n            title=\"确认删除？\"\n            @confirm=\"handleDelete(row)\"\n          >\n            <ghost-button\n              danger\n              v-access:code=\"['system:config:remove']\"\n              @click.stop=\"\"\n            >\n              {{ $t('pages.common.delete') }}\n            </ghost-button>\n          </Popconfirm>\n        </Space>\n      </template>\n    </BasicTable>\n    <SpelDrawer @reload=\"tableApi.query()\" />\n  </Page>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/workflow/spel/spel-drawer.vue",
    "content": "<script setup lang=\"ts\">\nimport { computed, ref } from 'vue';\n\nimport { useVbenDrawer } from '@vben/common-ui';\nimport { $t } from '@vben/locales';\nimport { cloneDeep } from '@vben/utils';\n\nimport { useVbenForm } from '#/adapter/form';\nimport { spelAdd, spelInfo, spelUpdate } from '#/api/workflow/spel';\nimport { defaultFormValueGetter, useBeforeCloseDiff } from '#/utils/popup';\n\nimport { generateSpel } from './common';\nimport { drawerSchema } from './data';\nimport SpelPreviewer from './spel-previewer.vue';\n\nconst emit = defineEmits<{ reload: [] }>();\n\nconst isUpdate = ref(false);\nconst title = computed(() => {\n  return isUpdate.value ? $t('pages.common.edit') : $t('pages.common.add');\n});\n\nconst [BasicForm, formApi] = useVbenForm({\n  commonConfig: {\n    formItemClass: 'col-span-2',\n    componentProps: {\n      class: 'w-full',\n    },\n    labelWidth: 80,\n  },\n  schema: drawerSchema(),\n  showDefaultActions: false,\n  wrapperClass: 'grid-cols-2',\n});\n\nconst { onBeforeClose, markInitialized, resetInitialized } = useBeforeCloseDiff(\n  {\n    initializedGetter: defaultFormValueGetter(formApi),\n    currentGetter: defaultFormValueGetter(formApi),\n  },\n);\n\nconst [BasicDrawer, drawerApi] = useVbenDrawer({\n  onBeforeClose,\n  onClosed: handleClosed,\n  onConfirm: handleConfirm,\n  async onOpenChange(isOpen) {\n    if (!isOpen) {\n      return null;\n    }\n    drawerApi.drawerLoading(true);\n    const { id } = drawerApi.getData() as { id?: number | string };\n    isUpdate.value = !!id;\n\n    // 更新 && 赋值\n    if (isUpdate.value && id) {\n      const record = await spelInfo(id);\n      await formApi.setValues(record);\n    }\n    await markInitialized();\n    drawerApi.drawerLoading(false);\n  },\n});\n\nasync function handleConfirm() {\n  try {\n    drawerApi.lock(true);\n    const { valid } = await formApi.validate();\n    if (!valid) {\n      return;\n    }\n    const data = cloneDeep(await formApi.getValues());\n    if (isUpdate.value) {\n      await spelUpdate(data);\n    } else {\n      // 新增需要生成\n      data.viewSpel = generateSpel(data);\n      await spelAdd(data);\n    }\n    resetInitialized();\n    emit('reload');\n    drawerApi.close();\n  } catch (error) {\n    console.error(error);\n  } finally {\n    drawerApi.lock(false);\n  }\n}\n\nasync function handleClosed() {\n  await formApi.resetForm();\n  resetInitialized();\n}\n</script>\n\n<template>\n  <BasicDrawer :title=\"title\" class=\"w-[600px]\">\n    <BasicForm>\n      <template #viewSpel>\n        <SpelPreviewer\n          :component-name=\"formApi.form.values.componentName\"\n          :method-name=\"formApi.form.values.methodName\"\n          :method-params=\"formApi.form.values.methodParams\"\n        />\n      </template>\n    </BasicForm>\n  </BasicDrawer>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/workflow/spel/spel-previewer.vue",
    "content": "<script setup lang=\"ts\">\nimport { computed } from 'vue';\n\ninterface Props {\n  componentName?: string;\n  methodName?: string;\n  methodParams?: string;\n}\n\nconst props = defineProps<Props>();\n\nconst text = computed(() => {\n  const { componentName, methodName, methodParams } = props;\n  if (!componentName || !methodName) {\n    return '-';\n  }\n\n  const params = methodParams ? methodParams.split(',') : [];\n  const methodParamsText = params.map((item) => `#${item}`).join(',');\n\n  return `#{@${componentName}.${methodName}(${methodParamsText})}`;\n});\n</script>\n\n<template>\n  <div class=\"w-full break-all rounded-[4px] bg-[black]/5 p-2\">{{ text }}</div>\n</template>\n"
  },
  {
    "path": "apps/web-antd/src/views/workflow/task/allTaskWaiting.vue",
    "content": "<!-- eslint-disable no-use-before-define -->\n<script setup lang=\"ts\">\nimport type { User } from '#/api/system/user/model';\nimport type { TaskInfo } from '#/api/workflow/task/model';\n\nimport { computed, nextTick, onMounted, ref, useTemplateRef } from 'vue';\n\nimport { Page } from '@vben/common-ui';\nimport { useTabs } from '@vben/hooks';\nimport { addFullName, getPopupContainer } from '@vben/utils';\n\nimport { FilterOutlined, RedoOutlined } from '@ant-design/icons-vue';\nimport {\n  Empty,\n  Form,\n  FormItem,\n  Input,\n  InputSearch,\n  Popover,\n  Segmented,\n  Spin,\n  Tooltip,\n  TreeSelect,\n} from 'ant-design-vue';\nimport { cloneDeep, debounce, uniqueId } from 'lodash-es';\n\nimport { categoryTree } from '#/api/workflow/category';\nimport { pageByAllTaskFinish, pageByAllTaskWait } from '#/api/workflow/task';\n\nimport { ApprovalCard, ApprovalPanel, CopyComponent } from '../components';\nimport { bottomOffset } from './constant';\n\nconst emptyImage = Empty.PRESENTED_IMAGE_SIMPLE;\n\n/**\n * 流程监控 - 待办任务页面的id不唯一 改为前端处理\n */\ninterface TaskItem extends TaskInfo {\n  active: boolean;\n  randomId: string;\n}\n\nconst taskList = ref<TaskItem[]>([]);\nconst taskTotal = ref(0);\nconst page = ref(1);\nconst loading = ref(false);\n\nconst typeOptions = [\n  { label: '待办任务', value: 'todo' },\n  { label: '已办任务', value: 'done' },\n];\nconst currentType = ref('todo');\nconst currentApi = computed(() => {\n  if (currentType.value === 'todo') {\n    return pageByAllTaskWait;\n  }\n  return pageByAllTaskFinish;\n});\nconst approvalType = computed(() => {\n  if (currentType.value === 'done') {\n    return 'readonly';\n  }\n  return 'admin';\n});\nasync function handleTypeChange() {\n  // 需要先滚动到顶部\n  cardContainerRef.value?.scroll({ top: 0, behavior: 'auto' });\n  page.value = 1;\n\n  taskList.value = [];\n  await nextTick();\n  await reload(true);\n}\n\nconst defaultFormData = {\n  flowName: '', // 流程定义名称\n  nodeName: '', // 任务名称\n  flowCode: '', // 流程定义编码\n  createByIds: [] as string[], // 创建人\n  category: null as null | number, // 流程分类\n};\nconst formData = ref(cloneDeep(defaultFormData));\n\n/**\n * 是否已经加载全部数据 即 taskList.length === taskTotal\n */\nconst isLoadComplete = computed(\n  () => taskList.value.length === taskTotal.value,\n);\n\n// 卡片父容器的ref\nconst cardContainerRef = useTemplateRef('cardContainerRef');\n\n/**\n * @param resetFields 是否清空查询参数\n */\nasync function reload(resetFields: boolean = false) {\n  // 需要先滚动到顶部\n  cardContainerRef.value?.scroll({ top: 0, behavior: 'auto' });\n\n  page.value = 1;\n  currentTask.value = undefined;\n  taskTotal.value = 0;\n  lastSelectId.value = '';\n\n  if (resetFields) {\n    formData.value = cloneDeep(defaultFormData);\n    selectedUserList.value = [];\n  }\n\n  loading.value = true;\n  const resp = await currentApi.value({\n    pageSize: 10,\n    pageNum: page.value,\n    ...formData.value,\n  });\n  taskList.value = resp.rows.map((item) => ({\n    ...item,\n    active: false,\n    randomId: uniqueId(),\n  }));\n  taskTotal.value = resp.total;\n\n  loading.value = false;\n  // 默认选中第一个\n  if (taskList.value.length > 0) {\n    const firstTask = taskList.value[0]!;\n    currentTask.value = firstTask;\n    handleCardClick(firstTask);\n  }\n}\n\nonMounted(reload);\n\nconst handleScroll = debounce(async (e: Event) => {\n  if (!e.target) {\n    return;\n  }\n  // e.target.scrollTop 是元素顶部到当前可视区域顶部的距离，即已滚动的高度。\n  // e.target.clientHeight 是元素的可视高度。\n  // e.target.scrollHeight 是元素的总高度。\n  const { scrollTop, clientHeight, scrollHeight } = e.target as HTMLElement;\n  // 判断是否滚动到底部\n  const isBottom = scrollTop + clientHeight >= scrollHeight - bottomOffset;\n  console.log('scrollTop + clientHeight', scrollTop + clientHeight);\n  console.log('scrollHeight', scrollHeight);\n\n  // 滚动到底部且没有加载完成\n  if (isBottom && !isLoadComplete.value) {\n    loading.value = true;\n    page.value += 1;\n    const resp = await currentApi.value({\n      pageSize: 10,\n      pageNum: page.value,\n      ...formData.value,\n    });\n    taskList.value.push(\n      ...resp.rows.map((item) => ({\n        ...item,\n        active: false,\n        randomId: uniqueId(),\n      })),\n    );\n    loading.value = false;\n  }\n}, 200);\n\nconst lastSelectId = ref('');\nconst currentTask = ref<TaskInfo>();\nasync function handleCardClick(item: TaskItem) {\n  const { randomId } = item;\n  // 点击的是同一个\n  if (lastSelectId.value === randomId) {\n    return;\n  }\n  currentTask.value = item;\n  // 反选状态 & 如果已经点击了 不变 & 保持只能有一个选中\n  taskList.value.forEach((item) => {\n    item.active = item.randomId === randomId;\n  });\n  lastSelectId.value = randomId;\n}\n\nconst { refreshTab } = useTabs();\n\n// 由于失去焦点浮层会消失 使用v-model选择人员完毕后强制显示\nconst popoverOpen = ref(false);\nconst selectedUserList = ref<User[]>([]);\nfunction handleFinish(userList: User[]) {\n  popoverOpen.value = true;\n  selectedUserList.value = userList;\n  formData.value.createByIds = userList.map((item) => item.userId);\n}\n\nconst treeData = ref<any[]>([]);\nonMounted(async () => {\n  // menu\n  const tree = await categoryTree();\n  addFullName(tree, 'label', ' / ');\n  treeData.value = tree;\n});\n</script>\n\n<template>\n  <Page :auto-content-height=\"true\">\n    <div class=\"flex h-full gap-2\">\n      <div\n        class=\"bg-background relative flex h-full min-w-[320px] max-w-[320px] flex-col rounded-lg\"\n      >\n        <!-- 搜索条件 -->\n        <div\n          class=\"bg-background z-100 sticky left-0 top-0 w-full rounded-t-lg border-b-[1px] border-solid p-2\"\n        >\n          <Segmented\n            v-model:value=\"currentType\"\n            :options=\"typeOptions\"\n            block\n            class=\"mb-2\"\n            @change=\"handleTypeChange\"\n          />\n          <div class=\"flex items-center gap-1\">\n            <InputSearch\n              v-model:value=\"formData.flowName\"\n              placeholder=\"流程名称搜索\"\n              @search=\"reload(false)\"\n            />\n            <Tooltip placement=\"top\" title=\"重置\">\n              <a-button @click=\"reload(true)\">\n                <RedoOutlined />\n              </a-button>\n            </Tooltip>\n            <Popover\n              v-model:open=\"popoverOpen\"\n              :get-popup-container=\"getPopupContainer\"\n              placement=\"rightTop\"\n              trigger=\"click\"\n            >\n              <template #title>\n                <div class=\"w-full border-b pb-[12px] text-[16px]\">搜索</div>\n              </template>\n              <template #content>\n                <Form\n                  :colon=\"false\"\n                  :label-col=\"{ span: 6 }\"\n                  :model=\"formData\"\n                  autocomplete=\"off\"\n                  class=\"w-[300px]\"\n                  @finish=\"() => reload(false)\"\n                >\n                  <FormItem label=\"申请人\">\n                    <!-- 弹窗关闭后仍然显示表单浮层 -->\n                    <CopyComponent\n                      v-model:user-list=\"selectedUserList\"\n                      @cancel=\"() => (popoverOpen = true)\"\n                      @finish=\"handleFinish\"\n                    />\n                  </FormItem>\n                  <FormItem label=\"流程分类\">\n                    <TreeSelect\n                      v-model:value=\"formData.category\"\n                      :allow-clear=\"true\"\n                      :field-names=\"{ label: 'label', value: 'id' }\"\n                      :get-popup-container=\"getPopupContainer\"\n                      :tree-data=\"treeData\"\n                      :tree-default-expand-all=\"true\"\n                      :tree-line=\"{ showLeafIcon: false }\"\n                      placeholder=\"请选择\"\n                      tree-node-filter-prop=\"label\"\n                      tree-node-label-prop=\"fullName\"\n                    />\n                  </FormItem>\n                  <FormItem label=\"任务名称\">\n                    <Input\n                      v-model:value=\"formData.nodeName\"\n                      placeholder=\"请输入\"\n                    />\n                  </FormItem>\n                  <FormItem label=\"流程编码\">\n                    <Input\n                      v-model:value=\"formData.flowCode\"\n                      placeholder=\"请输入\"\n                    />\n                  </FormItem>\n                  <FormItem>\n                    <div class=\"flex\">\n                      <a-button block html-type=\"submit\" type=\"primary\">\n                        搜索\n                      </a-button>\n                      <a-button block class=\"ml-2\" @click=\"reload(true)\">\n                        重置\n                      </a-button>\n                    </div>\n                  </FormItem>\n                </Form>\n              </template>\n              <a-button>\n                <FilterOutlined />\n              </a-button>\n            </Popover>\n          </div>\n        </div>\n        <div\n          ref=\"cardContainerRef\"\n          class=\"thin-scrollbar flex flex-1 flex-col gap-2 overflow-y-auto py-3\"\n          @scroll=\"handleScroll\"\n        >\n          <template v-if=\"taskList.length > 0\">\n            <ApprovalCard\n              v-for=\"item in taskList\"\n              :key=\"item.randomId\"\n              :info=\"item\"\n              class=\"mx-2\"\n              row-key=\"randomId\"\n              @click=\"handleCardClick(item)\"\n            />\n          </template>\n          <Empty v-else :image=\"emptyImage\" />\n          <div\n            v-if=\"isLoadComplete && taskList.length > 0\"\n            class=\"flex items-center justify-center text-[14px] opacity-50\"\n          >\n            没有更多数据了\n          </div>\n          <!-- 遮罩loading层 -->\n          <div\n            v-if=\"loading\"\n            class=\"absolute left-0 top-0 flex h-full w-full items-center justify-center bg-[rgba(0,0,0,0.1)]\"\n          >\n            <Spin tip=\"加载中...\" />\n          </div>\n        </div>\n        <!-- total显示 -->\n        <div\n          class=\"bg-background sticky bottom-0 w-full rounded-b-lg border-t-[1px] py-2\"\n        >\n          <div class=\"flex items-center justify-center\">\n            共 {{ taskTotal }} 条记录\n          </div>\n        </div>\n      </div>\n      <ApprovalPanel\n        :task=\"currentTask\"\n        :type=\"approvalType\"\n        @reload=\"refreshTab\"\n      />\n    </div>\n  </Page>\n</template>\n\n<style lang=\"scss\" scoped>\n.thin-scrollbar {\n  &::-webkit-scrollbar {\n    width: 5px;\n  }\n}\n\n:deep(.ant-card-body) {\n  @apply thin-scrollbar;\n}\n</style>\n"
  },
  {
    "path": "apps/web-antd/src/views/workflow/task/constant.ts",
    "content": "/**\n * 底部偏移量\n * 在缩放时会差大概0.5px 导致触底逻辑不会触发\n * 在这里设置手动补偿\n * @see https://gitee.com/dapppp/ruoyi-plus-vben5/issues/IC28RE#note_40175381\n */\nexport const bottomOffset = 2;\n"
  },
  {
    "path": "apps/web-antd/src/views/workflow/task/myDocument.vue",
    "content": "<!-- eslint-disable no-use-before-define -->\n<script setup lang=\"ts\">\nimport type { TaskInfo } from '#/api/workflow/task/model';\n\nimport { computed, onMounted, ref, useTemplateRef } from 'vue';\n\nimport { Page } from '@vben/common-ui';\nimport { useTabs } from '@vben/hooks';\nimport { getPopupContainer } from '@vben/utils';\n\nimport { FilterOutlined, RedoOutlined } from '@ant-design/icons-vue';\nimport {\n  Empty,\n  Form,\n  FormItem,\n  Input,\n  InputSearch,\n  Popover,\n  Spin,\n  Tooltip,\n} from 'ant-design-vue';\nimport { cloneDeep, debounce } from 'lodash-es';\n\nimport { pageByCurrent } from '#/api/workflow/instance';\n\nimport { ApprovalCard, ApprovalPanel } from '../components';\nimport { bottomOffset } from './constant';\n\nconst emptyImage = Empty.PRESENTED_IMAGE_SIMPLE;\n\nconst taskList = ref<(TaskInfo & { active: boolean })[]>([]);\nconst taskTotal = ref(0);\nconst page = ref(1);\nconst loading = ref(false);\n\nconst defaultFormData = {\n  flowName: '', // 流程定义名称\n  nodeName: '', // 任务名称\n  flowCode: '', // 流程定义编码\n  category: null as null | number, // 流程分类\n};\nconst formData = ref(cloneDeep(defaultFormData));\n\n/**\n * 是否已经加载全部数据 即 taskList.length === taskTotal\n */\nconst isLoadComplete = computed(\n  () => taskList.value.length === taskTotal.value,\n);\n\n// 卡片父容器的ref\nconst cardContainerRef = useTemplateRef('cardContainerRef');\n\n/**\n * @param resetFields 是否清空查询参数\n */\nasync function reload(resetFields: boolean = false) {\n  // 需要先滚动到顶部\n  cardContainerRef.value?.scroll({ top: 0, behavior: 'auto' });\n\n  page.value = 1;\n  currentTask.value = undefined;\n  taskTotal.value = 0;\n  lastSelectId.value = '';\n\n  if (resetFields) {\n    formData.value = cloneDeep(defaultFormData);\n  }\n\n  loading.value = true;\n  const resp = await pageByCurrent({\n    pageSize: 10,\n    pageNum: page.value,\n    ...formData.value,\n  });\n  taskList.value = resp.rows.map((item) => ({ ...item, active: false }));\n  taskTotal.value = resp.total;\n\n  loading.value = false;\n  // 默认选中第一个\n  if (taskList.value.length > 0) {\n    const firstTask = taskList.value[0]!;\n    currentTask.value = firstTask;\n    handleCardClick(firstTask);\n  }\n}\n\nonMounted(reload);\n\nconst handleScroll = debounce(async (e: Event) => {\n  if (!e.target) {\n    return;\n  }\n  // e.target.scrollTop 是元素顶部到当前可视区域顶部的距离，即已滚动的高度。\n  // e.target.clientHeight 是元素的可视高度。\n  // e.target.scrollHeight 是元素的总高度。\n  const { scrollTop, clientHeight, scrollHeight } = e.target as HTMLElement;\n  // 判断是否滚动到底部\n  const isBottom = scrollTop + clientHeight >= scrollHeight - bottomOffset;\n\n  // 滚动到底部且没有加载完成\n  if (isBottom && !isLoadComplete.value) {\n    loading.value = true;\n    page.value += 1;\n    const resp = await pageByCurrent({\n      pageSize: 10,\n      pageNum: page.value,\n      ...formData.value,\n    });\n    taskList.value.push(\n      ...resp.rows.map((item) => ({ ...item, active: false })),\n    );\n    loading.value = false;\n  }\n}, 200);\n\nconst lastSelectId = ref('');\nconst currentTask = ref<TaskInfo>();\nasync function handleCardClick(item: TaskInfo) {\n  const { id } = item;\n  // 点击的是同一个\n  if (lastSelectId.value === id) {\n    return;\n  }\n  currentTask.value = item;\n  // 反选状态 & 如果已经点击了 不变 & 保持只能有一个选中\n  taskList.value.forEach((item) => {\n    item.active = item.id === id;\n  });\n  lastSelectId.value = id;\n}\n\nconst { refreshTab } = useTabs();\n</script>\n\n<template>\n  <Page :auto-content-height=\"true\">\n    <div class=\"flex h-full gap-2\">\n      <div\n        class=\"bg-background relative flex h-full min-w-[320px] max-w-[320px] flex-col rounded-lg\"\n      >\n        <!-- 搜索条件 -->\n        <div\n          class=\"bg-background z-100 sticky left-0 top-0 w-full rounded-t-lg border-b-[1px] border-solid p-2\"\n        >\n          <div class=\"flex items-center gap-1\">\n            <InputSearch\n              v-model:value=\"formData.flowName\"\n              placeholder=\"流程名称搜索\"\n              @search=\"reload(false)\"\n            />\n            <Tooltip placement=\"top\" title=\"重置\">\n              <a-button @click=\"reload(true)\">\n                <RedoOutlined />\n              </a-button>\n            </Tooltip>\n            <Popover\n              :get-popup-container=\"getPopupContainer\"\n              placement=\"rightTop\"\n              trigger=\"click\"\n            >\n              <template #title>\n                <div class=\"w-full border-b pb-[12px] text-[16px]\">搜索</div>\n              </template>\n              <template #content>\n                <Form\n                  :colon=\"false\"\n                  :label-col=\"{ span: 6 }\"\n                  :model=\"formData\"\n                  autocomplete=\"off\"\n                  class=\"w-[300px]\"\n                  @finish=\"() => reload(false)\"\n                >\n                  <FormItem label=\"任务名称\">\n                    <Input\n                      v-model:value=\"formData.nodeName\"\n                      placeholder=\"请输入\"\n                    />\n                  </FormItem>\n                  <FormItem label=\"流程编码\">\n                    <Input\n                      v-model:value=\"formData.flowCode\"\n                      placeholder=\"请输入\"\n                    />\n                  </FormItem>\n                  <FormItem>\n                    <div class=\"flex\">\n                      <a-button block html-type=\"submit\" type=\"primary\">\n                        搜索\n                      </a-button>\n                      <a-button block class=\"ml-2\" @click=\"reload(true)\">\n                        重置\n                      </a-button>\n                    </div>\n                  </FormItem>\n                </Form>\n              </template>\n              <a-button>\n                <FilterOutlined />\n              </a-button>\n            </Popover>\n          </div>\n        </div>\n        <div\n          ref=\"cardContainerRef\"\n          class=\"thin-scrollbar flex flex-1 flex-col gap-2 overflow-y-auto py-3\"\n          @scroll=\"handleScroll\"\n        >\n          <template v-if=\"taskList.length > 0\">\n            <ApprovalCard\n              v-for=\"item in taskList\"\n              :key=\"item.id\"\n              :info=\"item\"\n              class=\"mx-2\"\n              @click=\"handleCardClick(item)\"\n            />\n          </template>\n          <Empty v-else :image=\"emptyImage\" />\n          <div\n            v-if=\"isLoadComplete && taskList.length > 0\"\n            class=\"flex items-center justify-center text-[14px] opacity-50\"\n          >\n            没有更多数据了\n          </div>\n          <!-- 遮罩loading层 -->\n          <div\n            v-if=\"loading\"\n            class=\"absolute left-0 top-0 flex h-full w-full items-center justify-center bg-[rgba(0,0,0,0.1)]\"\n          >\n            <Spin tip=\"加载中...\" />\n          </div>\n        </div>\n        <!-- total显示 -->\n        <div\n          class=\"bg-background sticky bottom-0 w-full rounded-b-lg border-t-[1px] py-2\"\n        >\n          <div class=\"flex items-center justify-center\">\n            共 {{ taskTotal }} 条记录\n          </div>\n        </div>\n      </div>\n      <ApprovalPanel :task=\"currentTask\" type=\"myself\" @reload=\"refreshTab\" />\n    </div>\n  </Page>\n</template>\n\n<style lang=\"scss\" scoped>\n.thin-scrollbar {\n  &::-webkit-scrollbar {\n    width: 5px;\n  }\n}\n\n:deep(.ant-card-body) {\n  @apply thin-scrollbar;\n}\n</style>\n"
  },
  {
    "path": "apps/web-antd/src/views/workflow/task/taskCopyList.vue",
    "content": "<!-- eslint-disable no-use-before-define -->\n<script setup lang=\"ts\">\nimport type { User } from '#/api/system/user/model';\nimport type { TaskInfo } from '#/api/workflow/task/model';\n\nimport { computed, onMounted, ref, useTemplateRef } from 'vue';\n\nimport { Page } from '@vben/common-ui';\nimport { addFullName, getPopupContainer } from '@vben/utils';\n\nimport { FilterOutlined, RedoOutlined } from '@ant-design/icons-vue';\nimport {\n  Empty,\n  Form,\n  FormItem,\n  Input,\n  InputSearch,\n  Popover,\n  Spin,\n  Tooltip,\n  TreeSelect,\n} from 'ant-design-vue';\nimport { cloneDeep, debounce } from 'lodash-es';\n\nimport { categoryTree } from '#/api/workflow/category';\nimport { pageByTaskCopy } from '#/api/workflow/task';\n\nimport { ApprovalCard, ApprovalPanel, CopyComponent } from '../components';\nimport { bottomOffset } from './constant';\n\nconst emptyImage = Empty.PRESENTED_IMAGE_SIMPLE;\n\nconst taskList = ref<(TaskInfo & { active: boolean })[]>([]);\nconst taskTotal = ref(0);\nconst page = ref(1);\nconst loading = ref(false);\n\nconst defaultFormData = {\n  flowName: '', // 流程定义名称\n  nodeName: '', // 任务名称\n  flowCode: '', // 流程定义编码\n  createByIds: [] as string[], // 创建人\n  category: null as null | number, // 流程分类\n};\nconst formData = ref(cloneDeep(defaultFormData));\n\n/**\n * 是否已经加载全部数据 即 taskList.length === taskTotal\n */\nconst isLoadComplete = computed(\n  () => taskList.value.length === taskTotal.value,\n);\n\n// 卡片父容器的ref\nconst cardContainerRef = useTemplateRef('cardContainerRef');\n\n/**\n * @param resetFields 是否清空查询参数\n */\nasync function reload(resetFields: boolean = false) {\n  // 需要先滚动到顶部\n  cardContainerRef.value?.scroll({ top: 0, behavior: 'auto' });\n\n  page.value = 1;\n  currentTask.value = undefined;\n  taskTotal.value = 0;\n  lastSelectId.value = '';\n\n  if (resetFields) {\n    formData.value = cloneDeep(defaultFormData);\n    selectedUserList.value = [];\n  }\n\n  loading.value = true;\n  const resp = await pageByTaskCopy({\n    pageSize: 10,\n    pageNum: page.value,\n    ...formData.value,\n  });\n  taskList.value = resp.rows.map((item) => ({ ...item, active: false }));\n  taskTotal.value = resp.total;\n\n  loading.value = false;\n  // 默认选中第一个\n  if (taskList.value.length > 0) {\n    const firstTask = taskList.value[0]!;\n    currentTask.value = firstTask;\n    handleCardClick(firstTask);\n  }\n}\n\nonMounted(reload);\n\nconst handleScroll = debounce(async (e: Event) => {\n  if (!e.target) {\n    return;\n  }\n  // e.target.scrollTop 是元素顶部到当前可视区域顶部的距离，即已滚动的高度。\n  // e.target.clientHeight 是元素的可视高度。\n  // e.target.scrollHeight 是元素的总高度。\n  const { scrollTop, clientHeight, scrollHeight } = e.target as HTMLElement;\n  // 判断是否滚动到底部\n  const isBottom = scrollTop + clientHeight >= scrollHeight - bottomOffset;\n\n  // 滚动到底部且没有加载完成\n  if (isBottom && !isLoadComplete.value) {\n    loading.value = true;\n    page.value += 1;\n    const resp = await pageByTaskCopy({\n      pageSize: 10,\n      pageNum: page.value,\n      ...formData.value,\n    });\n    taskList.value.push(\n      ...resp.rows.map((item) => ({ ...item, active: false })),\n    );\n    loading.value = false;\n  }\n}, 200);\n\nconst lastSelectId = ref('');\nconst currentTask = ref<TaskInfo>();\nasync function handleCardClick(item: TaskInfo) {\n  const { id } = item;\n  // 点击的是同一个\n  if (lastSelectId.value === id) {\n    return;\n  }\n  currentTask.value = item;\n  // 反选状态 & 如果已经点击了 不变 & 保持只能有一个选中\n  taskList.value.forEach((item) => {\n    item.active = item.id === id;\n  });\n  lastSelectId.value = id;\n}\n\n// 由于失去焦点浮层会消失 使用v-model选择人员完毕后强制显示\nconst popoverOpen = ref(false);\nconst selectedUserList = ref<User[]>([]);\nfunction handleFinish(userList: User[]) {\n  popoverOpen.value = true;\n  selectedUserList.value = userList;\n  formData.value.createByIds = userList.map((item) => item.userId);\n}\n\nconst treeData = ref<any[]>([]);\nonMounted(async () => {\n  // menu\n  const tree = await categoryTree();\n  addFullName(tree, 'label', ' / ');\n  treeData.value = tree;\n});\n</script>\n\n<template>\n  <Page :auto-content-height=\"true\">\n    <div class=\"flex h-full gap-2\">\n      <div\n        class=\"bg-background relative flex h-full min-w-[320px] max-w-[320px] flex-col rounded-lg\"\n      >\n        <!-- 搜索条件 -->\n        <div\n          class=\"bg-background z-100 sticky left-0 top-0 w-full rounded-t-lg border-b-[1px] border-solid p-2\"\n        >\n          <div class=\"flex items-center gap-1\">\n            <InputSearch\n              v-model:value=\"formData.flowName\"\n              placeholder=\"流程名称搜索\"\n              @search=\"reload(false)\"\n            />\n            <Tooltip placement=\"top\" title=\"重置\">\n              <a-button @click=\"reload(true)\">\n                <RedoOutlined />\n              </a-button>\n            </Tooltip>\n            <Popover\n              v-model:open=\"popoverOpen\"\n              :get-popup-container=\"getPopupContainer\"\n              placement=\"rightTop\"\n              trigger=\"click\"\n            >\n              <template #title>\n                <div class=\"w-full border-b pb-[12px] text-[16px]\">搜索</div>\n              </template>\n              <template #content>\n                <Form\n                  :colon=\"false\"\n                  :label-col=\"{ span: 6 }\"\n                  :model=\"formData\"\n                  autocomplete=\"off\"\n                  class=\"w-[300px]\"\n                  @finish=\"() => reload(false)\"\n                >\n                  <FormItem label=\"申请人\">\n                    <!-- 弹窗关闭后仍然显示表单浮层 -->\n                    <CopyComponent\n                      v-model:user-list=\"selectedUserList\"\n                      @cancel=\"() => (popoverOpen = true)\"\n                      @finish=\"handleFinish\"\n                    />\n                  </FormItem>\n                  <FormItem label=\"流程分类\">\n                    <TreeSelect\n                      v-model:value=\"formData.category\"\n                      :allow-clear=\"true\"\n                      :field-names=\"{ label: 'label', value: 'id' }\"\n                      :get-popup-container=\"getPopupContainer\"\n                      :tree-data=\"treeData\"\n                      :tree-default-expand-all=\"true\"\n                      :tree-line=\"{ showLeafIcon: false }\"\n                      placeholder=\"请选择\"\n                      tree-node-filter-prop=\"label\"\n                      tree-node-label-prop=\"fullName\"\n                    />\n                  </FormItem>\n                  <FormItem label=\"任务名称\">\n                    <Input\n                      v-model:value=\"formData.nodeName\"\n                      placeholder=\"请输入\"\n                    />\n                  </FormItem>\n                  <FormItem label=\"流程编码\">\n                    <Input\n                      v-model:value=\"formData.flowCode\"\n                      placeholder=\"请输入\"\n                    />\n                  </FormItem>\n                  <FormItem>\n                    <div class=\"flex\">\n                      <a-button block html-type=\"submit\" type=\"primary\">\n                        搜索\n                      </a-button>\n                      <a-button block class=\"ml-2\" @click=\"reload(true)\">\n                        重置\n                      </a-button>\n                    </div>\n                  </FormItem>\n                </Form>\n              </template>\n              <a-button>\n                <FilterOutlined />\n              </a-button>\n            </Popover>\n          </div>\n        </div>\n        <div\n          ref=\"cardContainerRef\"\n          class=\"thin-scrollbar flex flex-1 flex-col gap-2 overflow-y-auto py-3\"\n          @scroll=\"handleScroll\"\n        >\n          <template v-if=\"taskList.length > 0\">\n            <ApprovalCard\n              v-for=\"item in taskList\"\n              :key=\"item.id\"\n              :info=\"item\"\n              class=\"mx-2\"\n              @click=\"handleCardClick(item)\"\n            />\n          </template>\n          <Empty v-else :image=\"emptyImage\" />\n          <div\n            v-if=\"isLoadComplete && taskList.length > 0\"\n            class=\"flex items-center justify-center text-[14px] opacity-50\"\n          >\n            没有更多数据了\n          </div>\n          <!-- 遮罩loading层 -->\n          <div\n            v-if=\"loading\"\n            class=\"absolute left-0 top-0 flex h-full w-full items-center justify-center bg-[rgba(0,0,0,0.1)]\"\n          >\n            <Spin tip=\"加载中...\" />\n          </div>\n        </div>\n        <!-- total显示 -->\n        <div\n          class=\"bg-background sticky bottom-0 w-full rounded-b-lg border-t-[1px] py-2\"\n        >\n          <div class=\"flex items-center justify-center\">\n            共 {{ taskTotal }} 条记录\n          </div>\n        </div>\n      </div>\n      <ApprovalPanel :task=\"currentTask\" type=\"readonly\" />\n    </div>\n  </Page>\n</template>\n\n<style lang=\"scss\" scoped>\n.thin-scrollbar {\n  &::-webkit-scrollbar {\n    width: 5px;\n  }\n}\n\n:deep(.ant-card-body) {\n  @apply thin-scrollbar;\n}\n</style>\n"
  },
  {
    "path": "apps/web-antd/src/views/workflow/task/taskFinish.vue",
    "content": "<!-- eslint-disable no-use-before-define -->\n<script setup lang=\"ts\">\nimport type { User } from '#/api/system/user/model';\nimport type { TaskInfo } from '#/api/workflow/task/model';\n\nimport { computed, onMounted, ref, useTemplateRef } from 'vue';\n\nimport { Page } from '@vben/common-ui';\nimport { addFullName, getPopupContainer } from '@vben/utils';\n\nimport { FilterOutlined, RedoOutlined } from '@ant-design/icons-vue';\nimport {\n  Empty,\n  Form,\n  FormItem,\n  Input,\n  InputSearch,\n  Popover,\n  Spin,\n  Tooltip,\n  TreeSelect,\n} from 'ant-design-vue';\nimport { cloneDeep, debounce } from 'lodash-es';\n\nimport { categoryTree } from '#/api/workflow/category';\nimport { pageByTaskFinish } from '#/api/workflow/task';\n\nimport { ApprovalCard, ApprovalPanel, CopyComponent } from '../components';\nimport { bottomOffset } from './constant';\n\nconst emptyImage = Empty.PRESENTED_IMAGE_SIMPLE;\n\nconst taskList = ref<(TaskInfo & { active: boolean })[]>([]);\nconst taskTotal = ref(0);\nconst page = ref(1);\nconst loading = ref(false);\n\nconst defaultFormData = {\n  flowName: '', // 流程定义名称\n  nodeName: '', // 任务名称\n  flowCode: '', // 流程定义编码\n  createByIds: [] as string[], // 创建人\n  category: null as null | number, // 流程分类\n};\nconst formData = ref(cloneDeep(defaultFormData));\n\n/**\n * 是否已经加载全部数据 即 taskList.length === taskTotal\n */\nconst isLoadComplete = computed(\n  () => taskList.value.length === taskTotal.value,\n);\n\n// 卡片父容器的ref\nconst cardContainerRef = useTemplateRef('cardContainerRef');\n\n/**\n * @param resetFields 是否清空查询参数\n */\nasync function reload(resetFields: boolean = false) {\n  // 需要先滚动到顶部\n  cardContainerRef.value?.scroll({ top: 0, behavior: 'auto' });\n\n  page.value = 1;\n  currentTask.value = undefined;\n  taskTotal.value = 0;\n  lastSelectId.value = '';\n\n  if (resetFields) {\n    formData.value = cloneDeep(defaultFormData);\n    selectedUserList.value = [];\n  }\n\n  loading.value = true;\n  const resp = await pageByTaskFinish({\n    pageSize: 10,\n    pageNum: page.value,\n    ...formData.value,\n  });\n  taskList.value = resp.rows.map((item) => ({ ...item, active: false }));\n  taskTotal.value = resp.total;\n\n  loading.value = false;\n  // 默认选中第一个\n  if (taskList.value.length > 0) {\n    const firstTask = taskList.value[0]!;\n    currentTask.value = firstTask;\n    handleCardClick(firstTask);\n  }\n}\n\nonMounted(reload);\n\nconst handleScroll = debounce(async (e: Event) => {\n  if (!e.target) {\n    return;\n  }\n  // e.target.scrollTop 是元素顶部到当前可视区域顶部的距离，即已滚动的高度。\n  // e.target.clientHeight 是元素的可视高度。\n  // e.target.scrollHeight 是元素的总高度。\n  const { scrollTop, clientHeight, scrollHeight } = e.target as HTMLElement;\n  // 判断是否滚动到底部\n  const isBottom = scrollTop + clientHeight >= scrollHeight - bottomOffset;\n\n  // 滚动到底部且没有加载完成\n  if (isBottom && !isLoadComplete.value) {\n    loading.value = true;\n    page.value += 1;\n    const resp = await pageByTaskFinish({\n      pageSize: 10,\n      pageNum: page.value,\n      ...formData.value,\n    });\n    taskList.value.push(\n      ...resp.rows.map((item) => ({ ...item, active: false })),\n    );\n    loading.value = false;\n  }\n}, 200);\n\nconst lastSelectId = ref('');\nconst currentTask = ref<TaskInfo>();\nasync function handleCardClick(item: TaskInfo) {\n  const { id } = item;\n  // 点击的是同一个\n  if (lastSelectId.value === id) {\n    return;\n  }\n  currentTask.value = item;\n  // 反选状态 & 如果已经点击了 不变 & 保持只能有一个选中\n  taskList.value.forEach((item) => {\n    item.active = item.id === id;\n  });\n  lastSelectId.value = id;\n}\n\n// 由于失去焦点浮层会消失 使用v-model选择人员完毕后强制显示\nconst popoverOpen = ref(false);\nconst selectedUserList = ref<User[]>([]);\nfunction handleFinish(userList: User[]) {\n  popoverOpen.value = true;\n  selectedUserList.value = userList;\n  formData.value.createByIds = userList.map((item) => item.userId);\n}\n\nconst treeData = ref<any[]>([]);\nonMounted(async () => {\n  // menu\n  const tree = await categoryTree();\n  addFullName(tree, 'label', ' / ');\n  treeData.value = tree;\n});\n</script>\n\n<template>\n  <Page :auto-content-height=\"true\">\n    <div class=\"flex h-full gap-2\">\n      <div\n        class=\"bg-background relative flex h-full min-w-[320px] max-w-[320px] flex-col rounded-lg\"\n      >\n        <!-- 搜索条件 -->\n        <div\n          class=\"bg-background z-100 sticky left-0 top-0 w-full rounded-t-lg border-b-[1px] border-solid p-2\"\n        >\n          <div class=\"flex items-center gap-1\">\n            <InputSearch\n              v-model:value=\"formData.flowName\"\n              placeholder=\"流程名称搜索\"\n              @search=\"reload(false)\"\n            />\n            <Tooltip placement=\"top\" title=\"重置\">\n              <a-button @click=\"reload(true)\">\n                <RedoOutlined />\n              </a-button>\n            </Tooltip>\n            <Popover\n              v-model:open=\"popoverOpen\"\n              :get-popup-container=\"getPopupContainer\"\n              placement=\"rightTop\"\n              trigger=\"click\"\n            >\n              <template #title>\n                <div class=\"w-full border-b pb-[12px] text-[16px]\">搜索</div>\n              </template>\n              <template #content>\n                <Form\n                  :colon=\"false\"\n                  :label-col=\"{ span: 6 }\"\n                  :model=\"formData\"\n                  autocomplete=\"off\"\n                  class=\"w-[300px]\"\n                  @finish=\"() => reload(false)\"\n                >\n                  <FormItem label=\"申请人\">\n                    <!-- 弹窗关闭后仍然显示表单浮层 -->\n                    <CopyComponent\n                      v-model:user-list=\"selectedUserList\"\n                      @cancel=\"() => (popoverOpen = true)\"\n                      @finish=\"handleFinish\"\n                    />\n                  </FormItem>\n                  <FormItem label=\"流程分类\">\n                    <TreeSelect\n                      v-model:value=\"formData.category\"\n                      :allow-clear=\"true\"\n                      :field-names=\"{ label: 'label', value: 'id' }\"\n                      :get-popup-container=\"getPopupContainer\"\n                      :tree-data=\"treeData\"\n                      :tree-default-expand-all=\"true\"\n                      :tree-line=\"{ showLeafIcon: false }\"\n                      placeholder=\"请选择\"\n                      tree-node-filter-prop=\"label\"\n                      tree-node-label-prop=\"fullName\"\n                    />\n                  </FormItem>\n                  <FormItem label=\"任务名称\">\n                    <Input\n                      v-model:value=\"formData.nodeName\"\n                      placeholder=\"请输入\"\n                    />\n                  </FormItem>\n                  <FormItem label=\"流程编码\">\n                    <Input\n                      v-model:value=\"formData.flowCode\"\n                      placeholder=\"请输入\"\n                    />\n                  </FormItem>\n                  <FormItem>\n                    <div class=\"flex\">\n                      <a-button block html-type=\"submit\" type=\"primary\">\n                        搜索\n                      </a-button>\n                      <a-button block class=\"ml-2\" @click=\"reload(true)\">\n                        重置\n                      </a-button>\n                    </div>\n                  </FormItem>\n                </Form>\n              </template>\n              <a-button>\n                <FilterOutlined />\n              </a-button>\n            </Popover>\n          </div>\n        </div>\n        <div\n          ref=\"cardContainerRef\"\n          class=\"thin-scrollbar flex flex-1 flex-col gap-2 overflow-y-auto py-3\"\n          @scroll=\"handleScroll\"\n        >\n          <template v-if=\"taskList.length > 0\">\n            <ApprovalCard\n              v-for=\"item in taskList\"\n              :key=\"item.id\"\n              :info=\"item\"\n              class=\"mx-2\"\n              @click=\"handleCardClick(item)\"\n            />\n          </template>\n          <Empty v-else :image=\"emptyImage\" />\n          <div\n            v-if=\"isLoadComplete && taskList.length > 0\"\n            class=\"flex items-center justify-center text-[14px] opacity-50\"\n          >\n            没有更多数据了\n          </div>\n          <!-- 遮罩loading层 -->\n          <div\n            v-if=\"loading\"\n            class=\"absolute left-0 top-0 flex h-full w-full items-center justify-center bg-[rgba(0,0,0,0.1)]\"\n          >\n            <Spin tip=\"加载中...\" />\n          </div>\n        </div>\n        <!-- total显示 -->\n        <div\n          class=\"bg-background sticky bottom-0 w-full rounded-b-lg border-t-[1px] py-2\"\n        >\n          <div class=\"flex items-center justify-center\">\n            共 {{ taskTotal }} 条记录\n          </div>\n        </div>\n      </div>\n      <ApprovalPanel :task=\"currentTask\" type=\"readonly\" />\n    </div>\n  </Page>\n</template>\n\n<style lang=\"scss\" scoped>\n.thin-scrollbar {\n  &::-webkit-scrollbar {\n    width: 5px;\n  }\n}\n\n:deep(.ant-card-body) {\n  @apply thin-scrollbar;\n}\n</style>\n"
  },
  {
    "path": "apps/web-antd/src/views/workflow/task/taskWaiting.vue",
    "content": "<!-- eslint-disable no-use-before-define -->\n<script setup lang=\"ts\">\nimport type { User } from '#/api/system/user/model';\nimport type { TaskInfo } from '#/api/workflow/task/model';\n\nimport { computed, onMounted, ref, useTemplateRef } from 'vue';\n\nimport { Page } from '@vben/common-ui';\nimport { useTabs } from '@vben/hooks';\nimport { addFullName, getPopupContainer } from '@vben/utils';\n\nimport { FilterOutlined, RedoOutlined } from '@ant-design/icons-vue';\nimport {\n  Empty,\n  Form,\n  FormItem,\n  Input,\n  InputSearch,\n  Popover,\n  Spin,\n  Tooltip,\n  TreeSelect,\n} from 'ant-design-vue';\nimport { cloneDeep, debounce } from 'lodash-es';\n\nimport { categoryTree } from '#/api/workflow/category';\nimport { pageByTaskWait } from '#/api/workflow/task';\n\nimport { ApprovalCard, ApprovalPanel, CopyComponent } from '../components';\nimport { bottomOffset } from './constant';\n\nconst emptyImage = Empty.PRESENTED_IMAGE_SIMPLE;\n\nconst taskList = ref<(TaskInfo & { active: boolean })[]>([]);\nconst taskTotal = ref(0);\nconst page = ref(1);\nconst loading = ref(false);\n\nconst defaultFormData = {\n  flowName: '', // 流程定义名称\n  nodeName: '', // 任务名称\n  flowCode: '', // 流程定义编码\n  createByIds: [] as string[], // 创建人\n  category: null as null | number, // 流程分类\n};\nconst formData = ref(cloneDeep(defaultFormData));\n\n/**\n * 是否已经加载全部数据 即 taskList.length === taskTotal\n */\nconst isLoadComplete = computed(\n  () => taskList.value.length === taskTotal.value,\n);\n\n// 卡片父容器的ref\nconst cardContainerRef = useTemplateRef('cardContainerRef');\n\n/**\n * @param resetFields 是否清空查询参数\n */\nasync function reload(resetFields: boolean = false) {\n  // 需要先滚动到顶部\n  cardContainerRef.value?.scroll({ top: 0, behavior: 'auto' });\n\n  page.value = 1;\n  currentTask.value = undefined;\n  taskTotal.value = 0;\n  lastSelectId.value = '';\n\n  if (resetFields) {\n    formData.value = cloneDeep(defaultFormData);\n    selectedUserList.value = [];\n  }\n\n  loading.value = true;\n  const resp = await pageByTaskWait({\n    pageSize: 10,\n    pageNum: page.value,\n    ...formData.value,\n  });\n  taskList.value = resp.rows.map((item) => ({ ...item, active: false }));\n  taskTotal.value = resp.total;\n\n  loading.value = false;\n  // 默认选中第一个\n  if (taskList.value.length > 0) {\n    const firstTask = taskList.value[0]!;\n    currentTask.value = firstTask;\n    handleCardClick(firstTask);\n  }\n}\n\nonMounted(reload);\n\nconst handleScroll = debounce(async (e: Event) => {\n  if (!e.target) {\n    return;\n  }\n  // e.target.scrollTop 是元素顶部到当前可视区域顶部的距离，即已滚动的高度。\n  // e.target.clientHeight 是元素的可视高度。\n  // e.target.scrollHeight 是元素的总高度。\n  const { scrollTop, clientHeight, scrollHeight } = e.target as HTMLElement;\n  // 判断是否滚动到底部\n  const isBottom = scrollTop + clientHeight >= scrollHeight - bottomOffset;\n\n  // 滚动到底部且没有加载完成\n  if (isBottom && !isLoadComplete.value) {\n    loading.value = true;\n    page.value += 1;\n    const resp = await pageByTaskWait({\n      pageSize: 10,\n      pageNum: page.value,\n      ...formData.value,\n    });\n    taskList.value.push(\n      ...resp.rows.map((item) => ({ ...item, active: false })),\n    );\n    loading.value = false;\n  }\n}, 200);\n\nconst lastSelectId = ref('');\nconst currentTask = ref<TaskInfo>();\nasync function handleCardClick(item: TaskInfo) {\n  const { id } = item;\n  // 点击的是同一个\n  if (lastSelectId.value === id) {\n    return;\n  }\n  currentTask.value = item;\n  // 反选状态 & 如果已经点击了 不变 & 保持只能有一个选中\n  taskList.value.forEach((item) => {\n    item.active = item.id === id;\n  });\n  lastSelectId.value = id;\n}\n\nconst { refreshTab } = useTabs();\n\n// 由于失去焦点浮层会消失 使用v-model选择人员完毕后强制显示\nconst popoverOpen = ref(false);\nconst selectedUserList = ref<User[]>([]);\nfunction handleFinish(userList: User[]) {\n  popoverOpen.value = true;\n  selectedUserList.value = userList;\n  formData.value.createByIds = userList.map((item) => item.userId);\n}\n\nconst treeData = ref<any[]>([]);\nonMounted(async () => {\n  // menu\n  const tree = await categoryTree();\n  addFullName(tree, 'label', ' / ');\n  treeData.value = tree;\n});\n</script>\n\n<template>\n  <Page :auto-content-height=\"true\">\n    <div class=\"flex h-full gap-2\">\n      <div\n        class=\"bg-background relative flex h-full min-w-[320px] max-w-[320px] flex-col rounded-lg\"\n      >\n        <!-- 搜索条件 -->\n        <div\n          class=\"bg-background z-100 sticky left-0 top-0 w-full rounded-t-lg border-b-[1px] border-solid p-2\"\n        >\n          <div class=\"flex items-center gap-1\">\n            <InputSearch\n              v-model:value=\"formData.flowName\"\n              placeholder=\"流程名称搜索\"\n              @search=\"reload(false)\"\n            />\n            <Tooltip placement=\"top\" title=\"重置\">\n              <a-button @click=\"reload(true)\">\n                <RedoOutlined />\n              </a-button>\n            </Tooltip>\n            <Popover\n              v-model:open=\"popoverOpen\"\n              :get-popup-container=\"getPopupContainer\"\n              placement=\"rightTop\"\n              trigger=\"click\"\n            >\n              <template #title>\n                <div class=\"w-full border-b pb-[12px] text-[16px]\">搜索</div>\n              </template>\n              <template #content>\n                <Form\n                  :colon=\"false\"\n                  :label-col=\"{ span: 6 }\"\n                  :model=\"formData\"\n                  autocomplete=\"off\"\n                  class=\"w-[300px]\"\n                  @finish=\"() => reload(false)\"\n                >\n                  <FormItem label=\"申请人\">\n                    <!-- 弹窗关闭后仍然显示表单浮层 -->\n                    <CopyComponent\n                      v-model:user-list=\"selectedUserList\"\n                      @cancel=\"() => (popoverOpen = true)\"\n                      @finish=\"handleFinish\"\n                    />\n                  </FormItem>\n                  <FormItem label=\"流程分类\">\n                    <TreeSelect\n                      v-model:value=\"formData.category\"\n                      :allow-clear=\"true\"\n                      :field-names=\"{ label: 'label', value: 'id' }\"\n                      :get-popup-container=\"getPopupContainer\"\n                      :tree-data=\"treeData\"\n                      :tree-default-expand-all=\"true\"\n                      :tree-line=\"{ showLeafIcon: false }\"\n                      placeholder=\"请选择\"\n                      tree-node-filter-prop=\"label\"\n                      tree-node-label-prop=\"fullName\"\n                    />\n                  </FormItem>\n                  <FormItem label=\"任务名称\">\n                    <Input\n                      v-model:value=\"formData.nodeName\"\n                      placeholder=\"请输入\"\n                    />\n                  </FormItem>\n                  <FormItem label=\"流程编码\">\n                    <Input\n                      v-model:value=\"formData.flowCode\"\n                      placeholder=\"请输入\"\n                    />\n                  </FormItem>\n                  <FormItem>\n                    <div class=\"flex\">\n                      <a-button block html-type=\"submit\" type=\"primary\">\n                        搜索\n                      </a-button>\n                      <a-button block class=\"ml-2\" @click=\"reload(true)\">\n                        重置\n                      </a-button>\n                    </div>\n                  </FormItem>\n                </Form>\n              </template>\n              <a-button>\n                <FilterOutlined />\n              </a-button>\n            </Popover>\n          </div>\n        </div>\n        <div\n          ref=\"cardContainerRef\"\n          class=\"thin-scrollbar flex flex-1 flex-col gap-2 overflow-y-auto py-3\"\n          @scroll=\"handleScroll\"\n        >\n          <template v-if=\"taskList.length > 0\">\n            <ApprovalCard\n              v-for=\"item in taskList\"\n              :key=\"item.id\"\n              :info=\"item\"\n              class=\"mx-2\"\n              @click=\"handleCardClick(item)\"\n            />\n          </template>\n          <Empty v-else :image=\"emptyImage\" />\n          <div\n            v-if=\"isLoadComplete && taskList.length > 0\"\n            class=\"flex items-center justify-center text-[14px] opacity-50\"\n          >\n            没有更多数据了\n          </div>\n          <!-- 遮罩loading层 -->\n          <div\n            v-if=\"loading\"\n            class=\"absolute left-0 top-0 flex h-full w-full items-center justify-center bg-[rgba(0,0,0,0.1)]\"\n          >\n            <Spin tip=\"加载中...\" />\n          </div>\n        </div>\n        <!-- total显示 -->\n        <div\n          class=\"bg-background sticky bottom-0 w-full rounded-b-lg border-t-[1px] py-2\"\n        >\n          <div class=\"flex items-center justify-center\">\n            共 {{ taskTotal }} 条记录\n          </div>\n        </div>\n      </div>\n      <ApprovalPanel :task=\"currentTask\" type=\"approve\" @reload=\"refreshTab\" />\n    </div>\n  </Page>\n</template>\n\n<style lang=\"scss\" scoped>\n.thin-scrollbar {\n  &::-webkit-scrollbar {\n    width: 5px;\n  }\n}\n\n:deep(.ant-card-body) {\n  @apply thin-scrollbar;\n}\n</style>\n"
  },
  {
    "path": "apps/web-antd/tailwind.config.mjs",
    "content": "export { default } from '@vben/tailwind-config';\n"
  },
  {
    "path": "apps/web-antd/tsconfig.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/tsconfig\",\n  \"extends\": \"@vben/tsconfig/web-app.json\",\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"#/*\": [\"./src/*\", \"../../../packages/*\"]\n    }\n  },\n  \"references\": [{ \"path\": \"./tsconfig.node.json\" }],\n  \"include\": [\"src/**/*.ts\", \"src/**/*.tsx\", \"src/**/*.vue\", \"types/**/*.d.ts\"]\n}\n"
  },
  {
    "path": "apps/web-antd/tsconfig.node.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/tsconfig\",\n  \"extends\": \"@vben/tsconfig/node.json\",\n  \"compilerOptions\": {\n    \"composite\": true,\n    \"tsBuildInfoFile\": \"./node_modules/.tmp/tsconfig.node.tsbuildinfo\",\n    \"noEmit\": false\n  },\n  \"include\": [\"vite.config.mts\"]\n}\n"
  },
  {
    "path": "apps/web-antd/types/directive.d.ts",
    "content": "import type { Directive } from 'vue';\n\ndeclare module 'vue' {\n  export interface ComponentCustomProperties {\n    /**\n     * 判断权限: v-access:code=\"\"\n     * 判断角色  v-access:role=\"\"\n     * 需要VueOfficial插件版本 >= 2.1.8\n     */\n    vAccess: Directive<Element, string | string[], string, 'code' | 'role'>;\n  }\n}\n"
  },
  {
    "path": "apps/web-antd/types/global-components.d.ts",
    "content": "export {};\n\n/* prettier-ignore */\ndeclare module 'vue' {\n  export interface GlobalComponents {\n    AButton: typeof import('ant-design-vue/es/button')['default'];\n    GhostButton: typeof import('#/components/global/button')['GhostButton']\n  }\n}\n"
  },
  {
    "path": "apps/web-antd/vite.config.mts",
    "content": "import { defineConfig } from '@vben/vite-config';\nimport { resolve } from 'path';\n\n// 自行取消注释来启用按需导入功能\n// import { AntDesignVueResolver } from 'unplugin-vue-components/resolvers';\n// import Components from 'unplugin-vue-components/vite';\n\nexport default defineConfig(async () => {\n  return {\n    application: {},\n    vite: {\n      define: {\n        // 注入项目根路径到运行时（ruoyi-admin目录层级）\n        __PROJECT_ROOT__: JSON.stringify((() => {\n          const cwd = process.cwd();\n          console.log('Vite当前工作目录:', cwd);\n\n          // 规范化路径分隔符，统一处理 Windows 和 macOS/Linux\n          const normalizedPath = cwd.replace(/\\\\/g, '/');\n\n          // 如果当前目录包含 /apps/web-antd，则向上两级到 ruoyi-admin\n          if (normalizedPath.includes('/apps/web-antd')) {\n            return resolve(cwd, '../..');\n          }\n          // 如果当前目录包含 /apps，则向上一级到 ruoyi-admin\n          else if (normalizedPath.includes('/apps')) {\n            return resolve(cwd, '..');\n          }\n          // 否则返回当前目录\n          else {\n            return cwd;\n          }\n        })()),\n      },\n      // 解决 jiti 构建问题\n      optimizeDeps: {\n        include: ['jiti'],\n      },\n      ssr: {\n        noExternal: ['jiti'],\n      },\n      build: {\n        commonjsOptions: {\n          transformMixedEsModules: true,\n        },\n        rollupOptions: {\n          external: [\n            /node_modules\\/jiti\\//,\n          ],\n        },\n        target: 'es2022',\n      },\n      plugins: [\n        // Components({\n        //   dirs: [], // 默认会导入src/components目录下所有组件 不需要\n        //   dts: './types/components.d.ts', // 输出类型文件\n        //   resolvers: [\n        //     AntDesignVueResolver({\n        //       // 需要排除Button组件 全局已经默认导入了\n        //       exclude: ['Button'],\n        //       importStyle: false, // css in js\n        //     }),\n        //   ],\n        // }),\n      ],\n      server: {\n        proxy: {\n          '/api': {\n            changeOrigin: true,\n            rewrite: (path) => path.replace(/^\\/api/, ''),\n            // mock代理目标地址\n            target: 'http://127.0.0.1:6039',\n            ws: true,\n          },\n        },\n      },\n    },\n  };\n});\n"
  },
  {
    "path": "docker-compose-all.yaml",
    "content": "# RuoYi-AI 一键启动全部服务\n# 使用方式: docker-compose up -d\n#\n# 包含服务:\n#   - MySQL 8.0 (数据库，包含初始化SQL)\n#   - Redis 6.2 (缓存)\n#   - Weaviate (向量数据库)\n#   - MinIO (对象存储)\n#   - RuoYi-Backend (后端服务)\n#   - RuoYi-Admin (管理端前端)\n#   - RuoYi-Web (用户端前端)\n#\n# 镜像仓库地址: crpi-31mraxd99y2gqdgr.cn-beijing.personal.cr.aliyuncs.com/ruoyi_ai\n\nversion: '3.8'\n\nservices:\n  # ==================== MySQL 数据库 ====================\n  mysql:\n    # 阿里云镜像地址（包含初始化SQL）\n    image: crpi-31mraxd99y2gqdgr.cn-beijing.personal.cr.aliyuncs.com/ruoyi_ai/mysql:v3\n    container_name: ruoyi-ai-mysql\n    restart: always\n    ports:\n      - \"23306:3306\"\n    environment:\n      MYSQL_ROOT_PASSWORD: root\n      MYSQL_DATABASE: ruoyi-ai-agent\n      TZ: Asia/Shanghai\n    volumes:\n      - mysql-data:/var/lib/mysql\n    healthcheck:\n      test: [\"CMD\", \"mysqladmin\", \"ping\", \"-h\", \"localhost\", \"-u\", \"root\", \"-proot\"]\n      interval: 15s\n      timeout: 10s\n      retries: 10\n      start_period: 60s\n    networks:\n      - ruoyi-net\n\n  # ==================== Redis 缓存 ====================\n  redis:\n    image: crpi-31mraxd99y2gqdgr.cn-beijing.personal.cr.aliyuncs.com/ruoyi_ai/redis:6.2\n    container_name: ruoyi-ai-redis\n    restart: always\n    ports:\n      - \"26379:6379\"\n    volumes:\n      - redis-data:/data\n    command: redis-server --appendonly yes\n    healthcheck:\n      test: [\"CMD\", \"redis-cli\", \"ping\"]\n      interval: 10s\n      timeout: 5s\n      retries: 5\n    networks:\n      - ruoyi-net\n\n  # ==================== Weaviate 向量数据库 ====================\n  weaviate:\n    image: crpi-31mraxd99y2gqdgr.cn-beijing.personal.cr.aliyuncs.com/ruoyi_ai/weaviate:1.30.0\n    container_name: ruoyi-ai-weaviate\n    restart: always\n    ports:\n      - \"28080:8080\"\n    environment:\n      QUERY_DEFAULTS_LIMIT: 25\n      AUTHENTICATION_ANONYMOUS_ACCESS_ENABLED: true\n      PERSISTENCE_DATA_PATH: /var/lib/weaviate\n      DEFAULT_VECTORIZER_MODULE: none\n      ENABLE_MODULES: text2vec-cohere,text2vec-huggingface,text2vec-palm,text2vec-openai,generative-openai,generative-cohere,generative-palm,ref2vec-centroid,reranker-cohere,qna-openai\n      CLUSTER_HOSTNAME: node1\n    volumes:\n      - weaviate-data:/var/lib/weaviate\n    networks:\n      - ruoyi-net\n\n  # ==================== MinIO 对象存储 ====================\n  minio:\n    image: crpi-31mraxd99y2gqdgr.cn-beijing.personal.cr.aliyuncs.com/ruoyi_ai/minio:latest\n    container_name: ruoyi-ai-minio\n    restart: always\n    ports:\n      - \"29000:9000\"\n      - \"29090:9090\"\n    environment:\n      MINIO_ROOT_USER: ruoyi\n      MINIO_ROOT_PASSWORD: ruoyi123\n    volumes:\n      - minio-data:/data\n    command: server /data --console-address \":9090\"\n    networks:\n      - ruoyi-net\n\n  # ==================== RuoYi-AI 后端服务 ====================\n  backend:\n    image: crpi-31mraxd99y2gqdgr.cn-beijing.personal.cr.aliyuncs.com/ruoyi_ai/ruoyi-ai-backend:latest\n    container_name: ruoyi-ai-backend\n    restart: always\n    ports:\n      - \"26039:6039\"\n    environment:\n      TZ: Asia/Shanghai\n      # MySQL 配置\n      SPRING_DATASOURCE_DYNAMIC_PRIMARY: master\n      SPRING_DATASOURCE_DYNAMIC_DATASOURCE_MASTER_DRIVERCLASSNAME: com.mysql.cj.jdbc.Driver\n      SPRING_DATASOURCE_DYNAMIC_DATASOURCE_MASTER_URL: jdbc:mysql://mysql:3306/ruoyi-ai-agent?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true\n      SPRING_DATASOURCE_DYNAMIC_DATASOURCE_MASTER_USERNAME: root\n      SPRING_DATASOURCE_DYNAMIC_DATASOURCE_MASTER_PASSWORD: root\n      # Redis 配置\n      SPRING_DATA_REDIS_HOST: redis\n      SPRING_DATA_REDIS_PORT: 6379\n      SPRING_DATA_REDIS_DATABASE: 0\n      # 日志配置\n      LOGGING_LEVEL_ORG_RUOYI: info\n      LOGGING_LEVEL_ORG_SPRINGFRAMEWORK: warn\n      SYS_UPLOAD_PATH: /ruoyi/upload\n    volumes:\n      - logs-data:/ruoyi/server/logs\n      - upload-data:/ruoyi/upload\n    depends_on:\n      mysql:\n        condition: service_healthy\n      redis:\n        condition: service_started\n    networks:\n      - ruoyi-net\n\n  # ==================== RuoYi-AI 管理端前端 ====================\n  admin-frontend:\n    image: crpi-31mraxd99y2gqdgr.cn-beijing.personal.cr.aliyuncs.com/ruoyi_ai/ruoyi-ai-admin:latest\n    container_name: ruoyi-ai-admin\n    restart: always\n    ports:\n      - \"25666:5666\"\n    environment:\n      # 后端 API 地址 - 运行时动态配置（无需重新构建镜像）\n      # 在完整 docker-compose 中应设置为: http://backend:6039\n      # 独立运行时可设置为实际后端地址，如: 192.168.1.100:6039\n      UPSTREAM_HOST: backend:6039\n    # 资源限制 - 防止 CPU 和内存耗尽\n    deploy:\n      resources:\n        limits:\n          cpus: '2'\n          memory: 3G\n        reservations:\n          cpus: '1'\n          memory: 1G\n    depends_on:\n      - backend\n    networks:\n      - ruoyi-net\n\n  # ==================== RuoYi-AI 用户端前端 ====================\n  web-frontend:\n    image: crpi-31mraxd99y2gqdgr.cn-beijing.personal.cr.aliyuncs.com/ruoyi_ai/ruoyi-ai-web:latest\n    container_name: ruoyi-ai-web\n    restart: always\n    ports:\n      - \"25137:5137\"\n    environment:\n      UPSTREAM_URL: http://backend:6039\n    depends_on:\n      - backend\n    networks:\n      - ruoyi-net\n\n# ==================== 网络配置 ====================\nnetworks:\n  ruoyi-net:\n    driver: bridge\n\n# ==================== 数据卷配置 ====================\nvolumes:\n  mysql-data:\n  redis-data:\n  weaviate-data:\n  minio-data:\n  logs-data:\n  upload-data:"
  },
  {
    "path": "eslint.config.mjs",
    "content": "// @ts-check\n\nimport { defineConfig } from '@vben/eslint-config';\n\nexport default defineConfig();\n"
  },
  {
    "path": "internal/lint-configs/commitlint-config/index.mjs",
    "content": "import { execSync } from 'node:child_process';\n\nimport { getPackagesSync } from '@vben/node-utils';\n\nconst { packages } = getPackagesSync();\n\nconst allowedScopes = [\n  ...packages.map((pkg) => pkg.packageJson.name),\n  'project',\n  'style',\n  'lint',\n  'ci',\n  'dev',\n  'deploy',\n  'other',\n];\n\n// precomputed scope\nconst scopeComplete = execSync('git status --porcelain || true')\n  .toString()\n  .trim()\n  .split('\\n')\n  .find((r) => ~r.indexOf('M  src'))\n  ?.replace(/(\\/)/g, '%%')\n  ?.match(/src%%((\\w|-)*)/)?.[1]\n  ?.replace(/s$/, '');\n\n/**\n * @type {import('cz-git').UserConfig}\n */\nconst userConfig = {\n  extends: ['@commitlint/config-conventional'],\n  plugins: ['commitlint-plugin-function-rules'],\n  prompt: {\n    /** @use `pnpm commit :f` */\n    alias: {\n      b: 'build: bump dependencies',\n      c: 'chore: update config',\n      f: 'docs: fix typos',\n      r: 'docs: update README',\n      s: 'style: update code format',\n    },\n    allowCustomIssuePrefixs: false,\n    // scopes: [...scopes, 'mock'],\n    allowEmptyIssuePrefixs: false,\n    customScopesAlign: scopeComplete ? 'bottom' : 'top',\n    defaultScope: scopeComplete,\n    // English\n    typesAppend: [\n      { name: 'workflow: workflow improvements', value: 'workflow' },\n      { name: 'types:    type definition file changes', value: 'types' },\n    ],\n\n    // 中英文对照版\n    // messages: {\n    //   type: '选择你要提交的类型 :',\n    //   scope: '选择一个提交范围 (可选):',\n    //   customScope: '请输入自定义的提交范围 :',\n    //   subject: '填写简短精炼的变更描述 :\\n',\n    //   body: '填写更加详细的变更描述 (可选)。使用 \"|\" 换行 :\\n',\n    //   breaking: '列举非兼容性重大的变更 (可选)。使用 \"|\" 换行 :\\n',\n    //   footerPrefixsSelect: '选择关联issue前缀 (可选):',\n    //   customFooterPrefixs: '输入自定义issue前缀 :',\n    //   footer: '列举关联issue (可选) 例如: #31, #I3244 :\\n',\n    //   confirmCommit: '是否提交或修改commit ?',\n    // },\n    // types: [\n    //   { value: 'feat', name: 'feat:     新增功能' },\n    //   { value: 'fix', name: 'fix:      修复缺陷' },\n    //   { value: 'docs', name: 'docs:     文档变更' },\n    //   { value: 'style', name: 'style:    代码格式' },\n    //   { value: 'refactor', name: 'refactor: 代码重构' },\n    //   { value: 'perf', name: 'perf:     性能优化' },\n    //   { value: 'test', name: 'test:     添加疏漏测试或已有测试改动' },\n    //   { value: 'build', name: 'build:    构建流程、外部依赖变更 (如升级 npm 包、修改打包配置等)' },\n    //   { value: 'ci', name: 'ci:       修改 CI 配置、脚本' },\n    //   { value: 'revert', name: 'revert:   回滚 commit' },\n    //   { value: 'chore', name: 'chore:    对构建过程或辅助工具和库的更改 (不影响源文件、测试用例)' },\n    //   { value: 'wip', name: 'wip:      正在开发中' },\n    //   { value: 'workflow', name: 'workflow: 工作流程改进' },\n    //   { value: 'types', name: 'types:    类型定义文件修改' },\n    // ],\n    // emptyScopesAlias: 'empty:      不填写',\n    // customScopesAlias: 'custom:     自定义',\n  },\n  rules: {\n    /**\n     * type[scope]: [function] description\n     *\n     * ^^^^^^^^^^^^^^ empty line.\n     * - Something here\n     */\n    'body-leading-blank': [2, 'always'],\n    /**\n     * type[scope]: [function] description\n     *\n     * - something here\n     *\n     * ^^^^^^^^^^^^^^\n     */\n    'footer-leading-blank': [1, 'always'],\n    /**\n     * type[scope]: [function] description\n     *      ^^^^^\n     */\n    'function-rules/scope-enum': [\n      2, // level: error\n      'always',\n      (parsed) => {\n        if (!parsed.scope || allowedScopes.includes(parsed.scope)) {\n          return [true];\n        }\n\n        return [false, `scope must be one of ${allowedScopes.join(', ')}`];\n      },\n    ],\n    /**\n     * type[scope]: [function] description [No more than 108 characters]\n     *      ^^^^^\n     */\n    'header-max-length': [2, 'always', 108],\n\n    'scope-enum': [0],\n    'subject-case': [0],\n    'subject-empty': [2, 'never'],\n    'type-empty': [2, 'never'],\n    /**\n     * type[scope]: [function] description\n     * ^^^^\n     */\n    'type-enum': [\n      2,\n      'always',\n      [\n        'feat',\n        'fix',\n        'perf',\n        'style',\n        'docs',\n        'test',\n        'refactor',\n        'build',\n        'ci',\n        'chore',\n        'revert',\n        'types',\n        'release',\n        'update',\n      ],\n    ],\n  },\n};\n\nexport default userConfig;\n"
  },
  {
    "path": "internal/lint-configs/commitlint-config/package.json",
    "content": "{\n  \"name\": \"@vben/commitlint-config\",\n  \"version\": \"5.5.9\",\n  \"private\": true,\n  \"homepage\": \"https://github.com/vbenjs/vue-vben-admin\",\n  \"bugs\": \"https://github.com/vbenjs/vue-vben-admin/issues\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/vbenjs/vue-vben-admin.git\",\n    \"directory\": \"internal/lint-configs/commitlint-config\"\n  },\n  \"license\": \"MIT\",\n  \"type\": \"module\",\n  \"files\": [\n    \"dist\"\n  ],\n  \"main\": \"./index.mjs\",\n  \"module\": \"./index.mjs\",\n  \"exports\": {\n    \".\": {\n      \"import\": \"./index.mjs\",\n      \"default\": \"./index.mjs\"\n    }\n  },\n  \"dependencies\": {\n    \"@commitlint/cli\": \"catalog:\",\n    \"@commitlint/config-conventional\": \"catalog:\",\n    \"@vben/node-utils\": \"workspace:*\",\n    \"commitlint-plugin-function-rules\": \"catalog:\",\n    \"cz-git\": \"catalog:\",\n    \"czg\": \"catalog:\"\n  }\n}\n"
  },
  {
    "path": "internal/lint-configs/eslint-config/build.config.ts",
    "content": "import { defineBuildConfig } from 'unbuild';\n\nexport default defineBuildConfig({\n  clean: true,\n  declaration: true,\n  entries: ['src/index'],\n});\n"
  },
  {
    "path": "internal/lint-configs/eslint-config/package.json",
    "content": "{\n  \"name\": \"@vben/eslint-config\",\n  \"version\": \"5.0.0\",\n  \"private\": true,\n  \"homepage\": \"https://github.com/vbenjs/vue-vben-admin\",\n  \"bugs\": \"https://github.com/vbenjs/vue-vben-admin/issues\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/vbenjs/vue-vben-admin.git\",\n    \"directory\": \"internal/lint-configs/eslint-config\"\n  },\n  \"license\": \"MIT\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"stub\": \"pnpm unbuild --stub\"\n  },\n  \"files\": [\n    \"dist\"\n  ],\n  \"main\": \"./dist/index.mjs\",\n  \"module\": \"./dist/index.mjs\",\n  \"types\": \"./dist/index.d.ts\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.ts\",\n      \"import\": \"./dist/index.mjs\"\n    }\n  },\n  \"dependencies\": {\n    \"eslint-config-turbo\": \"catalog:\",\n    \"eslint-plugin-command\": \"catalog:\",\n    \"eslint-plugin-import-x\": \"catalog:\"\n  },\n  \"devDependencies\": {\n    \"@eslint/js\": \"catalog:\",\n    \"@types/eslint\": \"catalog:\",\n    \"@typescript-eslint/eslint-plugin\": \"catalog:\",\n    \"@typescript-eslint/parser\": \"catalog:\",\n    \"eslint\": \"catalog:\",\n    \"eslint-plugin-eslint-comments\": \"catalog:\",\n    \"eslint-plugin-jsdoc\": \"catalog:\",\n    \"eslint-plugin-jsonc\": \"catalog:\",\n    \"eslint-plugin-n\": \"catalog:\",\n    \"eslint-plugin-no-only-tests\": \"catalog:\",\n    \"eslint-plugin-perfectionist\": \"catalog:\",\n    \"eslint-plugin-prettier\": \"catalog:\",\n    \"eslint-plugin-regexp\": \"catalog:\",\n    \"eslint-plugin-unicorn\": \"catalog:\",\n    \"eslint-plugin-unused-imports\": \"catalog:\",\n    \"eslint-plugin-vitest\": \"catalog:\",\n    \"eslint-plugin-vue\": \"catalog:\",\n    \"globals\": \"catalog:\",\n    \"jsonc-eslint-parser\": \"catalog:\",\n    \"vue-eslint-parser\": \"catalog:\"\n  }\n}\n"
  },
  {
    "path": "internal/lint-configs/eslint-config/src/configs/command.ts",
    "content": "import createCommand from 'eslint-plugin-command/config';\n\nexport async function command() {\n  return [\n    {\n      // @ts-expect-error - no types\n      ...createCommand(),\n    },\n  ];\n}\n"
  },
  {
    "path": "internal/lint-configs/eslint-config/src/configs/comments.ts",
    "content": "import type { Linter } from 'eslint';\n\nimport { interopDefault } from '../util';\n\nexport async function comments(): Promise<Linter.Config[]> {\n  const [pluginComments] = await Promise.all([\n    // @ts-expect-error - no types\n    interopDefault(import('eslint-plugin-eslint-comments')),\n  ] as const);\n\n  return [\n    {\n      plugins: {\n        'eslint-comments': pluginComments,\n      },\n      rules: {\n        'eslint-comments/no-aggregating-enable': 'error',\n        'eslint-comments/no-duplicate-disable': 'error',\n        'eslint-comments/no-unlimited-disable': 'error',\n        'eslint-comments/no-unused-enable': 'error',\n      },\n    },\n  ];\n}\n"
  },
  {
    "path": "internal/lint-configs/eslint-config/src/configs/disableds.ts",
    "content": "import type { Linter } from 'eslint';\n\nexport async function disableds(): Promise<Linter.Config[]> {\n  return [\n    {\n      files: ['**/__tests__/**/*.?([cm])[jt]s?(x)'],\n      name: 'disables/test',\n      rules: {\n        '@typescript-eslint/ban-ts-comment': 'off',\n        'no-console': 'off',\n      },\n    },\n    {\n      files: ['**/*.d.ts'],\n      name: 'disables/dts',\n      rules: {\n        '@typescript-eslint/triple-slash-reference': 'off',\n      },\n    },\n    {\n      files: ['**/*.js', '**/*.mjs', '**/*.cjs'],\n      name: 'disables/js',\n      rules: {\n        '@typescript-eslint/explicit-module-boundary-types': 'off',\n      },\n    },\n  ];\n}\n"
  },
  {
    "path": "internal/lint-configs/eslint-config/src/configs/ignores.ts",
    "content": "import type { Linter } from 'eslint';\n\nexport async function ignores(): Promise<Linter.Config[]> {\n  return [\n    {\n      ignores: [\n        '**/node_modules',\n        '**/dist',\n        '**/dist-*',\n        '**/*-dist',\n        '**/.husky',\n        '**/.nitro',\n        '**/.output',\n        '**/Dockerfile',\n        '**/package-lock.json',\n        '**/yarn.lock',\n        '**/pnpm-lock.yaml',\n        '**/bun.lockb',\n        '**/output',\n        '**/coverage',\n        '**/temp',\n        '**/.temp',\n        '**/tmp',\n        '**/.tmp',\n        '**/.history',\n        '**/.turbo',\n        '**/.nuxt',\n        '**/.next',\n        '**/.vercel',\n        '**/.changeset',\n        '**/.idea',\n        '**/.cache',\n        '**/.output',\n        '**/.vite-inspect',\n\n        '**/CHANGELOG*.md',\n        '**/*.min.*',\n        '**/LICENSE*',\n        '**/__snapshots__',\n        '**/*.snap',\n        '**/fixtures/**',\n        '**/.vitepress/cache/**',\n        '**/auto-import?(s).d.ts',\n        '**/components.d.ts',\n        '**/vite.config.mts.*',\n        '**/*.sh',\n        '**/*.ttf',\n        '**/*.woff',\n      ],\n    },\n  ];\n}\n"
  },
  {
    "path": "internal/lint-configs/eslint-config/src/configs/import.ts",
    "content": "import type { Linter } from 'eslint';\n\nimport * as pluginImport from 'eslint-plugin-import-x';\n\nexport async function importPluginConfig(): Promise<Linter.Config[]> {\n  return [\n    {\n      plugins: {\n        // @ts-expect-error - This is a dynamic import\n        import: pluginImport,\n      },\n      rules: {\n        'import/consistent-type-specifier-style': ['error', 'prefer-top-level'],\n        'import/first': 'error',\n        'import/newline-after-import': 'error',\n        'import/no-duplicates': 'error',\n        'import/no-mutable-exports': 'error',\n        'import/no-named-default': 'error',\n        'import/no-self-import': 'error',\n        'import/no-unresolved': 'off',\n        'import/no-webpack-loader-syntax': 'error',\n      },\n    },\n  ];\n}\n"
  },
  {
    "path": "internal/lint-configs/eslint-config/src/configs/index.ts",
    "content": "export * from './command';\nexport * from './comments';\nexport * from './disableds';\nexport * from './ignores';\nexport * from './import';\nexport * from './javascript';\nexport * from './jsdoc';\nexport * from './jsonc';\nexport * from './node';\nexport * from './perfectionist';\nexport * from './prettier';\nexport * from './regexp';\nexport * from './test';\nexport * from './turbo';\nexport * from './typescript';\nexport * from './unicorn';\nexport * from './vue';\n"
  },
  {
    "path": "internal/lint-configs/eslint-config/src/configs/javascript.ts",
    "content": "import type { Linter } from 'eslint';\n\nimport js from '@eslint/js';\nimport pluginUnusedImports from 'eslint-plugin-unused-imports';\nimport globals from 'globals';\n\nexport async function javascript(): Promise<Linter.Config[]> {\n  return [\n    {\n      languageOptions: {\n        ecmaVersion: 'latest',\n        globals: {\n          ...globals.browser,\n          ...globals.es2021,\n          ...globals.node,\n          document: 'readonly',\n          navigator: 'readonly',\n          window: 'readonly',\n        },\n        parserOptions: {\n          ecmaFeatures: {\n            jsx: true,\n          },\n          ecmaVersion: 'latest',\n          sourceType: 'module',\n        },\n        sourceType: 'module',\n      },\n      linterOptions: {\n        reportUnusedDisableDirectives: true,\n      },\n      plugins: {\n        'unused-imports': pluginUnusedImports,\n      },\n      rules: {\n        ...js.configs.recommended.rules,\n        'accessor-pairs': [\n          'error',\n          { enforceForClassMembers: true, setWithoutGet: true },\n        ],\n        'array-callback-return': 'error',\n        'block-scoped-var': 'error',\n        'constructor-super': 'error',\n        'default-case-last': 'error',\n        'dot-notation': ['error', { allowKeywords: true }],\n        eqeqeq: ['error', 'always'],\n        'keyword-spacing': 'off',\n\n        'new-cap': [\n          'error',\n          { capIsNew: false, newIsCap: true, properties: true },\n        ],\n        'no-alert': 'error',\n        'no-array-constructor': 'error',\n        'no-async-promise-executor': 'error',\n        'no-caller': 'error',\n        'no-case-declarations': 'error',\n        'no-class-assign': 'error',\n        'no-compare-neg-zero': 'error',\n        'no-cond-assign': ['error', 'always'],\n        'no-console': ['error', { allow: ['warn', 'error'] }],\n        'no-const-assign': 'error',\n        'no-control-regex': 'error',\n        'no-debugger': 'error',\n        'no-delete-var': 'error',\n        'no-dupe-args': 'error',\n        'no-dupe-class-members': 'error',\n        'no-dupe-keys': 'error',\n        'no-duplicate-case': 'error',\n        'no-empty': ['error', { allowEmptyCatch: true }],\n        'no-empty-character-class': 'error',\n        'no-empty-function': 'off',\n        'no-empty-pattern': 'error',\n        'no-eval': 'error',\n        'no-ex-assign': 'error',\n        'no-extend-native': 'error',\n        'no-extra-bind': 'error',\n        'no-extra-boolean-cast': 'error',\n        'no-fallthrough': 'error',\n        'no-func-assign': 'error',\n        'no-global-assign': 'error',\n        'no-implied-eval': 'error',\n        'no-import-assign': 'error',\n        'no-invalid-regexp': 'error',\n        'no-irregular-whitespace': 'error',\n        'no-iterator': 'error',\n        'no-labels': ['error', { allowLoop: false, allowSwitch: false }],\n        'no-lone-blocks': 'error',\n        'no-loss-of-precision': 'error',\n        'no-misleading-character-class': 'error',\n        'no-multi-str': 'error',\n        'no-new': 'error',\n        'no-new-func': 'error',\n        'no-new-object': 'error',\n        'no-new-symbol': 'error',\n        'no-new-wrappers': 'error',\n        'no-obj-calls': 'error',\n        'no-octal': 'error',\n        'no-octal-escape': 'error',\n        'no-proto': 'error',\n        'no-prototype-builtins': 'error',\n        'no-redeclare': ['error', { builtinGlobals: false }],\n        'no-regex-spaces': 'error',\n        'no-restricted-globals': [\n          'error',\n          { message: 'Use `globalThis` instead.', name: 'global' },\n          { message: 'Use `globalThis` instead.', name: 'self' },\n        ],\n        'no-restricted-properties': [\n          'error',\n          {\n            message:\n              'Use `Object.getPrototypeOf` or `Object.setPrototypeOf` instead.',\n            property: '__proto__',\n          },\n          {\n            message: 'Use `Object.defineProperty` instead.',\n            property: '__defineGetter__',\n          },\n          {\n            message: 'Use `Object.defineProperty` instead.',\n            property: '__defineSetter__',\n          },\n          {\n            message: 'Use `Object.getOwnPropertyDescriptor` instead.',\n            property: '__lookupGetter__',\n          },\n          {\n            message: 'Use `Object.getOwnPropertyDescriptor` instead.',\n            property: '__lookupSetter__',\n          },\n        ],\n        'no-restricted-syntax': [\n          'error',\n          'DebuggerStatement',\n          'LabeledStatement',\n          'WithStatement',\n          'TSEnumDeclaration[const=true]',\n          'TSExportAssignment',\n        ],\n        'no-self-assign': ['error', { props: true }],\n        'no-self-compare': 'error',\n        'no-sequences': 'error',\n        'no-shadow-restricted-names': 'error',\n        'no-sparse-arrays': 'error',\n        'no-template-curly-in-string': 'error',\n        'no-this-before-super': 'error',\n        'no-throw-literal': 'error',\n        'no-undef': 'off',\n        'no-undef-init': 'error',\n        'no-unexpected-multiline': 'error',\n        'no-unmodified-loop-condition': 'error',\n        'no-unneeded-ternary': ['error', { defaultAssignment: false }],\n        'no-unreachable': 'error',\n        'no-unreachable-loop': 'error',\n        'no-unsafe-finally': 'error',\n        'no-unsafe-negation': 'error',\n        'no-unused-expressions': [\n          'error',\n          {\n            allowShortCircuit: true,\n            allowTaggedTemplates: true,\n            allowTernary: true,\n          },\n        ],\n        'no-unused-vars': [\n          'error',\n          {\n            args: 'none',\n            caughtErrors: 'none',\n            ignoreRestSiblings: true,\n            vars: 'all',\n          },\n        ],\n        'no-use-before-define': [\n          'error',\n          { classes: false, functions: false, variables: false },\n        ],\n        'no-useless-backreference': 'error',\n        'no-useless-call': 'error',\n        'no-useless-catch': 'error',\n        'no-useless-computed-key': 'error',\n        'no-useless-constructor': 'error',\n        'no-useless-rename': 'error',\n        'no-useless-return': 'error',\n        'no-var': 'error',\n        'no-with': 'error',\n        'object-shorthand': [\n          'error',\n          'always',\n          { avoidQuotes: true, ignoreConstructors: false },\n        ],\n        'one-var': ['error', { initialized: 'never' }],\n        'prefer-arrow-callback': [\n          'error',\n          {\n            allowNamedFunctions: false,\n            allowUnboundThis: true,\n          },\n        ],\n        'prefer-const': [\n          'error',\n          {\n            destructuring: 'all',\n            ignoreReadBeforeAssign: true,\n          },\n        ],\n        'prefer-exponentiation-operator': 'error',\n\n        'prefer-promise-reject-errors': 'error',\n        'prefer-regex-literals': ['error', { disallowRedundantWrapping: true }],\n        'prefer-rest-params': 'error',\n        'prefer-spread': 'error',\n        'prefer-template': 'error',\n        'space-before-function-paren': 'off',\n        'spaced-comment': 'error',\n        'symbol-description': 'error',\n        'unicode-bom': ['error', 'never'],\n\n        'unused-imports/no-unused-imports': 'error',\n        'unused-imports/no-unused-vars': [\n          'error',\n          {\n            args: 'after-used',\n            argsIgnorePattern: '^_',\n            vars: 'all',\n            varsIgnorePattern: '^_',\n          },\n        ],\n        'use-isnan': [\n          'error',\n          { enforceForIndexOf: true, enforceForSwitchCase: true },\n        ],\n        'valid-typeof': ['error', { requireStringLiterals: true }],\n\n        'vars-on-top': 'error',\n        yoda: ['error', 'never'],\n      },\n    },\n  ];\n}\n"
  },
  {
    "path": "internal/lint-configs/eslint-config/src/configs/jsdoc.ts",
    "content": "import type { Linter } from 'eslint';\n\nimport { interopDefault } from '../util';\n\nexport async function jsdoc(): Promise<Linter.Config[]> {\n  const [pluginJsdoc] = await Promise.all([\n    interopDefault(import('eslint-plugin-jsdoc')),\n  ] as const);\n\n  return [\n    {\n      plugins: {\n        jsdoc: pluginJsdoc,\n      },\n      rules: {\n        'jsdoc/check-access': 'warn',\n        'jsdoc/check-param-names': 'warn',\n        'jsdoc/check-property-names': 'warn',\n        'jsdoc/check-types': 'warn',\n        'jsdoc/empty-tags': 'warn',\n        'jsdoc/implements-on-classes': 'warn',\n        'jsdoc/no-defaults': 'warn',\n        'jsdoc/no-multi-asterisks': 'warn',\n        'jsdoc/require-param-name': 'warn',\n        'jsdoc/require-property': 'warn',\n        'jsdoc/require-property-description': 'warn',\n        'jsdoc/require-property-name': 'warn',\n        'jsdoc/require-returns-check': 'warn',\n        'jsdoc/require-returns-description': 'warn',\n        'jsdoc/require-yields-check': 'warn',\n      },\n    },\n  ];\n}\n"
  },
  {
    "path": "internal/lint-configs/eslint-config/src/configs/jsonc.ts",
    "content": "import type { Linter } from 'eslint';\n\nimport { interopDefault } from '../util';\n\nexport async function jsonc(): Promise<Linter.Config[]> {\n  const [pluginJsonc, parserJsonc] = await Promise.all([\n    interopDefault(import('eslint-plugin-jsonc')),\n    interopDefault(import('jsonc-eslint-parser')),\n  ] as const);\n\n  return [\n    {\n      files: ['**/*.json', '**/*.json5', '**/*.jsonc', '*.code-workspace'],\n      languageOptions: {\n        parser: parserJsonc as any,\n      },\n      plugins: {\n        jsonc: pluginJsonc as any,\n      },\n      rules: {\n        'jsonc/no-bigint-literals': 'error',\n        'jsonc/no-binary-expression': 'error',\n        'jsonc/no-binary-numeric-literals': 'error',\n        'jsonc/no-dupe-keys': 'error',\n        'jsonc/no-escape-sequence-in-identifier': 'error',\n        'jsonc/no-floating-decimal': 'error',\n        'jsonc/no-hexadecimal-numeric-literals': 'error',\n        'jsonc/no-infinity': 'error',\n        'jsonc/no-multi-str': 'error',\n        'jsonc/no-nan': 'error',\n        'jsonc/no-number-props': 'error',\n        'jsonc/no-numeric-separators': 'error',\n        'jsonc/no-octal': 'error',\n        'jsonc/no-octal-escape': 'error',\n        'jsonc/no-octal-numeric-literals': 'error',\n        'jsonc/no-parenthesized': 'error',\n        'jsonc/no-plus-sign': 'error',\n        'jsonc/no-regexp-literals': 'error',\n        'jsonc/no-sparse-arrays': 'error',\n        'jsonc/no-template-literals': 'error',\n        'jsonc/no-undefined-value': 'error',\n        'jsonc/no-unicode-codepoint-escapes': 'error',\n        'jsonc/no-useless-escape': 'error',\n        'jsonc/space-unary-ops': 'error',\n        'jsonc/valid-json-number': 'error',\n        'jsonc/vue-custom-block/no-parsing-error': 'error',\n      },\n    },\n    sortTsconfig(),\n    sortPackageJson(),\n  ];\n}\n\nfunction sortPackageJson(): Linter.Config {\n  return {\n    files: ['**/package.json'],\n    rules: {\n      'jsonc/sort-array-values': [\n        'error',\n        {\n          order: { type: 'asc' },\n          pathPattern: '^files$|^pnpm.neverBuiltDependencies$',\n        },\n      ],\n      'jsonc/sort-keys': [\n        'error',\n        {\n          order: [\n            'name',\n            'version',\n            'description',\n            'private',\n            'keywords',\n            'homepage',\n            'bugs',\n            'repository',\n            'license',\n            'author',\n            'contributors',\n            'categories',\n            'funding',\n            'type',\n            'scripts',\n            'files',\n            'sideEffects',\n            'bin',\n            'main',\n            'module',\n            'unpkg',\n            'jsdelivr',\n            'types',\n            'typesVersions',\n            'imports',\n            'exports',\n            'publishConfig',\n            'icon',\n            'activationEvents',\n            'contributes',\n            'peerDependencies',\n            'peerDependenciesMeta',\n            'dependencies',\n            'optionalDependencies',\n            'devDependencies',\n            'engines',\n            'packageManager',\n            'pnpm',\n            'overrides',\n            'resolutions',\n            'husky',\n            'simple-git-hooks',\n            'lint-staged',\n            'eslintConfig',\n          ],\n          pathPattern: '^$',\n        },\n        {\n          order: { type: 'asc' },\n          pathPattern: '^(?:dev|peer|optional|bundled)?[Dd]ependencies(Meta)?$',\n        },\n        {\n          order: { type: 'asc' },\n          pathPattern: '^(?:resolutions|overrides|pnpm.overrides)$',\n        },\n        {\n          order: ['types', 'import', 'require', 'default'],\n          pathPattern: '^exports.*$',\n        },\n      ],\n    },\n  };\n}\n\nfunction sortTsconfig(): Linter.Config {\n  return {\n    files: [\n      '**/tsconfig.json',\n      '**/tsconfig.*.json',\n      'internal/tsconfig/*.json',\n    ],\n    rules: {\n      'jsonc/sort-keys': [\n        'error',\n        {\n          order: [\n            'extends',\n            'compilerOptions',\n            'references',\n            'files',\n            'include',\n            'exclude',\n          ],\n          pathPattern: '^$',\n        },\n        {\n          order: [\n            /* Projects */\n            'incremental',\n            'composite',\n            'tsBuildInfoFile',\n            'disableSourceOfProjectReferenceRedirect',\n            'disableSolutionSearching',\n            'disableReferencedProjectLoad',\n            /* Language and Environment */\n            'target',\n            'jsx',\n            'jsxFactory',\n            'jsxFragmentFactory',\n            'jsxImportSource',\n            'lib',\n            'moduleDetection',\n            'noLib',\n            'reactNamespace',\n            'useDefineForClassFields',\n            'emitDecoratorMetadata',\n            'experimentalDecorators',\n            /* Modules */\n            'baseUrl',\n            'rootDir',\n            'rootDirs',\n            'customConditions',\n            'module',\n            'moduleResolution',\n            'moduleSuffixes',\n            'noResolve',\n            'paths',\n            'resolveJsonModule',\n            'resolvePackageJsonExports',\n            'resolvePackageJsonImports',\n            'typeRoots',\n            'types',\n            'allowArbitraryExtensions',\n            'allowImportingTsExtensions',\n            'allowUmdGlobalAccess',\n            /* JavaScript Support */\n            'allowJs',\n            'checkJs',\n            'maxNodeModuleJsDepth',\n            /* Type Checking */\n            'strict',\n            'strictBindCallApply',\n            'strictFunctionTypes',\n            'strictNullChecks',\n            'strictPropertyInitialization',\n            'allowUnreachableCode',\n            'allowUnusedLabels',\n            'alwaysStrict',\n            'exactOptionalPropertyTypes',\n            'noFallthroughCasesInSwitch',\n            'noImplicitAny',\n            'noImplicitOverride',\n            'noImplicitReturns',\n            'noImplicitThis',\n            'noPropertyAccessFromIndexSignature',\n            'noUncheckedIndexedAccess',\n            'noUnusedLocals',\n            'noUnusedParameters',\n            'useUnknownInCatchVariables',\n            /* Emit */\n            'declaration',\n            'declarationDir',\n            'declarationMap',\n            'downlevelIteration',\n            'emitBOM',\n            'emitDeclarationOnly',\n            'importHelpers',\n            'importsNotUsedAsValues',\n            'inlineSourceMap',\n            'inlineSources',\n            'mapRoot',\n            'newLine',\n            'noEmit',\n            'noEmitHelpers',\n            'noEmitOnError',\n            'outDir',\n            'outFile',\n            'preserveConstEnums',\n            'preserveValueImports',\n            'removeComments',\n            'sourceMap',\n            'sourceRoot',\n            'stripInternal',\n            /* Interop Constraints */\n            'allowSyntheticDefaultImports',\n            'esModuleInterop',\n            'forceConsistentCasingInFileNames',\n            'isolatedModules',\n            'preserveSymlinks',\n            'verbatimModuleSyntax',\n            /* Completeness */\n            'skipDefaultLibCheck',\n            'skipLibCheck',\n          ],\n          pathPattern: '^compilerOptions$',\n        },\n      ],\n    },\n  };\n}\n"
  },
  {
    "path": "internal/lint-configs/eslint-config/src/configs/node.ts",
    "content": "import type { Linter } from 'eslint';\n\nimport { interopDefault } from '../util';\n\nexport async function node(): Promise<Linter.Config[]> {\n  const pluginNode = await interopDefault(import('eslint-plugin-n'));\n\n  return [\n    {\n      plugins: {\n        n: pluginNode,\n      },\n      rules: {\n        'n/handle-callback-err': ['error', '^(err|error)$'],\n        'n/no-deprecated-api': 'error',\n        'n/no-exports-assign': 'error',\n        'n/no-extraneous-import': [\n          'error',\n          {\n            allowModules: [\n              'unbuild',\n              '@vben/vite-config',\n              'vitest',\n              'vite',\n              '@vue/test-utils',\n              '@vben/tailwind-config',\n              '@playwright/test',\n            ],\n          },\n        ],\n        'n/no-new-require': 'error',\n        'n/no-path-concat': 'error',\n        // 'n/no-unpublished-import': 'off',\n        'n/no-unsupported-features/es-syntax': [\n          'error',\n          {\n            ignores: [],\n            version: '>=18.0.0',\n          },\n        ],\n        'n/prefer-global/buffer': ['error', 'never'],\n        // 'n/no-missing-import': 'off',\n        'n/prefer-global/process': ['error', 'never'],\n        'n/process-exit-as-throw': 'error',\n      },\n    },\n    {\n      files: [\n        'scripts/**/*.?([cm])[jt]s?(x)',\n        'internal/**/*.?([cm])[jt]s?(x)',\n      ],\n      rules: {\n        'n/prefer-global/process': 'off',\n      },\n    },\n  ];\n}\n"
  },
  {
    "path": "internal/lint-configs/eslint-config/src/configs/perfectionist.ts",
    "content": "import type { Linter } from 'eslint';\n\nimport { interopDefault } from '../util';\n\nexport async function perfectionist(): Promise<Linter.Config[]> {\n  const perfectionistPlugin = await interopDefault(\n    // @ts-expect-error - no types\n    import('eslint-plugin-perfectionist'),\n  );\n\n  return [\n    perfectionistPlugin.configs['recommended-natural'],\n    {\n      rules: {\n        'perfectionist/sort-exports': [\n          'error',\n          {\n            order: 'asc',\n            type: 'natural',\n          },\n        ],\n        'perfectionist/sort-imports': [\n          'error',\n          {\n            customGroups: {\n              type: {\n                'vben-core-type': ['^@vben-core/.+'],\n                'vben-type': ['^@vben/.+'],\n                'vue-type': ['^vue$', '^vue-.+', '^@vue/.+'],\n              },\n              value: {\n                vben: ['^@vben/.+'],\n                'vben-core': ['^@vben-core/.+'],\n                vue: ['^vue$', '^vue-.+', '^@vue/.+'],\n              },\n            },\n            environment: 'node',\n            groups: [\n              ['external-type', 'builtin-type', 'type'],\n              'vue-type',\n              'vben-type',\n              'vben-core-type',\n              ['parent-type', 'sibling-type', 'index-type'],\n              ['internal-type'],\n              'builtin',\n              'vue',\n              'vben',\n              'vben-core',\n              'external',\n              'internal',\n              ['parent', 'sibling', 'index'],\n              'side-effect',\n              'side-effect-style',\n              'style',\n              'object',\n              'unknown',\n            ],\n            internalPattern: ['^#/.+'],\n            newlinesBetween: 'always',\n            order: 'asc',\n            type: 'natural',\n          },\n        ],\n        'perfectionist/sort-modules': 'off',\n        'perfectionist/sort-named-exports': [\n          'error',\n          {\n            order: 'asc',\n            type: 'natural',\n          },\n        ],\n        'perfectionist/sort-objects': [\n          'off',\n          {\n            customGroups: {\n              items: 'items',\n              list: 'list',\n              children: 'children',\n            },\n            groups: ['unknown', 'items', 'list', 'children'],\n            ignorePattern: ['children'],\n            order: 'asc',\n            type: 'natural',\n          },\n        ],\n      },\n    },\n  ];\n}\n"
  },
  {
    "path": "internal/lint-configs/eslint-config/src/configs/prettier.ts",
    "content": "import type { Linter } from 'eslint';\n\nimport { interopDefault } from '../util';\n\nexport async function prettier(): Promise<Linter.Config[]> {\n  const [pluginPrettier] = await Promise.all([\n    interopDefault(import('eslint-plugin-prettier')),\n  ] as const);\n  return [\n    {\n      plugins: {\n        prettier: pluginPrettier,\n      },\n      rules: {\n        'prettier/prettier': 'error',\n      },\n    },\n  ];\n}\n"
  },
  {
    "path": "internal/lint-configs/eslint-config/src/configs/regexp.ts",
    "content": "import type { Linter } from 'eslint';\n\nimport { interopDefault } from '../util';\n\nexport async function regexp(): Promise<Linter.Config[]> {\n  const [pluginRegexp] = await Promise.all([\n    interopDefault(import('eslint-plugin-regexp')),\n  ] as const);\n\n  return [\n    {\n      plugins: {\n        regexp: pluginRegexp,\n      },\n      rules: {\n        ...pluginRegexp.configs.recommended.rules,\n      },\n    },\n  ];\n}\n"
  },
  {
    "path": "internal/lint-configs/eslint-config/src/configs/test.ts",
    "content": "import type { Linter } from 'eslint';\n\nimport { interopDefault } from '../util';\n\nexport async function test(): Promise<Linter.Config[]> {\n  const [pluginTest, pluginNoOnlyTests] = await Promise.all([\n    interopDefault(import('eslint-plugin-vitest')),\n    // @ts-expect-error - no types\n    interopDefault(import('eslint-plugin-no-only-tests')),\n  ] as const);\n\n  return [\n    {\n      files: [\n        `**/__tests__/**/*.?([cm])[jt]s?(x)`,\n        `**/*.spec.?([cm])[jt]s?(x)`,\n        `**/*.test.?([cm])[jt]s?(x)`,\n        `**/*.bench.?([cm])[jt]s?(x)`,\n        `**/*.benchmark.?([cm])[jt]s?(x)`,\n      ],\n      plugins: {\n        test: {\n          ...pluginTest,\n          rules: {\n            ...pluginTest.rules,\n            ...pluginNoOnlyTests.rules,\n          },\n        },\n      },\n      rules: {\n        'no-console': 'off',\n        'node/prefer-global/process': 'off',\n        'test/consistent-test-it': [\n          'error',\n          { fn: 'it', withinDescribe: 'it' },\n        ],\n        'test/no-identical-title': 'error',\n        'test/no-import-node-test': 'error',\n        'test/no-only-tests': 'error',\n        'test/prefer-hooks-in-order': 'error',\n        'test/prefer-lowercase-title': 'error',\n      },\n    },\n  ];\n}\n"
  },
  {
    "path": "internal/lint-configs/eslint-config/src/configs/turbo.ts",
    "content": "import type { Linter } from 'eslint';\n\nimport { interopDefault } from '../util';\n\nexport async function turbo(): Promise<Linter.Config[]> {\n  const [pluginTurbo] = await Promise.all([\n    // @ts-expect-error - no types\n    interopDefault(import('eslint-config-turbo')),\n  ] as const);\n\n  return [\n    {\n      plugins: {\n        turbo: pluginTurbo,\n      },\n    },\n  ];\n}\n"
  },
  {
    "path": "internal/lint-configs/eslint-config/src/configs/typescript.ts",
    "content": "import type { Linter } from 'eslint';\n\nimport { interopDefault } from '../util';\n\nexport async function typescript(): Promise<Linter.Config[]> {\n  const [pluginTs, parserTs] = await Promise.all([\n    interopDefault(import('@typescript-eslint/eslint-plugin')),\n    // @ts-expect-error missing types\n    interopDefault(import('@typescript-eslint/parser')),\n  ] as const);\n\n  return [\n    {\n      files: ['**/*.?([cm])[jt]s?(x)'],\n      languageOptions: {\n        parser: parserTs,\n        parserOptions: {\n          createDefaultProgram: false,\n          ecmaFeatures: {\n            jsx: true,\n          },\n          ecmaVersion: 'latest',\n          extraFileExtensions: ['.vue'],\n          jsxPragma: 'React',\n          project: './tsconfig.*.json',\n          sourceType: 'module',\n        },\n      },\n      plugins: {\n        '@typescript-eslint': pluginTs,\n      },\n      rules: {\n        ...pluginTs.configs['eslint-recommended'].overrides?.[0].rules,\n        ...pluginTs.configs.strict.rules,\n        '@typescript-eslint/ban-ts-comment': [\n          'error',\n          {\n            'ts-check': false,\n            'ts-expect-error': 'allow-with-description',\n            'ts-ignore': 'allow-with-description',\n            'ts-nocheck': 'allow-with-description',\n          },\n        ],\n\n        // '@typescript-eslint/consistent-type-definitions': ['warn', 'interface'],\n        '@typescript-eslint/consistent-type-definitions': 'off',\n        '@typescript-eslint/explicit-function-return-type': 'off',\n        '@typescript-eslint/explicit-module-boundary-types': 'off',\n        '@typescript-eslint/no-empty-function': [\n          'error',\n          {\n            allow: ['arrowFunctions', 'functions', 'methods'],\n          },\n        ],\n        '@typescript-eslint/no-explicit-any': 'off',\n        '@typescript-eslint/no-namespace': 'off',\n        '@typescript-eslint/no-non-null-assertion': 'error',\n        '@typescript-eslint/no-unused-expressions': 'off',\n        '@typescript-eslint/no-unused-vars': [\n          'error',\n          {\n            argsIgnorePattern: '^_',\n            varsIgnorePattern: '^_',\n          },\n        ],\n        '@typescript-eslint/no-use-before-define': 'off',\n        '@typescript-eslint/no-var-requires': 'error',\n        'unused-imports/no-unused-vars': 'off',\n      },\n    },\n  ];\n}\n"
  },
  {
    "path": "internal/lint-configs/eslint-config/src/configs/unicorn.ts",
    "content": "import type { Linter } from 'eslint';\n\nimport { interopDefault } from '../util';\n\nexport async function unicorn(): Promise<Linter.Config[]> {\n  const [pluginUnicorn] = await Promise.all([\n    interopDefault(import('eslint-plugin-unicorn')),\n  ] as const);\n\n  return [\n    {\n      plugins: {\n        unicorn: pluginUnicorn,\n      },\n      rules: {\n        ...pluginUnicorn.configs.recommended.rules,\n\n        'unicorn/better-regex': 'off',\n        'unicorn/consistent-destructuring': 'off',\n        'unicorn/consistent-function-scoping': 'off',\n        'unicorn/expiring-todo-comments': 'off',\n        'unicorn/filename-case': 'off',\n        'unicorn/import-style': 'off',\n        'unicorn/no-array-for-each': 'off',\n        'unicorn/no-null': 'off',\n        'unicorn/no-useless-undefined': 'off',\n        'unicorn/prefer-at': 'off',\n        'unicorn/prefer-dom-node-text-content': 'off',\n        'unicorn/prefer-export-from': ['error', { ignoreUsedVariables: true }],\n        'unicorn/prefer-global-this': 'off',\n        'unicorn/prefer-top-level-await': 'off',\n        'unicorn/prevent-abbreviations': 'off',\n      },\n    },\n    {\n      files: [\n        'scripts/**/*.?([cm])[jt]s?(x)',\n        'internal/**/*.?([cm])[jt]s?(x)',\n      ],\n      rules: {\n        'unicorn/no-process-exit': 'off',\n      },\n    },\n  ];\n}\n"
  },
  {
    "path": "internal/lint-configs/eslint-config/src/configs/vue.ts",
    "content": "import type { Linter } from 'eslint';\n\nimport { interopDefault } from '../util';\n\nexport async function vue(): Promise<Linter.Config[]> {\n  const [pluginVue, parserVue, parserTs] = await Promise.all([\n    interopDefault(import('eslint-plugin-vue')),\n    interopDefault(import('vue-eslint-parser')),\n    // @ts-expect-error missing types\n    interopDefault(import('@typescript-eslint/parser')),\n  ] as const);\n\n  const flatEssential = pluginVue.configs?.['flat/essential'] || [];\n  const flatStronglyRecommended =\n    pluginVue.configs?.['flat/strongly-recommended'] || [];\n  const flatRecommended = pluginVue.configs?.['flat/recommended'] || [];\n\n  return [\n    ...flatEssential,\n    ...flatStronglyRecommended,\n    ...flatRecommended,\n    {\n      files: ['**/*.vue'],\n      languageOptions: {\n        // globals: {\n        //   computed: 'readonly',\n        //   defineEmits: 'readonly',\n        //   defineExpose: 'readonly',\n        //   defineProps: 'readonly',\n        //   onMounted: 'readonly',\n        //   onUnmounted: 'readonly',\n        //   reactive: 'readonly',\n        //   ref: 'readonly',\n        //   shallowReactive: 'readonly',\n        //   shallowRef: 'readonly',\n        //   toRef: 'readonly',\n        //   toRefs: 'readonly',\n        //   watch: 'readonly',\n        //   watchEffect: 'readonly',\n        // },\n        parser: parserVue,\n        parserOptions: {\n          ecmaFeatures: {\n            jsx: true,\n          },\n          extraFileExtensions: ['.vue'],\n          parser: parserTs,\n          sourceType: 'module',\n        },\n      },\n      plugins: {\n        vue: pluginVue,\n      },\n      processor: pluginVue.processors?.['.vue'],\n      rules: {\n        ...pluginVue.configs?.base?.rules,\n\n        'vue/attribute-hyphenation': [\n          'error',\n          'always',\n          {\n            ignore: [],\n          },\n        ],\n        'vue/attributes-order': 'off',\n        'vue/block-order': [\n          'error',\n          {\n            order: ['script', 'template', 'style'],\n          },\n        ],\n        'vue/component-name-in-template-casing': ['error', 'PascalCase'],\n        'vue/component-options-name-casing': ['error', 'PascalCase'],\n        'vue/custom-event-name-casing': ['error', 'camelCase'],\n        'vue/define-macros-order': [\n          'error',\n          {\n            order: [\n              'defineOptions',\n              'defineProps',\n              'defineEmits',\n              'defineSlots',\n            ],\n          },\n        ],\n        'vue/dot-location': ['error', 'property'],\n        'vue/dot-notation': ['error', { allowKeywords: true }],\n        'vue/eqeqeq': ['error', 'smart'],\n        'vue/html-closing-bracket-newline': 'error',\n        'vue/html-indent': 'off',\n        // 'vue/html-indent': ['error', 2],\n        'vue/html-quotes': ['error', 'double'],\n        'vue/html-self-closing': [\n          'error',\n          {\n            html: {\n              component: 'always',\n              normal: 'never',\n              void: 'always',\n            },\n            math: 'always',\n            svg: 'always',\n          },\n        ],\n        'vue/max-attributes-per-line': 'off',\n        'vue/multi-word-component-names': 'off',\n        'vue/multiline-html-element-content-newline': 'error',\n        'vue/no-empty-pattern': 'error',\n        'vue/no-extra-parens': ['error', 'functions'],\n        'vue/no-irregular-whitespace': 'error',\n        'vue/no-loss-of-precision': 'error',\n        'vue/no-reserved-component-names': 'off',\n        'vue/no-restricted-syntax': [\n          'error',\n          'DebuggerStatement',\n          'LabeledStatement',\n          'WithStatement',\n        ],\n        'vue/no-restricted-v-bind': ['error', '/^v-/'],\n        'vue/no-sparse-arrays': 'error',\n        'vue/no-unused-refs': 'error',\n        'vue/no-useless-v-bind': 'error',\n        'vue/object-shorthand': [\n          'error',\n          'always',\n          {\n            avoidQuotes: true,\n            ignoreConstructors: false,\n          },\n        ],\n        'vue/one-component-per-file': 'error',\n        'vue/prefer-import-from-vue': 'error',\n        'vue/prefer-separate-static-class': 'error',\n        'vue/prefer-template': 'error',\n        'vue/prop-name-casing': ['error', 'camelCase'],\n        'vue/require-default-prop': 'error',\n        'vue/require-explicit-emits': 'error',\n        'vue/require-prop-types': 'off',\n        'vue/singleline-html-element-content-newline': 'off',\n        'vue/space-infix-ops': 'error',\n        'vue/space-unary-ops': ['error', { nonwords: false, words: true }],\n        'vue/v-on-event-hyphenation': [\n          'error',\n          'always',\n          {\n            autofix: true,\n            ignore: [],\n          },\n        ],\n      },\n    },\n  ];\n}\n"
  },
  {
    "path": "internal/lint-configs/eslint-config/src/custom-config.ts",
    "content": "import type { Linter } from 'eslint';\n\nconst restrictedImportIgnores = [\n  '**/vite.config.mts',\n  '**/tailwind.config.mjs',\n  '**/postcss.config.mjs',\n];\n\nconst customConfig: Linter.Config[] = [\n  // shadcn-ui 内部组件是自动生成的，不做太多限制\n  {\n    files: ['packages/@core/ui-kit/shadcn-ui/**/**'],\n    rules: {\n      'vue/require-default-prop': 'off',\n    },\n  },\n  {\n    files: [\n      'apps/**/**',\n      'packages/effects/**/**',\n      'packages/utils/**/**',\n      'packages/types/**/**',\n      'packages/locales/**/**',\n    ],\n    ignores: restrictedImportIgnores,\n    rules: {\n      'perfectionist/sort-interfaces': 'off',\n      'perfectionist/sort-objects': 'off',\n    },\n  },\n  {\n    files: ['**/**.vue'],\n    ignores: restrictedImportIgnores,\n    rules: {\n      'perfectionist/sort-objects': 'off',\n    },\n  },\n  {\n    // apps内部的一些基础规则\n    files: ['apps/**/**'],\n    ignores: restrictedImportIgnores,\n    rules: {\n      // 允许使用void类型\n      '@typescript-eslint/no-invalid-void-type': 'off',\n      // 关闭 不允许使用console\n      'no-console': 'off',\n      'no-restricted-imports': [\n        'error',\n        {\n          patterns: [\n            {\n              group: ['#/api/*'],\n              message:\n                'The #/api package cannot be imported, please use the @core package itself',\n            },\n            {\n              group: ['#/layouts/*'],\n              message:\n                'The #/layouts package cannot be imported, please use the @core package itself',\n            },\n            {\n              group: ['#/locales/*'],\n              message:\n                'The #/locales package cannot be imported, please use the @core package itself',\n            },\n            {\n              group: ['#/stores/*'],\n              message:\n                'The #/stores package cannot be imported, please use the @core package itself',\n            },\n          ],\n        },\n      ],\n      'perfectionist/sort-interfaces': 'off',\n    },\n  },\n  {\n    // @core内部组件，不能引入@vben/* 里面的包\n    files: ['packages/@core/**/**'],\n    ignores: restrictedImportIgnores,\n    rules: {\n      'no-restricted-imports': [\n        'error',\n        {\n          patterns: [\n            {\n              group: ['@vben/*'],\n              message:\n                'The @core package cannot import the @vben package, please use the @core package itself',\n            },\n          ],\n        },\n      ],\n    },\n  },\n  {\n    // @core/shared内部组件，不能引入@vben/* 或者 @vben-core/* 里面的包\n    files: ['packages/@core/base/**/**'],\n    ignores: restrictedImportIgnores,\n    rules: {\n      'no-restricted-imports': [\n        'error',\n        {\n          patterns: [\n            {\n              group: ['@vben/*', '@vben-core/*'],\n              message:\n                'The @vben-core/shared package cannot import the @vben package, please use the @core/shared package itself',\n            },\n          ],\n        },\n      ],\n    },\n  },\n\n  {\n    // 不能引入@vben/*里面的包\n    files: [\n      'packages/types/**/**',\n      'packages/utils/**/**',\n      'packages/icons/**/**',\n      'packages/constants/**/**',\n      'packages/styles/**/**',\n      'packages/stores/**/**',\n      'packages/preferences/**/**',\n      'packages/locales/**/**',\n    ],\n    ignores: restrictedImportIgnores,\n    rules: {\n      'no-restricted-imports': [\n        'error',\n        {\n          patterns: [\n            {\n              group: ['@vben/*'],\n              message:\n                'The @vben package cannot be imported, please use the @core package itself',\n            },\n          ],\n        },\n      ],\n    },\n  },\n  // 后端模拟代码，不需要太多规则\n  {\n    files: ['apps/backend-mock/**/**', 'docs/**/**'],\n    rules: {\n      '@typescript-eslint/no-extraneous-class': 'off',\n      'n/no-extraneous-import': 'off',\n      'n/prefer-global/buffer': 'off',\n      'n/prefer-global/process': 'off',\n      'no-console': 'off',\n      'unicorn/prefer-module': 'off',\n    },\n  },\n  {\n    files: ['**/**/playwright.config.ts'],\n    rules: {\n      'n/prefer-global/buffer': 'off',\n      'n/prefer-global/process': 'off',\n      'no-console': 'off',\n    },\n  },\n  {\n    files: ['internal/**/**', 'scripts/**/**'],\n    rules: {\n      'no-console': 'off',\n    },\n  },\n];\n\nexport { customConfig };\n"
  },
  {
    "path": "internal/lint-configs/eslint-config/src/index.ts",
    "content": "import type { Linter } from 'eslint';\n\nimport {\n  command,\n  comments,\n  disableds,\n  ignores,\n  importPluginConfig,\n  javascript,\n  jsdoc,\n  jsonc,\n  node,\n  perfectionist,\n  prettier,\n  regexp,\n  test,\n  turbo,\n  typescript,\n  unicorn,\n  vue,\n} from './configs';\nimport { customConfig } from './custom-config';\n\ntype FlatConfig = Linter.Config;\n\ntype FlatConfigPromise =\n  | FlatConfig\n  | FlatConfig[]\n  | Promise<FlatConfig>\n  | Promise<FlatConfig[]>;\n\nasync function defineConfig(config: FlatConfig[] = []) {\n  const configs: FlatConfigPromise[] = [\n    vue(),\n    javascript(),\n    ignores(),\n    prettier(),\n    typescript(),\n    jsonc(),\n    disableds(),\n    importPluginConfig(),\n    node(),\n    perfectionist(),\n    comments(),\n    jsdoc(),\n    unicorn(),\n    test(),\n    regexp(),\n    command(),\n    turbo(),\n    ...customConfig,\n    ...config,\n  ];\n\n  const resolved = await Promise.all(configs);\n\n  return resolved.flat();\n}\n\nexport { defineConfig };\n"
  },
  {
    "path": "internal/lint-configs/eslint-config/src/util.ts",
    "content": "export type Awaitable<T> = Promise<T> | T;\n\nexport async function interopDefault<T>(\n  m: Awaitable<T>,\n): Promise<T extends { default: infer U } ? U : T> {\n  const resolved = await m;\n  return (resolved as any).default || resolved;\n}\n"
  },
  {
    "path": "internal/lint-configs/eslint-config/tsconfig.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/tsconfig\",\n  \"extends\": \"@vben/tsconfig/node.json\",\n  \"include\": [\"src\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "internal/lint-configs/prettier-config/index.mjs",
    "content": "export default {\n  endOfLine: 'auto',\n  overrides: [\n    {\n      files: ['*.json5'],\n      options: {\n        quoteProps: 'preserve',\n        singleQuote: false,\n      },\n    },\n  ],\n  plugins: ['prettier-plugin-tailwindcss'],\n  printWidth: 80,\n  proseWrap: 'never',\n  semi: true,\n  singleQuote: true,\n  trailingComma: 'all',\n};\n"
  },
  {
    "path": "internal/lint-configs/prettier-config/package.json",
    "content": "{\n  \"name\": \"@vben/prettier-config\",\n  \"version\": \"5.0.0\",\n  \"private\": true,\n  \"homepage\": \"https://github.com/vbenjs/vue-vben-admin\",\n  \"bugs\": \"https://github.com/vbenjs/vue-vben-admin/issues\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/vbenjs/vue-vben-admin.git\",\n    \"directory\": \"internal/lint-configs/prettier-config\"\n  },\n  \"license\": \"MIT\",\n  \"type\": \"module\",\n  \"files\": [\n    \"dist\"\n  ],\n  \"main\": \"./index.mjs\",\n  \"module\": \"./index.mjs\",\n  \"exports\": {\n    \".\": {\n      \"default\": \"./index.mjs\"\n    }\n  },\n  \"dependencies\": {\n    \"prettier\": \"catalog:\",\n    \"prettier-plugin-tailwindcss\": \"catalog:\"\n  }\n}\n"
  },
  {
    "path": "internal/lint-configs/stylelint-config/index.mjs",
    "content": "export default {\n  extends: ['stylelint-config-standard', 'stylelint-config-recess-order'],\n  ignoreFiles: [\n    '**/*.js',\n    '**/*.jsx',\n    '**/*.tsx',\n    '**/*.ts',\n    '**/*.json',\n    '**/*.md',\n  ],\n  overrides: [\n    {\n      customSyntax: 'postcss-html',\n      files: ['*.(html|vue)', '**/*.(html|vue)'],\n      rules: {\n        'selector-pseudo-class-no-unknown': [\n          true,\n          {\n            ignorePseudoClasses: ['global', 'deep'],\n          },\n        ],\n        'selector-pseudo-element-no-unknown': [\n          true,\n          {\n            ignorePseudoElements: ['v-deep', 'v-global', 'v-slotted'],\n          },\n        ],\n      },\n    },\n    {\n      customSyntax: 'postcss-scss',\n      extends: [\n        'stylelint-config-recommended-scss',\n        'stylelint-config-recommended-vue/scss',\n      ],\n      files: ['*.scss', '**/*.scss'],\n    },\n  ],\n  plugins: [\n    'stylelint-order',\n    '@stylistic/stylelint-plugin',\n    'stylelint-prettier',\n    'stylelint-scss',\n  ],\n  rules: {\n    'at-rule-no-deprecated': null,\n    'at-rule-no-unknown': [\n      true,\n      {\n        ignoreAtRules: [\n          'extends',\n          'ignores',\n          'include',\n          'mixin',\n          'if',\n          'else',\n          'media',\n          'for',\n          'at-root',\n          'tailwind',\n          'apply',\n          'variants',\n          'responsive',\n          'screen',\n          'function',\n          'each',\n          'use',\n          'forward',\n          'return',\n        ],\n      },\n    ],\n    'font-family-no-missing-generic-family-keyword': null,\n    'function-no-unknown': null,\n    'import-notation': null,\n    'media-feature-range-notation': null,\n    'named-grid-areas-no-invalid': null,\n    'no-descending-specificity': null,\n    'no-empty-source': null,\n    'order/order': [\n      [\n        'dollar-variables',\n        'custom-properties',\n        'at-rules',\n        'declarations',\n        {\n          name: 'supports',\n          type: 'at-rule',\n        },\n        {\n          name: 'media',\n          type: 'at-rule',\n        },\n        {\n          name: 'include',\n          type: 'at-rule',\n        },\n        'rules',\n      ],\n      { severity: 'error' },\n    ],\n    'prettier/prettier': true,\n    'rule-empty-line-before': [\n      'always',\n      {\n        ignore: ['after-comment', 'first-nested'],\n      },\n    ],\n    'scss/at-rule-no-unknown': [\n      true,\n      {\n        ignoreAtRules: [\n          'extends',\n          'ignores',\n          'include',\n          'mixin',\n          'if',\n          'else',\n          'media',\n          'for',\n          'at-root',\n          'tailwind',\n          'apply',\n          'variants',\n          'responsive',\n          'screen',\n          'function',\n          'each',\n          'use',\n          'forward',\n          'return',\n        ],\n      },\n    ],\n    'scss/operator-no-newline-after': null,\n    'selector-class-pattern':\n      '^(?:(?:o|c|u|t|s|is|has|_|js|qa)-)?[a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*(?:__[a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*)?(?:--[a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*)?(?:[.+])?$',\n\n    'selector-not-notation': null,\n  },\n};\n"
  },
  {
    "path": "internal/lint-configs/stylelint-config/package.json",
    "content": "{\n  \"name\": \"@vben/stylelint-config\",\n  \"version\": \"5.5.9\",\n  \"private\": true,\n  \"homepage\": \"https://github.com/vbenjs/vue-vben-admin\",\n  \"bugs\": \"https://github.com/vbenjs/vue-vben-admin/issues\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/vbenjs/vue-vben-admin.git\",\n    \"directory\": \"internal/lint-configs/stylelint-config\"\n  },\n  \"license\": \"MIT\",\n  \"type\": \"module\",\n  \"files\": [\n    \"dist\"\n  ],\n  \"main\": \"./index.mjs\",\n  \"module\": \"./index.mjs\",\n  \"exports\": {\n    \".\": {\n      \"import\": \"./index.mjs\",\n      \"default\": \"./index.mjs\"\n    }\n  },\n  \"dependencies\": {\n    \"@stylistic/stylelint-plugin\": \"catalog:\",\n    \"stylelint-config-recess-order\": \"catalog:\",\n    \"stylelint-scss\": \"catalog:\"\n  },\n  \"devDependencies\": {\n    \"postcss\": \"catalog:\",\n    \"postcss-html\": \"catalog:\",\n    \"postcss-scss\": \"catalog:\",\n    \"prettier\": \"catalog:\",\n    \"stylelint\": \"catalog:\",\n    \"stylelint-config-recommended\": \"catalog:\",\n    \"stylelint-config-recommended-scss\": \"catalog:\",\n    \"stylelint-config-recommended-vue\": \"catalog:\",\n    \"stylelint-config-standard\": \"catalog:\",\n    \"stylelint-order\": \"catalog:\",\n    \"stylelint-prettier\": \"catalog:\"\n  }\n}\n"
  },
  {
    "path": "internal/node-utils/build.config.ts",
    "content": "import { defineBuildConfig } from 'unbuild';\n\nexport default defineBuildConfig({\n  clean: true,\n  declaration: true,\n  entries: ['src/index'],\n});\n"
  },
  {
    "path": "internal/node-utils/package.json",
    "content": "{\n  \"name\": \"@vben/node-utils\",\n  \"version\": \"5.5.9\",\n  \"private\": true,\n  \"homepage\": \"https://github.com/vbenjs/vue-vben-admin\",\n  \"bugs\": \"https://github.com/vbenjs/vue-vben-admin/issues\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/vbenjs/vue-vben-admin.git\",\n    \"directory\": \"internal/node-utils\"\n  },\n  \"license\": \"MIT\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"stub\": \"pnpm unbuild --stub\"\n  },\n  \"files\": [\n    \"dist\"\n  ],\n  \"main\": \"./dist/index.mjs\",\n  \"module\": \"./dist/index.mjs\",\n  \"types\": \"./dist/index.d.ts\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./src/index.ts\",\n      \"import\": \"./dist/index.mjs\",\n      \"default\": \"./dist/index.mjs\"\n    }\n  },\n  \"dependencies\": {\n    \"@changesets/git\": \"catalog:\",\n    \"@manypkg/get-packages\": \"catalog:\",\n    \"chalk\": \"catalog:\",\n    \"consola\": \"catalog:\",\n    \"dayjs\": \"catalog:\",\n    \"execa\": \"catalog:\",\n    \"find-up\": \"catalog:\",\n    \"ora\": \"catalog:\",\n    \"pkg-types\": \"catalog:\",\n    \"prettier\": \"catalog:\",\n    \"rimraf\": \"catalog:\"\n  }\n}\n"
  },
  {
    "path": "internal/node-utils/src/__tests__/hash.test.ts",
    "content": "import { createHash } from 'node:crypto';\n\nimport { describe, expect, it } from 'vitest';\n\nimport { generatorContentHash } from '../hash';\n\ndescribe('generatorContentHash', () => {\n  it('should generate an MD5 hash for the content', () => {\n    const content = 'example content';\n    const expectedHash = createHash('md5')\n      .update(content, 'utf8')\n      .digest('hex');\n    const actualHash = generatorContentHash(content);\n    expect(actualHash).toBe(expectedHash);\n  });\n\n  it('should generate an MD5 hash with specified length', () => {\n    const content = 'example content';\n    const hashLength = 10;\n    const generatedHash = generatorContentHash(content, hashLength);\n    expect(generatedHash).toHaveLength(hashLength);\n  });\n\n  it('should correctly generate the hash with specified length', () => {\n    const content = 'example content';\n    const hashLength = 8;\n    const expectedHash = createHash('md5')\n      .update(content, 'utf8')\n      .digest('hex')\n      .slice(0, hashLength);\n    const generatedHash = generatorContentHash(content, hashLength);\n    expect(generatedHash).toBe(expectedHash);\n  });\n\n  it('should return full hash if hash length parameter is not provided', () => {\n    const content = 'example content';\n    const expectedHash = createHash('md5')\n      .update(content, 'utf8')\n      .digest('hex');\n    const actualHash = generatorContentHash(content);\n    expect(actualHash).toBe(expectedHash);\n  });\n\n  it('should handle empty content', () => {\n    const content = '';\n    const expectedHash = createHash('md5')\n      .update(content, 'utf8')\n      .digest('hex');\n    const actualHash = generatorContentHash(content);\n    expect(actualHash).toBe(expectedHash);\n  });\n});\n"
  },
  {
    "path": "internal/node-utils/src/__tests__/path.test.ts",
    "content": "// pathUtils.test.ts\n\nimport { describe, expect, it } from 'vitest';\n\nimport { toPosixPath } from '../path';\n\ndescribe('toPosixPath', () => {\n  // 测试 Windows 风格路径到 POSIX 风格路径的转换\n  it('converts Windows-style paths to POSIX paths', () => {\n    const windowsPath = String.raw`C:\\Users\\Example\\file.txt`;\n    const expectedPosixPath = 'C:/Users/Example/file.txt';\n    expect(toPosixPath(windowsPath)).toBe(expectedPosixPath);\n  });\n\n  // 确认 POSIX 风格路径不会被改变\n  it('leaves POSIX-style paths unchanged', () => {\n    const posixPath = '/home/user/file.txt';\n    expect(toPosixPath(posixPath)).toBe(posixPath);\n  });\n\n  // 测试带有多个分隔符的路径\n  it('converts paths with mixed separators', () => {\n    const mixedPath = String.raw`C:/Users\\Example\\file.txt`;\n    const expectedPosixPath = 'C:/Users/Example/file.txt';\n    expect(toPosixPath(mixedPath)).toBe(expectedPosixPath);\n  });\n\n  // 测试空字符串\n  it('handles empty strings', () => {\n    const emptyPath = '';\n    expect(toPosixPath(emptyPath)).toBe('');\n  });\n\n  // 测试仅包含分隔符的路径\n  it('handles path with only separators', () => {\n    const separatorsPath = '\\\\\\\\\\\\';\n    const expectedPosixPath = '///';\n    expect(toPosixPath(separatorsPath)).toBe(expectedPosixPath);\n  });\n\n  // 测试不包含任何分隔符的路径\n  it('handles path without separators', () => {\n    const noSeparatorPath = 'file.txt';\n    expect(toPosixPath(noSeparatorPath)).toBe('file.txt');\n  });\n\n  // 测试以分隔符结尾的路径\n  it('handles path ending with a separator', () => {\n    const endingSeparatorPath = 'C:\\\\Users\\\\Example\\\\';\n    const expectedPosixPath = 'C:/Users/Example/';\n    expect(toPosixPath(endingSeparatorPath)).toBe(expectedPosixPath);\n  });\n\n  // 测试以分隔符开头的路径\n  it('handles path starting with a separator', () => {\n    const startingSeparatorPath = String.raw`\\Users\\Example`;\n    const expectedPosixPath = '/Users/Example';\n    expect(toPosixPath(startingSeparatorPath)).toBe(expectedPosixPath);\n  });\n\n  // 测试包含非法字符的路径\n  it('handles path with invalid characters', () => {\n    const invalidCharsPath = String.raw`C:\\Us*?ers\\Ex<ample>|file.txt`;\n    const expectedPosixPath = 'C:/Us*?ers/Ex<ample>|file.txt';\n    expect(toPosixPath(invalidCharsPath)).toBe(expectedPosixPath);\n  });\n});\n"
  },
  {
    "path": "internal/node-utils/src/constants.ts",
    "content": "enum UNICODE {\n  FAILURE = '\\u2716', // ✖\n  SUCCESS = '\\u2714', // ✔\n}\n\nexport { UNICODE };\n"
  },
  {
    "path": "internal/node-utils/src/date.ts",
    "content": "import dayjs from 'dayjs';\nimport timezone from 'dayjs/plugin/timezone';\nimport utc from 'dayjs/plugin/utc';\n\ndayjs.extend(utc);\ndayjs.extend(timezone);\n\ndayjs.tz.setDefault('Asia/Shanghai');\n\nconst dateUtil = dayjs;\n\nexport { dateUtil };\n"
  },
  {
    "path": "internal/node-utils/src/fs.ts",
    "content": "import { promises as fs } from 'node:fs';\nimport { dirname } from 'node:path';\n\nexport async function outputJSON(\n  filePath: string,\n  data: any,\n  spaces: number = 2,\n) {\n  try {\n    const dir = dirname(filePath);\n    await fs.mkdir(dir, { recursive: true });\n    const jsonData = JSON.stringify(data, null, spaces);\n    await fs.writeFile(filePath, jsonData, 'utf8');\n  } catch (error) {\n    console.error('Error writing JSON file:', error);\n    throw error;\n  }\n}\n\nexport async function ensureFile(filePath: string) {\n  try {\n    const dir = dirname(filePath);\n    await fs.mkdir(dir, { recursive: true });\n    await fs.writeFile(filePath, '', { flag: 'a' });\n  } catch (error) {\n    console.error('Error ensuring file:', error);\n    throw error;\n  }\n}\n\nexport async function readJSON(filePath: string) {\n  try {\n    const data = await fs.readFile(filePath, 'utf8');\n    return JSON.parse(data);\n  } catch (error) {\n    console.error('Error reading JSON file:', error);\n    throw error;\n  }\n}\n"
  },
  {
    "path": "internal/node-utils/src/git.ts",
    "content": "import path from 'node:path';\n\nimport { execa } from 'execa';\n\nexport * from '@changesets/git';\n\n/**\n * 获取暂存区文件\n */\nasync function getStagedFiles(): Promise<string[]> {\n  try {\n    const { stdout } = await execa('git', [\n      '-c',\n      'submodule.recurse=false',\n      'diff',\n      '--staged',\n      '--diff-filter=ACMR',\n      '--name-only',\n      '--ignore-submodules',\n      '-z',\n    ]);\n\n    let changedList = stdout ? stdout.replace(/\\0$/, '').split('\\0') : [];\n    changedList = changedList.map((item) => path.resolve(process.cwd(), item));\n    const changedSet = new Set(changedList);\n    changedSet.delete('');\n    return [...changedSet];\n  } catch (error) {\n    console.error('Failed to get staged files:', error);\n    return [];\n  }\n}\n\nexport { getStagedFiles };\n"
  },
  {
    "path": "internal/node-utils/src/hash.ts",
    "content": "import { createHash } from 'node:crypto';\n\n/**\n * 生产基于内容的 hash，可自定义长度\n * @param content\n * @param hashLSize\n */\nfunction generatorContentHash(content: string, hashLSize?: number) {\n  const hash = createHash('md5').update(content, 'utf8').digest('hex');\n\n  if (hashLSize) {\n    return hash.slice(0, hashLSize);\n  }\n\n  return hash;\n}\n\nexport { generatorContentHash };\n"
  },
  {
    "path": "internal/node-utils/src/index.ts",
    "content": "export * from './constants';\nexport * from './date';\nexport * from './fs';\nexport * from './git';\nexport { add as gitAdd, getStagedFiles } from './git';\nexport { generatorContentHash } from './hash';\nexport * from './monorepo';\nexport { toPosixPath } from './path';\nexport { prettierFormat } from './prettier';\nexport * from './spinner';\nexport type { Package } from '@manypkg/get-packages';\nexport { default as colors } from 'chalk';\nexport { consola } from 'consola';\nexport * from 'execa';\n\nexport { default as fs } from 'node:fs/promises';\n\nexport { type PackageJson, readPackageJSON } from 'pkg-types';\nexport { rimraf } from 'rimraf';\n"
  },
  {
    "path": "internal/node-utils/src/monorepo.ts",
    "content": "import { dirname } from 'node:path';\n\nimport {\n  getPackages as getPackagesFunc,\n  getPackagesSync as getPackagesSyncFunc,\n} from '@manypkg/get-packages';\nimport { findUpSync } from 'find-up';\n\n/**\n * 查找大仓的根目录\n * @param cwd\n */\nfunction findMonorepoRoot(cwd: string = process.cwd()) {\n  const lockFile = findUpSync('pnpm-lock.yaml', {\n    cwd,\n    type: 'file',\n  });\n  return dirname(lockFile || '');\n}\n\n/**\n * 获取大仓的所有包\n */\nfunction getPackagesSync() {\n  const root = findMonorepoRoot();\n  return getPackagesSyncFunc(root);\n}\n\n/**\n * 获取大仓的所有包\n */\nasync function getPackages() {\n  const root = findMonorepoRoot();\n\n  return await getPackagesFunc(root);\n}\n\n/**\n * 获取大仓指定的包\n */\nasync function getPackage(pkgName: string) {\n  const { packages } = await getPackages();\n  return packages.find((pkg) => pkg.packageJson.name === pkgName);\n}\n\nexport { findMonorepoRoot, getPackage, getPackages, getPackagesSync };\n"
  },
  {
    "path": "internal/node-utils/src/path.ts",
    "content": "import { posix } from 'node:path';\n\n/**\n * 将给定的文件路径转换为 POSIX 风格。\n * @param {string} pathname - 原始文件路径。\n */\nfunction toPosixPath(pathname: string) {\n  return pathname.split(`\\\\`).join(posix.sep);\n}\n\nexport { toPosixPath };\n"
  },
  {
    "path": "internal/node-utils/src/prettier.ts",
    "content": "import fs from 'node:fs/promises';\n\nimport { format, getFileInfo, resolveConfig } from 'prettier';\n\nasync function prettierFormat(filepath: string) {\n  const prettierOptions = await resolveConfig(filepath, {});\n\n  const fileInfo = await getFileInfo(filepath);\n\n  const input = await fs.readFile(filepath, 'utf8');\n  const output = await format(input, {\n    ...prettierOptions,\n    parser: fileInfo.inferredParser as any,\n  });\n  if (output !== input) {\n    await fs.writeFile(filepath, output, 'utf8');\n  }\n  return output;\n}\n\nexport { prettierFormat };\n"
  },
  {
    "path": "internal/node-utils/src/spinner.ts",
    "content": "import type { Ora } from 'ora';\n\nimport ora from 'ora';\n\ninterface SpinnerOptions {\n  failedText?: string;\n  successText?: string;\n  title: string;\n}\nexport async function spinner<T>(\n  { failedText, successText, title }: SpinnerOptions,\n  callback: () => Promise<T>,\n): Promise<T> {\n  const loading: Ora = ora(title).start();\n\n  try {\n    const result = await callback();\n    loading.succeed(successText || 'Success!');\n    return result;\n  } catch (error) {\n    loading.fail(failedText || 'Failed!');\n    throw error;\n  } finally {\n    loading.stop();\n  }\n}\n"
  },
  {
    "path": "internal/node-utils/tsconfig.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/tsconfig\",\n  \"extends\": \"@vben/tsconfig/node.json\",\n  \"include\": [\"src\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "internal/tailwind-config/build.config.ts",
    "content": "import { defineBuildConfig } from 'unbuild';\n\nexport default defineBuildConfig({\n  clean: true,\n  declaration: true,\n  entries: ['src/index', './src/postcss.config'],\n  rollup: {\n    emitCJS: true,\n  },\n});\n"
  },
  {
    "path": "internal/tailwind-config/package.json",
    "content": "{\n  \"name\": \"@vben/tailwind-config\",\n  \"version\": \"5.5.9\",\n  \"private\": true,\n  \"homepage\": \"https://github.com/vbenjs/vue-vben-admin\",\n  \"bugs\": \"https://github.com/vbenjs/vue-vben-admin/issues\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/vbenjs/vue-vben-admin.git\",\n    \"directory\": \"internal/tailwind-config\"\n  },\n  \"license\": \"MIT\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"stub\": \"pnpm unbuild --stub\"\n  },\n  \"files\": [\n    \"dist\"\n  ],\n  \"main\": \"./dist/index.mjs\",\n  \"module\": \"./dist/index.mjs\",\n  \"types\": \"./dist/index.d.ts\",\n  \"typesVersions\": {\n    \"*\": {\n      \"*\": [\n        \"./dist/*\",\n        \"./*\"\n      ]\n    }\n  },\n  \"exports\": {\n    \".\": {\n      \"types\": \"./src/index.ts\",\n      \"import\": \"./dist/index.mjs\",\n      \"require\": \"./dist/index.cjs\"\n    },\n    \"./postcss\": {\n      \"types\": \"./src/postcss.config.ts\",\n      \"import\": \"./dist/postcss.config.mjs\",\n      \"require\": \"./dist/postcss.config.cjs\",\n      \"default\": \"./dist/postcss.config.mjs\"\n    },\n    \"./*\": \"./*\"\n  },\n  \"peerDependencies\": {\n    \"tailwindcss\": \"^3.4.3\"\n  },\n  \"dependencies\": {\n    \"@iconify/json\": \"catalog:\",\n    \"@iconify/tailwind\": \"catalog:\",\n    \"@manypkg/get-packages\": \"catalog:\",\n    \"@tailwindcss/nesting\": \"catalog:\",\n    \"@tailwindcss/typography\": \"catalog:\",\n    \"autoprefixer\": \"catalog:\",\n    \"cssnano\": \"catalog:\",\n    \"postcss\": \"catalog:\",\n    \"postcss-antd-fixes\": \"catalog:\",\n    \"postcss-import\": \"catalog:\",\n    \"postcss-preset-env\": \"catalog:\",\n    \"tailwindcss\": \"catalog:\",\n    \"tailwindcss-animate\": \"catalog:\"\n  },\n  \"devDependencies\": {\n    \"@types/postcss-import\": \"catalog:\"\n  }\n}\n"
  },
  {
    "path": "internal/tailwind-config/src/index.ts",
    "content": "import type { Config } from 'tailwindcss';\n\nimport path from 'node:path';\n\nimport { addDynamicIconSelectors } from '@iconify/tailwind';\nimport { getPackagesSync } from '@manypkg/get-packages';\nimport typographyPlugin from '@tailwindcss/typography';\nimport animate from 'tailwindcss-animate';\n\nimport { enterAnimationPlugin } from './plugins/entry';\n\n// import defaultTheme from 'tailwindcss/defaultTheme';\n\nconst { packages } = getPackagesSync(process.cwd());\n\nconst tailwindPackages: string[] = [];\n\npackages.forEach((pkg) => {\n  // apps目录下和 @vben-core/tailwind-ui 包需要使用到 tailwindcss ui\n  // if (fs.existsSync(path.join(pkg.dir, 'tailwind.config.mjs'))) {\n  tailwindPackages.push(pkg.dir);\n  // }\n});\n\nconst shadcnUiColors = {\n  accent: {\n    DEFAULT: 'hsl(var(--accent))',\n    foreground: 'hsl(var(--accent-foreground))',\n    hover: 'hsl(var(--accent-hover))',\n    lighter: 'has(val(--accent-lighter))',\n  },\n  background: {\n    deep: 'hsl(var(--background-deep))',\n    DEFAULT: 'hsl(var(--background))',\n  },\n  border: {\n    DEFAULT: 'hsl(var(--border))',\n  },\n  card: {\n    DEFAULT: 'hsl(var(--card))',\n    foreground: 'hsl(var(--card-foreground))',\n  },\n  destructive: {\n    ...createColorsPalette('destructive'),\n    DEFAULT: 'hsl(var(--destructive))',\n  },\n\n  foreground: {\n    DEFAULT: 'hsl(var(--foreground))',\n  },\n\n  input: {\n    background: 'hsl(var(--input-background))',\n    DEFAULT: 'hsl(var(--input))',\n  },\n  muted: {\n    DEFAULT: 'hsl(var(--muted))',\n    foreground: 'hsl(var(--muted-foreground))',\n  },\n  popover: {\n    DEFAULT: 'hsl(var(--popover))',\n    foreground: 'hsl(var(--popover-foreground))',\n  },\n  primary: {\n    ...createColorsPalette('primary'),\n    DEFAULT: 'hsl(var(--primary))',\n  },\n\n  ring: 'hsl(var(--ring))',\n  secondary: {\n    DEFAULT: 'hsl(var(--secondary))',\n    desc: 'hsl(var(--secondary-desc))',\n    foreground: 'hsl(var(--secondary-foreground))',\n  },\n};\n\nconst customColors = {\n  green: {\n    ...createColorsPalette('green'),\n    foreground: 'hsl(var(--success-foreground))',\n  },\n  header: {\n    DEFAULT: 'hsl(var(--header))',\n  },\n  heavy: {\n    DEFAULT: 'hsl(var(--heavy))',\n    foreground: 'hsl(var(--heavy-foreground))',\n  },\n  main: {\n    DEFAULT: 'hsl(var(--main))',\n  },\n  overlay: {\n    content: 'hsl(var(--overlay-content))',\n    DEFAULT: 'hsl(var(--overlay))',\n  },\n  red: {\n    ...createColorsPalette('red'),\n    foreground: 'hsl(var(--destructive-foreground))',\n  },\n  sidebar: {\n    deep: 'hsl(var(--sidebar-deep))',\n    DEFAULT: 'hsl(var(--sidebar))',\n  },\n  success: {\n    ...createColorsPalette('success'),\n    DEFAULT: 'hsl(var(--success))',\n  },\n  warning: {\n    ...createColorsPalette('warning'),\n    DEFAULT: 'hsl(var(--warning))',\n  },\n  yellow: {\n    ...createColorsPalette('yellow'),\n    foreground: 'hsl(var(--warning-foreground))',\n  },\n};\n\nexport default {\n  content: [\n    './index.html',\n    ...tailwindPackages.map((item) =>\n      path.join(item, 'src/**/*.{vue,js,ts,jsx,tsx,svelte,astro,html}'),\n    ),\n  ],\n  darkMode: 'selector',\n  plugins: [\n    animate,\n    typographyPlugin,\n    addDynamicIconSelectors(),\n    enterAnimationPlugin,\n  ],\n  prefix: '',\n  safelist: ['dark'],\n  theme: {\n    container: {\n      center: true,\n      padding: '2rem',\n      screens: {\n        '2xl': '1400px',\n      },\n    },\n    extend: {\n      animation: {\n        'accordion-down': 'accordion-down 0.2s ease-out',\n        'accordion-up': 'accordion-up 0.2s ease-out',\n        'collapsible-down': 'collapsible-down 0.2s ease-in-out',\n        'collapsible-up': 'collapsible-up 0.2s ease-in-out',\n        float: 'float 5s linear 0ms infinite',\n      },\n\n      animationDuration: {\n        '2000': '2000ms',\n        '3000': '3000ms',\n      },\n      borderRadius: {\n        lg: 'var(--radius)',\n        md: 'calc(var(--radius) - 2px)',\n        sm: 'calc(var(--radius) - 4px)',\n        xl: 'calc(var(--radius) + 4px)',\n      },\n      boxShadow: {\n        float: `0 6px 16px 0 rgb(0 0 0 / 8%),\n          0 3px 6px -4px rgb(0 0 0 / 12%),\n          0 9px 28px 8px rgb(0 0 0 / 5%)`,\n      },\n      colors: {\n        ...customColors,\n        ...shadcnUiColors,\n      },\n      fontFamily: {\n        sans: [\n          'var(--font-family)',\n          //  ...defaultTheme.fontFamily.sans\n        ],\n      },\n      keyframes: {\n        'accordion-down': {\n          from: { height: '0' },\n          to: { height: 'var(--radix-accordion-content-height)' },\n        },\n        'accordion-up': {\n          from: { height: 'var(--radix-accordion-content-height)' },\n          to: { height: '0' },\n        },\n        'collapsible-down': {\n          from: { height: '0' },\n          to: { height: 'var(--radix-collapsible-content-height)' },\n        },\n        'collapsible-up': {\n          from: { height: 'var(--radix-collapsible-content-height)' },\n          to: { height: '0' },\n        },\n        float: {\n          '0%': { transform: 'translateY(0)' },\n          '50%': { transform: 'translateY(-20px)' },\n          '100%': { transform: 'translateY(0)' },\n        },\n      },\n      zIndex: {\n        '100': '100',\n        '1000': '1000',\n      },\n    },\n  },\n} as Config;\n\nfunction createColorsPalette(name: string) {\n  // backgroundLightest: '#EFF6FF', // Tailwind CSS 默认的 `blue-50`\n  //         backgroundLighter: '#DBEAFE',  // Tailwind CSS 默认的 `blue-100`\n  //         backgroundLight: '#BFDBFE',    // Tailwind CSS 默认的 `blue-200`\n  //         borderLight: '#93C5FD',        // Tailwind CSS 默认的 `blue-300`\n  //         border: '#60A5FA',             // Tailwind CSS 默认的 `blue-400`\n  //         main: '#3B82F6',               // Tailwind CSS 默认的 `blue-500`\n  //         hover: '#2563EB',              // Tailwind CSS 默认的 `blue-600`\n  //         active: '#1D4ED8',             // Tailwind CSS 默认的 `blue-700`\n  //         backgroundDark: '#1E40AF',     // Tailwind CSS 默认的 `blue-800`\n  //         backgroundDarker: '#1E3A8A',   // Tailwind CSS 默认的 `blue-900`\n  //         backgroundDarkest: '#172554',  // Tailwind CSS 默认的 `blue-950`\n\n  // •\tbackgroundLightest (#EFF6FF): 适用于最浅的背景色，可能用于非常轻微的阴影或卡片的背景。\n  // •\tbackgroundLighter (#DBEAFE): 适用于略浅的背景色，通常用于次要背景或略浅的区域。\n  // •\tbackgroundLight (#BFDBFE): 适用于浅色背景，可能用于输入框或表单区域的背景。\n  // •\tborderLight (#93C5FD): 适用于浅色边框，可能用于输入框或卡片的边框。\n  // •\tborder (#60A5FA): 适用于普通边框，可能用于按钮或卡片的边框。\n  // •\tmain (#3B82F6): 适用于主要的主题色，通常用于按钮、链接或主要的强调色。\n  // •\thover (#2563EB): 适用于鼠标悬停状态下的颜色，例如按钮悬停时的背景色或边框色。\n  // •\tactive (#1D4ED8): 适用于激活状态下的颜色，例如按钮按下时的背景色或边框色。\n  // •\tbackgroundDark (#1E40AF): 适用于深色背景，可能用于主要按钮或深色卡片背景。\n  // •\tbackgroundDarker (#1E3A8A): 适用于更深的背景，通常用于头部导航栏或页脚。\n  // •\tbackgroundDarkest (#172554): 适用于最深的背景，可能用于非常深色的区域或极端对比色。\n\n  return {\n    50: `hsl(var(--${name}-50))`,\n    100: `hsl(var(--${name}-100))`,\n    200: `hsl(var(--${name}-200))`,\n    300: `hsl(var(--${name}-300))`,\n    400: `hsl(var(--${name}-400))`,\n    500: `hsl(var(--${name}-500))`,\n    600: `hsl(var(--${name}-600))`,\n    700: `hsl(var(--${name}-700))`,\n    // 800: `hsl(var(--${name}-800))`,\n    // 900: `hsl(var(--${name}-900))`,\n    // 950: `hsl(var(--${name}-950))`,\n    // 激活状态下的颜色，适用于按钮按下时的背景色或边框色。\n    active: `hsl(var(--${name}-700))`,\n    // 浅色背景，适用于输入框或表单区域的背景。\n    'background-light': `hsl(var(--${name}-200))`,\n    // 适用于略浅的背景色，通常用于次要背景或略浅的区域。\n    'background-lighter': `hsl(var(--${name}-100))`,\n    // 最浅的背景色，适用于非常轻微的阴影或卡片的背景。\n    'background-lightest': `hsl(var(--${name}-50))`,\n    // 适用于普通边框，可能用于按钮或卡片的边框。\n    border: `hsl(var(--${name}-400))`,\n    // 浅色边框，适用于输入框或卡片的边框。\n    'border-light': `hsl(var(--${name}-300))`,\n    foreground: `hsl(var(--${name}-foreground))`,\n    // 鼠标悬停状态下的颜色，适用于按钮悬停时的背景色或边框色。\n    hover: `hsl(var(--${name}-600))`,\n    // 主色文本\n    text: `hsl(var(--${name}-500))`,\n    // 主色文本激活态\n    'text-active': `hsl(var(--${name}-700))`,\n    // 主色文本悬浮态\n    'text-hover': `hsl(var(--${name}-600))`,\n  };\n}\n"
  },
  {
    "path": "internal/tailwind-config/src/module.d.ts",
    "content": "declare module '@tailwindcss/nesting' {\n  export default any;\n}\n"
  },
  {
    "path": "internal/tailwind-config/src/plugins/entry.ts",
    "content": "import plugin from 'tailwindcss/plugin.js';\n\nconst enterAnimationPlugin = plugin(({ addUtilities }) => {\n  const maxChild = 5;\n  const utilities: Record<string, any> = {};\n  for (let i = 1; i <= maxChild; i++) {\n    const baseDelay = 0.1;\n    const delay = `${baseDelay * i}s`;\n\n    utilities[`.enter-x:nth-child(${i})`] = {\n      animation: `enter-x-animation 0.3s ease-in-out ${delay} forwards`,\n      opacity: '0',\n      transform: `translateX(50px)`,\n    };\n\n    utilities[`.enter-y:nth-child(${i})`] = {\n      animation: `enter-y-animation 0.3s ease-in-out ${delay} forwards`,\n      opacity: '0',\n      transform: `translateY(50px)`,\n    };\n\n    utilities[`.-enter-x:nth-child(${i})`] = {\n      animation: `enter-x-animation 0.3s ease-in-out ${delay} forwards`,\n      opacity: '0',\n      transform: `translateX(-50px)`,\n    };\n\n    utilities[`.-enter-y:nth-child(${i})`] = {\n      animation: `enter-y-animation 0.3s ease-in-out ${delay} forwards`,\n      opacity: '0',\n      transform: `translateY(-50px)`,\n    };\n  }\n\n  // 添加动画关键帧\n  addUtilities(utilities);\n  addUtilities({\n    '@keyframes enter-x-animation': {\n      to: {\n        opacity: '1',\n        transform: 'translateX(0)',\n      },\n    },\n    '@keyframes enter-y-animation': {\n      to: {\n        opacity: '1',\n        transform: 'translateY(0)',\n      },\n    },\n  });\n});\n\nexport { enterAnimationPlugin };\n"
  },
  {
    "path": "internal/tailwind-config/src/postcss.config.ts",
    "content": "import config from '.';\n\nexport default {\n  plugins: {\n    ...(process.env.NODE_ENV === 'production' ? { cssnano: {} } : {}),\n    // Specifying the config is not necessary in most cases, but it is included\n    autoprefixer: {},\n    // 修复 element-plus 和 ant-design-vue 的样式和tailwindcss冲突问题\n    'postcss-antd-fixes': { prefixes: ['ant', 'el'] },\n    'postcss-import': {},\n    'postcss-preset-env': {},\n    tailwindcss: { config },\n    'tailwindcss/nesting': {},\n  },\n};\n"
  },
  {
    "path": "internal/tailwind-config/tsconfig.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/tsconfig\",\n  \"extends\": \"@vben/tsconfig/node.json\",\n  \"compilerOptions\": {\n    \"moduleResolution\": \"bundler\"\n  },\n  \"include\": [\"src\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "internal/tsconfig/base.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/tsconfig\",\n  \"display\": \"Base\",\n  \"compilerOptions\": {\n    \"composite\": false,\n    \"target\": \"ESNext\",\n\n    \"moduleDetection\": \"force\",\n    \"experimentalDecorators\": true,\n\n    \"baseUrl\": \".\",\n    \"module\": \"ESNext\",\n\n    \"moduleResolution\": \"node\",\n    \"resolveJsonModule\": true,\n\n    \"strict\": true,\n    \"strictNullChecks\": true,\n    \"noFallthroughCasesInSwitch\": true,\n    \"noImplicitAny\": true,\n    \"noImplicitOverride\": true,\n    \"noImplicitThis\": true,\n    \"noUncheckedIndexedAccess\": true,\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n\n    \"inlineSources\": false,\n    \"noEmit\": true,\n    \"removeComments\": true,\n    \"sourceMap\": false,\n    \"allowSyntheticDefaultImports\": true,\n    \"esModuleInterop\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"isolatedModules\": true,\n    \"verbatimModuleSyntax\": true,\n    \"skipLibCheck\": true,\n    \"preserveWatchOutput\": true\n  },\n  \"exclude\": [\"**/node_modules/**\", \"**/dist/**\", \"**/.turbo/**\"]\n}\n"
  },
  {
    "path": "internal/tsconfig/library.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/tsconfig\",\n  \"display\": \"Web Application\",\n  \"extends\": \"./base.json\",\n  \"compilerOptions\": {\n    \"jsx\": \"preserve\",\n    \"lib\": [\"ESNext\", \"DOM\", \"DOM.Iterable\"],\n    \"useDefineForClassFields\": true,\n    \"moduleResolution\": \"bundler\",\n    \"declaration\": true,\n    \"noEmit\": false\n  }\n}\n"
  },
  {
    "path": "internal/tsconfig/node.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/tsconfig\",\n  \"display\": \"Node Config\",\n  \"extends\": \"./base.json\",\n  \"compilerOptions\": {\n    \"composite\": false,\n    \"lib\": [\"ESNext\"],\n    \"baseUrl\": \"./\",\n    \"types\": [\"node\"],\n    \"noImplicitAny\": true\n  }\n}\n"
  },
  {
    "path": "internal/tsconfig/package.json",
    "content": "{\n  \"name\": \"@vben/tsconfig\",\n  \"version\": \"5.5.9\",\n  \"private\": true,\n  \"homepage\": \"https://github.com/vbenjs/vue-vben-admin\",\n  \"bugs\": \"https://github.com/vbenjs/vue-vben-admin/issues\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/vbenjs/vue-vben-admin.git\",\n    \"directory\": \"internal/tsconfig\"\n  },\n  \"license\": \"MIT\",\n  \"type\": \"module\",\n  \"files\": [\n    \"base.json\",\n    \"library.json\",\n    \"node.json\",\n    \"web-app.json\",\n    \"web.json\"\n  ],\n  \"dependencies\": {\n    \"@vben/types\": \"workspace:*\",\n    \"vite\": \"catalog:\"\n  }\n}\n"
  },
  {
    "path": "internal/tsconfig/web-app.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/tsconfig\",\n  \"display\": \"Web Application\",\n  \"extends\": \"./web.json\",\n  \"compilerOptions\": {\n    \"types\": [\"vite/client\", \"@vben/types/global\"]\n  }\n}\n"
  },
  {
    "path": "internal/tsconfig/web.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/tsconfig\",\n  \"display\": \"Web Package\",\n  \"extends\": \"./base.json\",\n  \"compilerOptions\": {\n    \"jsx\": \"preserve\",\n    \"jsxImportSource\": \"vue\",\n    \"lib\": [\"ESNext\", \"DOM\", \"DOM.Iterable\"],\n    \"useDefineForClassFields\": true,\n    \"moduleResolution\": \"bundler\",\n    \"types\": [\"vite/client\"],\n    \"declaration\": false\n  }\n}\n"
  },
  {
    "path": "internal/vite-config/build.config.ts",
    "content": "import { defineBuildConfig } from 'unbuild';\n\nexport default defineBuildConfig({\n  clean: true,\n  declaration: true,\n  entries: ['src/index'],\n});\n"
  },
  {
    "path": "internal/vite-config/package.json",
    "content": "{\n  \"name\": \"@vben/vite-config\",\n  \"version\": \"5.5.9\",\n  \"private\": true,\n  \"homepage\": \"https://github.com/vbenjs/vue-vben-admin\",\n  \"bugs\": \"https://github.com/vbenjs/vue-vben-admin/issues\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/vbenjs/vue-vben-admin.git\",\n    \"directory\": \"internal/vite-config\"\n  },\n  \"license\": \"MIT\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"stub\": \"pnpm unbuild --stub\"\n  },\n  \"files\": [\n    \"dist\"\n  ],\n  \"main\": \"./dist/index.mjs\",\n  \"module\": \"./dist/index.mjs\",\n  \"types\": \"./dist/index.d.ts\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./src/index.ts\",\n      \"default\": \"./dist/index.mjs\"\n    }\n  },\n  \"dependencies\": {\n    \"@intlify/unplugin-vue-i18n\": \"catalog:\",\n    \"@jspm/generator\": \"catalog:\",\n    \"archiver\": \"catalog:\",\n    \"cheerio\": \"catalog:\",\n    \"get-port\": \"catalog:\",\n    \"html-minifier-terser\": \"catalog:\",\n    \"nitropack\": \"catalog:\",\n    \"resolve.exports\": \"catalog:\",\n    \"vite-plugin-pwa\": \"catalog:\",\n    \"vite-plugin-vue-devtools\": \"catalog:\"\n  },\n  \"devDependencies\": {\n    \"@pnpm/workspace.read-manifest\": \"catalog:\",\n    \"@types/archiver\": \"catalog:\",\n    \"@types/html-minifier-terser\": \"catalog:\",\n    \"@vben/node-utils\": \"workspace:*\",\n    \"@vitejs/plugin-vue\": \"catalog:\",\n    \"@vitejs/plugin-vue-jsx\": \"catalog:\",\n    \"dayjs\": \"catalog:\",\n    \"dotenv\": \"catalog:\",\n    \"rollup\": \"catalog:\",\n    \"rollup-plugin-visualizer\": \"catalog:\",\n    \"sass\": \"catalog:\",\n    \"vite\": \"catalog:\",\n    \"vite-plugin-compression\": \"catalog:\",\n    \"vite-plugin-dts\": \"catalog:\",\n    \"vite-plugin-html\": \"catalog:\",\n    \"vite-plugin-lazy-import\": \"catalog:\"\n  }\n}\n"
  },
  {
    "path": "internal/vite-config/src/config/application.ts",
    "content": "import type { CSSOptions, UserConfig } from 'vite';\n\nimport type { DefineApplicationOptions } from '../typing';\n\nimport path, { relative } from 'node:path';\n\nimport { findMonorepoRoot } from '@vben/node-utils';\n\nimport { NodePackageImporter } from 'sass';\nimport { defineConfig, loadEnv, mergeConfig } from 'vite';\n\nimport { defaultImportmapOptions, getDefaultPwaOptions } from '../options';\nimport { loadApplicationPlugins } from '../plugins';\nimport { loadAndConvertEnv } from '../utils/env';\nimport { getCommonConfig } from './common';\n\nfunction defineApplicationConfig(userConfigPromise?: DefineApplicationOptions) {\n  return defineConfig(async (config) => {\n    const options = await userConfigPromise?.(config);\n    const { appTitle, base, port, ...envConfig } = await loadAndConvertEnv();\n    const { command, mode } = config;\n    const { application = {}, vite = {} } = options || {};\n    const root = process.cwd();\n    const isBuild = command === 'build';\n    const env = loadEnv(mode, root);\n\n    const plugins = await loadApplicationPlugins({\n      archiver: true,\n      archiverPluginOptions: {},\n      compress: false,\n      compressTypes: ['brotli', 'gzip'],\n      devtools: true,\n      env,\n      extraAppConfig: true,\n      html: true,\n      i18n: true,\n      importmapOptions: defaultImportmapOptions,\n      injectAppLoading: true,\n      injectMetadata: true,\n      isBuild,\n      license: true,\n      mode,\n      nitroMock: !isBuild,\n      nitroMockOptions: {},\n      print: !isBuild,\n      printInfoMap: {\n        'Vben Admin Docs': 'https://doc.vben.pro',\n      },\n      pwa: true,\n      pwaOptions: getDefaultPwaOptions(appTitle),\n      vxeTableLazyImport: true,\n      ...envConfig,\n      ...application,\n    });\n\n    const { injectGlobalScss = true } = application;\n\n    const applicationConfig: UserConfig = {\n      base,\n      build: {\n        rollupOptions: {\n          output: {\n            assetFileNames: '[ext]/[name]-[hash].[ext]',\n            chunkFileNames: 'js/[name]-[hash].js',\n            entryFileNames: 'js/index-[name]-[hash].js',\n          },\n        },\n        target: 'es2015',\n      },\n      css: createCssOptions(injectGlobalScss),\n      esbuild: {\n        drop: isBuild\n          ? [\n              // 'console',\n              'debugger',\n            ]\n          : [],\n        legalComments: 'none',\n      },\n      plugins,\n      server: {\n        host: true,\n        port,\n        warmup: {\n          // 预热文件\n          clientFiles: [\n            './index.html',\n            './src/bootstrap.ts',\n            './src/{views,layouts,router,store,api,adapter}/*',\n          ],\n        },\n      },\n    };\n\n    const mergedCommonConfig = mergeConfig(\n      await getCommonConfig(),\n      applicationConfig,\n    );\n    return mergeConfig(mergedCommonConfig, vite);\n  });\n}\n\nfunction createCssOptions(injectGlobalScss = true): CSSOptions {\n  const root = findMonorepoRoot();\n  return {\n    preprocessorOptions: injectGlobalScss\n      ? {\n          scss: {\n            additionalData: (content: string, filepath: string) => {\n              const relativePath = relative(root, filepath);\n              // apps下的包注入全局样式\n              if (relativePath.startsWith(`apps${path.sep}`)) {\n                return `@use \"@vben/styles/global\" as *;\\n${content}`;\n              }\n              return content;\n            },\n            api: 'modern',\n            importers: [new NodePackageImporter()],\n          },\n        }\n      : {},\n  };\n}\n\nexport { defineApplicationConfig };\n"
  },
  {
    "path": "internal/vite-config/src/config/common.ts",
    "content": "import type { UserConfig } from 'vite';\n\nasync function getCommonConfig(): Promise<UserConfig> {\n  return {\n    build: {\n      chunkSizeWarningLimit: 2000,\n      reportCompressedSize: false,\n      sourcemap: false,\n    },\n  };\n}\n\nexport { getCommonConfig };\n"
  },
  {
    "path": "internal/vite-config/src/config/index.ts",
    "content": "import type { DefineConfig } from '../typing';\n\nimport { existsSync } from 'node:fs';\nimport { join } from 'node:path';\n\nimport { defineApplicationConfig } from './application';\nimport { defineLibraryConfig } from './library';\n\nexport * from './application';\nexport * from './library';\n\nfunction defineConfig(\n  userConfigPromise?: DefineConfig,\n  type: 'application' | 'auto' | 'library' = 'auto',\n) {\n  let projectType = type;\n\n  // 根据包是否存在 index.html,自动判断类型\n  if (projectType === 'auto') {\n    const htmlPath = join(process.cwd(), 'index.html');\n    projectType = existsSync(htmlPath) ? 'application' : 'library';\n  }\n\n  switch (projectType) {\n    case 'application': {\n      return defineApplicationConfig(userConfigPromise);\n    }\n    case 'library': {\n      return defineLibraryConfig(userConfigPromise);\n    }\n    default: {\n      throw new Error(`Unsupported project type: ${projectType}`);\n    }\n  }\n}\n\nexport { defineConfig };\n"
  },
  {
    "path": "internal/vite-config/src/config/library.ts",
    "content": "import type { ConfigEnv, UserConfig } from 'vite';\n\nimport type { DefineLibraryOptions } from '../typing';\n\nimport { readPackageJSON } from '@vben/node-utils';\n\nimport { defineConfig, mergeConfig } from 'vite';\n\nimport { loadLibraryPlugins } from '../plugins';\nimport { getCommonConfig } from './common';\n\nfunction defineLibraryConfig(userConfigPromise?: DefineLibraryOptions) {\n  return defineConfig(async (config: ConfigEnv) => {\n    const options = await userConfigPromise?.(config);\n    const { command, mode } = config;\n    const { library = {}, vite = {} } = options || {};\n    const root = process.cwd();\n    const isBuild = command === 'build';\n\n    const plugins = await loadLibraryPlugins({\n      dts: false,\n      injectMetadata: true,\n      isBuild,\n      mode,\n      ...library,\n    });\n\n    const { dependencies = {}, peerDependencies = {} } =\n      await readPackageJSON(root);\n\n    const externalPackages = [\n      ...Object.keys(dependencies),\n      ...Object.keys(peerDependencies),\n    ];\n\n    const packageConfig: UserConfig = {\n      build: {\n        lib: {\n          entry: 'src/index.ts',\n          fileName: () => 'index.mjs',\n          formats: ['es'],\n        },\n        rollupOptions: {\n          external: (id) => {\n            return externalPackages.some(\n              (pkg) => id === pkg || id.startsWith(`${pkg}/`),\n            );\n          },\n        },\n      },\n      plugins,\n    };\n    const commonConfig = await getCommonConfig();\n    const mergedConmonConfig = mergeConfig(commonConfig, packageConfig);\n    return mergeConfig(mergedConmonConfig, vite);\n  });\n}\n\nexport { defineLibraryConfig };\n"
  },
  {
    "path": "internal/vite-config/src/index.ts",
    "content": "export * from './config';\nexport * from './options';\nexport * from './plugins';\nexport { loadAndConvertEnv } from './utils/env';\n"
  },
  {
    "path": "internal/vite-config/src/options.ts",
    "content": "import type { Options as PwaPluginOptions } from 'vite-plugin-pwa';\n\nimport type { ImportmapPluginOptions } from './typing';\n\nconst isDevelopment = process.env.NODE_ENV === 'development';\n\nconst getDefaultPwaOptions = (name: string): Partial<PwaPluginOptions> => ({\n  manifest: {\n    description:\n      'Vben Admin is a modern admin dashboard template based on Vue 3. ',\n    icons: [\n      {\n        sizes: '192x192',\n        src: 'https://unpkg.com/@vbenjs/static-source@0.1.7/source/pwa-icon-192.png',\n        type: 'image/png',\n      },\n      {\n        sizes: '512x512',\n        src: 'https://unpkg.com/@vbenjs/static-source@0.1.7/source/pwa-icon-512.png',\n        type: 'image/png',\n      },\n    ],\n    name: `${name}${isDevelopment ? ' dev' : ''}`,\n    short_name: `${name}${isDevelopment ? ' dev' : ''}`,\n  },\n});\n\n/**\n * importmap CDN 暂时不开启，因为有些包不支持，且网络不稳定\n */\nconst defaultImportmapOptions: ImportmapPluginOptions = {\n  // 通过 Importmap CDN 方式引入,\n  // 目前只有esm.sh源兼容性好一点，jspm.io对于 esm 入口要求高\n  defaultProvider: 'esm.sh',\n  importmap: [\n    { name: 'vue' },\n    { name: 'pinia' },\n    { name: 'vue-router' },\n    // { name: 'vue-i18n' },\n    { name: 'dayjs' },\n    { name: 'vue-demi' },\n  ],\n};\n\nexport { defaultImportmapOptions, getDefaultPwaOptions };\n"
  },
  {
    "path": "internal/vite-config/src/plugins/archiver.ts",
    "content": "import type { PluginOption } from 'vite';\n\nimport type { ArchiverPluginOptions } from '../typing';\n\nimport fs from 'node:fs';\nimport fsp from 'node:fs/promises';\nimport { join } from 'node:path';\n\nimport archiver from 'archiver';\n\nexport const viteArchiverPlugin = (\n  options: ArchiverPluginOptions = {},\n): PluginOption => {\n  return {\n    apply: 'build',\n    closeBundle: {\n      handler() {\n        const { name = 'dist', outputDir = '.' } = options;\n\n        setTimeout(async () => {\n          const folderToZip = 'dist';\n\n          const zipOutputDir = join(process.cwd(), outputDir);\n          const zipOutputPath = join(zipOutputDir, `${name}.zip`);\n          try {\n            await fsp.mkdir(zipOutputDir, { recursive: true });\n          } catch {\n            // ignore\n          }\n\n          try {\n            await zipFolder(folderToZip, zipOutputPath);\n            console.log(`Folder has been zipped to: ${zipOutputPath}`);\n          } catch (error) {\n            console.error('Error zipping folder:', error);\n          }\n        }, 0);\n      },\n      order: 'post',\n    },\n    enforce: 'post',\n    name: 'vite:archiver',\n  };\n};\n\nasync function zipFolder(\n  folderPath: string,\n  outputPath: string,\n): Promise<void> {\n  return new Promise((resolve, reject) => {\n    const output = fs.createWriteStream(outputPath);\n    const archive = archiver('zip', {\n      zlib: { level: 9 }, // 设置压缩级别为 9 以实现最高压缩率\n    });\n\n    output.on('close', () => {\n      console.log(\n        `ZIP file created: ${outputPath} (${archive.pointer()} total bytes)`,\n      );\n      resolve();\n    });\n\n    archive.on('error', (err) => {\n      reject(err);\n    });\n\n    archive.pipe(output);\n\n    // 使用 directory 方法以流的方式压缩文件夹，减少内存消耗\n    archive.directory(folderPath, false);\n\n    // 流式处理完成\n    archive.finalize();\n  });\n}\n"
  },
  {
    "path": "internal/vite-config/src/plugins/extra-app-config.ts",
    "content": "import type { PluginOption } from 'vite';\n\nimport {\n  colors,\n  generatorContentHash,\n  readPackageJSON,\n} from '@vben/node-utils';\n\nimport { loadEnv } from '../utils/env';\n\ninterface PluginOptions {\n  isBuild: boolean;\n  root: string;\n}\n\nconst GLOBAL_CONFIG_FILE_NAME = '_app.config.js';\nconst VBEN_ADMIN_PRO_APP_CONF = '_VBEN_ADMIN_PRO_APP_CONF_';\n\n/**\n * 用于将配置文件抽离出来并注入到项目中\n * @returns\n */\n\nasync function viteExtraAppConfigPlugin({\n  isBuild,\n  root,\n}: PluginOptions): Promise<PluginOption | undefined> {\n  let publicPath: string;\n  let source: string;\n\n  if (!isBuild) {\n    return;\n  }\n\n  const { version = '' } = await readPackageJSON(root);\n\n  return {\n    async configResolved(config) {\n      publicPath = ensureTrailingSlash(config.base);\n      source = await getConfigSource();\n    },\n    async generateBundle() {\n      try {\n        this.emitFile({\n          fileName: GLOBAL_CONFIG_FILE_NAME,\n          source,\n          type: 'asset',\n        });\n\n        console.log(colors.cyan(`✨configuration file is build successfully!`));\n      } catch (error) {\n        console.log(\n          colors.red(\n            `configuration file configuration file failed to package:\\n${error}`,\n          ),\n        );\n      }\n    },\n    name: 'vite:extra-app-config',\n    async transformIndexHtml(html) {\n      const hash = `v=${version}-${generatorContentHash(source, 8)}`;\n\n      const appConfigSrc = `${publicPath}${GLOBAL_CONFIG_FILE_NAME}?${hash}`;\n\n      return {\n        html,\n        tags: [{ attrs: { src: appConfigSrc }, tag: 'script' }],\n      };\n    },\n  };\n}\n\nasync function getConfigSource() {\n  const config = await loadEnv();\n  const windowVariable = `window.${VBEN_ADMIN_PRO_APP_CONF}`;\n  // 确保变量不会被修改\n  let source = `${windowVariable}=${JSON.stringify(config)};`;\n  source += `\n    Object.freeze(${windowVariable});\n    Object.defineProperty(window, \"${VBEN_ADMIN_PRO_APP_CONF}\", {\n      configurable: false,\n      writable: false,\n    });\n  `.replaceAll(/\\s/g, '');\n  return source;\n}\n\nfunction ensureTrailingSlash(path: string) {\n  return path.endsWith('/') ? path : `${path}/`;\n}\n\nexport { viteExtraAppConfigPlugin };\n"
  },
  {
    "path": "internal/vite-config/src/plugins/importmap.ts",
    "content": "/**\n * 参考 https://github.com/jspm/vite-plugin-jspm，调整为需要的功能\n */\nimport type { GeneratorOptions } from '@jspm/generator';\nimport type { Plugin } from 'vite';\n\nimport { Generator } from '@jspm/generator';\nimport { load } from 'cheerio';\nimport { minify } from 'html-minifier-terser';\n\nconst DEFAULT_PROVIDER = 'jspm.io';\n\ntype pluginOptions = {\n  debug?: boolean;\n  defaultProvider?: 'esm.sh' | 'jsdelivr' | 'jspm.io';\n  importmap?: Array<{ name: string; range?: string }>;\n} & GeneratorOptions;\n\n// async function getLatestVersionOfShims() {\n//   const result = await fetch('https://ga.jspm.io/npm:es-module-shims');\n//   const version = result.text();\n//   return version;\n// }\n\nasync function getShimsUrl(provide: string) {\n  // const version = await getLatestVersionOfShims();\n  const version = '1.10.0';\n\n  const shimsSubpath = `dist/es-module-shims.js`;\n  const providerShimsMap: Record<string, string> = {\n    'esm.sh': `https://esm.sh/es-module-shims@${version}/${shimsSubpath}`,\n    // unpkg: `https://unpkg.com/es-module-shims@${version}/${shimsSubpath}`,\n    jsdelivr: `https://cdn.jsdelivr.net/npm/es-module-shims@${version}/${shimsSubpath}`,\n\n    // 下面两个CDN不稳定，暂时不用\n    'jspm.io': `https://ga.jspm.io/npm:es-module-shims@${version}/${shimsSubpath}`,\n  };\n\n  return providerShimsMap[provide] || providerShimsMap[DEFAULT_PROVIDER];\n}\n\nlet generator: Generator;\n\nasync function viteImportMapPlugin(\n  pluginOptions?: pluginOptions,\n): Promise<Plugin[]> {\n  const { importmap } = pluginOptions || {};\n\n  let isSSR = false;\n  let isBuild = false;\n  let installed = false;\n  let installError: Error | null = null;\n\n  const options: pluginOptions = Object.assign(\n    {},\n    {\n      debug: false,\n      defaultProvider: 'jspm.io',\n      env: ['production', 'browser', 'module'],\n      importmap: [],\n    },\n    pluginOptions,\n  );\n\n  generator = new Generator({\n    ...options,\n    baseUrl: process.cwd(),\n  });\n\n  if (options?.debug) {\n    (async () => {\n      for await (const { message, type } of generator.logStream()) {\n        console.log(`${type}: ${message}`);\n      }\n    })();\n  }\n\n  const imports = options.inputMap?.imports ?? {};\n  const scopes = options.inputMap?.scopes ?? {};\n  const firstLayerKeys = Object.keys(scopes);\n  const inputMapScopes: string[] = [];\n  firstLayerKeys.forEach((key) => {\n    inputMapScopes.push(...Object.keys(scopes[key] || {}));\n  });\n  const inputMapImports = Object.keys(imports);\n\n  const allDepNames: string[] = [\n    ...(importmap?.map((item) => item.name) || []),\n    ...inputMapImports,\n    ...inputMapScopes,\n  ];\n  const depNames = new Set<string>(allDepNames);\n\n  const installDeps = importmap?.map((item) => ({\n    range: item.range,\n    target: item.name,\n  }));\n\n  return [\n    {\n      async config(_, { command, isSsrBuild }) {\n        isBuild = command === 'build';\n        isSSR = !!isSsrBuild;\n      },\n      enforce: 'pre',\n      name: 'importmap:external',\n      resolveId(id) {\n        if (isSSR || !isBuild) {\n          return null;\n        }\n\n        if (!depNames.has(id)) {\n          return null;\n        }\n        return { external: true, id };\n      },\n    },\n    {\n      enforce: 'post',\n      name: 'importmap:install',\n      async resolveId() {\n        if (isSSR || !isBuild || installed) {\n          return null;\n        }\n        try {\n          installed = true;\n          await Promise.allSettled(\n            (installDeps || []).map((dep) => generator.install(dep)),\n          );\n        } catch (error: any) {\n          installError = error;\n          installed = false;\n        }\n        return null;\n      },\n    },\n    {\n      buildEnd() {\n        // 未生成importmap时，抛出错误，防止被turbo缓存\n        if (!installed && !isSSR) {\n          installError && console.error(installError);\n          throw new Error('Importmap installation failed.');\n        }\n      },\n      enforce: 'post',\n      name: 'importmap:html',\n      transformIndexHtml: {\n        async handler(html) {\n          if (isSSR || !isBuild) {\n            return html;\n          }\n\n          const importmapJson = generator.getMap();\n\n          if (!importmapJson) {\n            return html;\n          }\n\n          const esModuleShimsSrc = await getShimsUrl(\n            options.defaultProvider || DEFAULT_PROVIDER,\n          );\n\n          const resultHtml = await injectShimsToHtml(\n            html,\n            esModuleShimsSrc || '',\n          );\n          html = await minify(resultHtml || html, {\n            collapseWhitespace: true,\n            minifyCSS: true,\n            minifyJS: true,\n            removeComments: false,\n          });\n\n          return {\n            html,\n            tags: [\n              {\n                attrs: {\n                  type: 'importmap',\n                },\n                injectTo: 'head-prepend',\n                tag: 'script',\n                children: `${JSON.stringify(importmapJson)}`,\n              },\n            ],\n          };\n        },\n        order: 'post',\n      },\n    },\n  ];\n}\n\nasync function injectShimsToHtml(html: string, esModuleShimUrl: string) {\n  const $ = load(html);\n\n  const $script = $(`script[type='module']`);\n\n  if (!$script) {\n    return;\n  }\n\n  const entry = $script.attr('src');\n\n  $script.removeAttr('type');\n  $script.removeAttr('crossorigin');\n  $script.removeAttr('src');\n  $script.html(`\nif (!HTMLScriptElement.supports || !HTMLScriptElement.supports('importmap')) {\n  self.importShim = function () {\n      const promise = new Promise((resolve, reject) => {\n          document.head.appendChild(\n              Object.assign(document.createElement('script'), {\n                  src: '${esModuleShimUrl}',\n                  crossorigin: 'anonymous',\n                  async: true,\n                  onload() {\n                      if (!importShim.$proxy) {\n                          resolve(importShim);\n                      } else {\n                          reject(new Error('No globalThis.importShim found:' + esModuleShimUrl));\n                      }\n                  },\n                  onerror(error) {\n                      reject(error);\n                  },\n              }),\n          );\n      });\n      importShim.$proxy = true;\n      return promise.then((importShim) => importShim(...arguments));\n  };\n}\n\nvar modules = ['${entry}'];\ntypeof importShim === 'function'\n  ? modules.forEach((moduleName) => importShim(moduleName))\n  : modules.forEach((moduleName) => import(moduleName));\n `);\n  $('body').after($script);\n  $('head').remove(`script[type='module']`);\n  return $.html();\n}\n\nexport { viteImportMapPlugin };\n"
  },
  {
    "path": "internal/vite-config/src/plugins/index.ts",
    "content": "import type { PluginOption } from 'vite';\n\nimport type {\n  ApplicationPluginOptions,\n  CommonPluginOptions,\n  ConditionPlugin,\n  LibraryPluginOptions,\n} from '../typing';\n\nimport viteVueI18nPlugin from '@intlify/unplugin-vue-i18n/vite';\nimport viteVue from '@vitejs/plugin-vue';\nimport viteVueJsx from '@vitejs/plugin-vue-jsx';\nimport { visualizer as viteVisualizerPlugin } from 'rollup-plugin-visualizer';\nimport viteCompressPlugin from 'vite-plugin-compression';\nimport viteDtsPlugin from 'vite-plugin-dts';\nimport { createHtmlPlugin as viteHtmlPlugin } from 'vite-plugin-html';\nimport { VitePWA } from 'vite-plugin-pwa';\nimport viteVueDevTools from 'vite-plugin-vue-devtools';\n\nimport { viteArchiverPlugin } from './archiver';\nimport { viteExtraAppConfigPlugin } from './extra-app-config';\nimport { viteImportMapPlugin } from './importmap';\nimport { viteInjectAppLoadingPlugin } from './inject-app-loading';\nimport { viteMetadataPlugin } from './inject-metadata';\nimport { viteLicensePlugin } from './license';\nimport { viteNitroMockPlugin } from './nitro-mock';\nimport { vitePrintPlugin } from './print';\nimport { viteVxeTableImportsPlugin } from './vxe-table';\n\n/**\n * 获取条件成立的 vite 插件\n * @param conditionPlugins\n */\nasync function loadConditionPlugins(conditionPlugins: ConditionPlugin[]) {\n  const plugins: PluginOption[] = [];\n  for (const conditionPlugin of conditionPlugins) {\n    if (conditionPlugin.condition) {\n      const realPlugins = await conditionPlugin.plugins();\n      plugins.push(...realPlugins);\n    }\n  }\n  return plugins.flat();\n}\n\n/**\n * 根据条件获取通用的vite插件\n */\nasync function loadCommonPlugins(\n  options: CommonPluginOptions,\n): Promise<ConditionPlugin[]> {\n  const { devtools, injectMetadata, isBuild, visualizer } = options;\n  return [\n    {\n      condition: true,\n      plugins: () => [\n        viteVue({\n          script: {\n            defineModel: true,\n            // propsDestructure: true,\n          },\n        }),\n        viteVueJsx(),\n      ],\n    },\n\n    {\n      condition: !isBuild && devtools,\n      plugins: () => [viteVueDevTools()],\n    },\n    {\n      condition: injectMetadata,\n      plugins: async () => [await viteMetadataPlugin()],\n    },\n    {\n      condition: isBuild && !!visualizer,\n      plugins: () => [<PluginOption>viteVisualizerPlugin({\n          filename: './node_modules/.cache/visualizer/stats.html',\n          gzipSize: true,\n          open: true,\n        })],\n    },\n  ];\n}\n\n/**\n * 根据条件获取应用类型的vite插件\n */\nasync function loadApplicationPlugins(\n  options: ApplicationPluginOptions,\n): Promise<PluginOption[]> {\n  // 单独取，否则commonOptions拿不到\n  const isBuild = options.isBuild;\n  const env = options.env;\n\n  const {\n    archiver,\n    archiverPluginOptions,\n    compress,\n    compressTypes,\n    extraAppConfig,\n    html,\n    i18n,\n    importmap,\n    importmapOptions,\n    injectAppLoading,\n    license,\n    nitroMock,\n    nitroMockOptions,\n    print,\n    printInfoMap,\n    pwa,\n    pwaOptions,\n    vxeTableLazyImport,\n    ...commonOptions\n  } = options;\n\n  const commonPlugins = await loadCommonPlugins(commonOptions);\n\n  return await loadConditionPlugins([\n    ...commonPlugins,\n    {\n      condition: i18n,\n      plugins: async () => {\n        return [\n          viteVueI18nPlugin({\n            compositionOnly: true,\n            fullInstall: true,\n            runtimeOnly: true,\n          }),\n        ];\n      },\n    },\n    {\n      condition: print,\n      plugins: async () => {\n        return [await vitePrintPlugin({ infoMap: printInfoMap })];\n      },\n    },\n    {\n      condition: vxeTableLazyImport,\n      plugins: async () => {\n        return [await viteVxeTableImportsPlugin()];\n      },\n    },\n    {\n      condition: nitroMock,\n      plugins: async () => {\n        return [await viteNitroMockPlugin(nitroMockOptions)];\n      },\n    },\n\n    {\n      condition: injectAppLoading,\n      plugins: async () => [await viteInjectAppLoadingPlugin(!!isBuild, env)],\n    },\n    {\n      condition: license,\n      plugins: async () => [await viteLicensePlugin()],\n    },\n    {\n      condition: pwa,\n      plugins: () =>\n        VitePWA({\n          injectRegister: false,\n          workbox: {\n            globPatterns: [],\n          },\n          ...pwaOptions,\n          manifest: {\n            display: 'standalone',\n            start_url: '/',\n            theme_color: '#ffffff',\n            ...pwaOptions?.manifest,\n          },\n        }),\n    },\n    {\n      condition: isBuild && !!compress,\n      plugins: () => {\n        const compressPlugins: PluginOption[] = [];\n        if (compressTypes?.includes('brotli')) {\n          compressPlugins.push(\n            viteCompressPlugin({ deleteOriginFile: false, ext: '.br' }),\n          );\n        }\n        if (compressTypes?.includes('gzip')) {\n          compressPlugins.push(\n            viteCompressPlugin({ deleteOriginFile: false, ext: '.gz' }),\n          );\n        }\n        return compressPlugins;\n      },\n    },\n    {\n      condition: !!html,\n      plugins: () => [viteHtmlPlugin({ minify: true })],\n    },\n    {\n      condition: isBuild && importmap,\n      plugins: () => {\n        return [viteImportMapPlugin(importmapOptions)];\n      },\n    },\n    {\n      condition: isBuild && extraAppConfig,\n      plugins: async () => [\n        await viteExtraAppConfigPlugin({ isBuild: true, root: process.cwd() }),\n      ],\n    },\n    {\n      condition: archiver,\n      plugins: async () => {\n        return [await viteArchiverPlugin(archiverPluginOptions)];\n      },\n    },\n  ]);\n}\n\n/**\n * 根据条件获取库类型的vite插件\n */\nasync function loadLibraryPlugins(\n  options: LibraryPluginOptions,\n): Promise<PluginOption[]> {\n  // 单独取，否则commonOptions拿不到\n  const isBuild = options.isBuild;\n  const { dts, ...commonOptions } = options;\n  const commonPlugins = await loadCommonPlugins(commonOptions);\n  return await loadConditionPlugins([\n    ...commonPlugins,\n    {\n      condition: isBuild && !!dts,\n      plugins: () => [viteDtsPlugin({ logLevel: 'error' })],\n    },\n  ]);\n}\n\nexport {\n  loadApplicationPlugins,\n  loadLibraryPlugins,\n  viteArchiverPlugin,\n  viteCompressPlugin,\n  viteDtsPlugin,\n  viteHtmlPlugin,\n  viteVisualizerPlugin,\n  viteVxeTableImportsPlugin,\n};\n"
  },
  {
    "path": "internal/vite-config/src/plugins/inject-app-loading/README.md",
    "content": "# inject-app-loading\n\n用于在应用加载时显示加载动画的插件，可自行选择加载动画的样式。\n"
  },
  {
    "path": "internal/vite-config/src/plugins/inject-app-loading/default-loading-antd.html",
    "content": "<style data-app-loading=\"inject-css\">\n  html {\n    /* same as ant-design-vue/dist/reset.css setting, avoid the title line-height changed */\n    line-height: 1.15;\n  }\n\n  .dark .loading {\n    background-color: #0d0d10;\n  }\n\n  .dark .loading .title {\n    color: rgb(255 255 255 / 85%);\n  }\n\n  .loading {\n    position: fixed;\n    top: 0;\n    left: 0;\n    z-index: 9999;\n    display: flex;\n    flex-direction: column;\n    align-items: center;\n    justify-content: center;\n    width: 100%;\n    height: 100%;\n    overflow: hidden;\n    pointer-events: none;\n    background-color: #f4f7f9;\n  }\n\n  .loading.hidden {\n    visibility: hidden;\n    opacity: 0;\n    transition: all 0.6s ease-out;\n  }\n\n  .loading .title {\n    margin-top: 36px;\n    font-size: 30px;\n    font-weight: 600;\n    color: rgb(0 0 0 / 85%);\n  }\n\n  .dot {\n    position: relative;\n    box-sizing: border-box;\n    display: inline-block;\n    width: 48px;\n    height: 48px;\n    margin-top: 30px;\n    font-size: 32px;\n    transform: rotate(45deg);\n    animation: rotate-ani 1.2s infinite linear;\n  }\n\n  .dot i {\n    position: absolute;\n    display: block;\n    width: 20px;\n    height: 20px;\n    background-color: hsl(var(--primary, 210 100% 50%));\n    border-radius: 100%;\n    opacity: 0.3;\n    transform: scale(0.75);\n    transform-origin: 50% 50%;\n    animation: spin-move-ani 1s infinite linear alternate;\n  }\n\n  .dot i:nth-child(1) {\n    top: 0;\n    left: 0;\n  }\n\n  .dot i:nth-child(2) {\n    top: 0;\n    right: 0;\n    animation-delay: 0.4s;\n  }\n\n  .dot i:nth-child(3) {\n    right: 0;\n    bottom: 0;\n    animation-delay: 0.8s;\n  }\n\n  .dot i:nth-child(4) {\n    bottom: 0;\n    left: 0;\n    animation-delay: 1.2s;\n  }\n\n  @keyframes rotate-ani {\n    to {\n      transform: rotate(405deg);\n    }\n  }\n\n  @keyframes spin-move-ani {\n    to {\n      opacity: 1;\n    }\n  }\n</style>\n<div class=\"loading\" id=\"__app-loading__\">\n  <span class=\"dot\"><i></i><i></i><i></i><i></i></span>\n  <div class=\"title\"><%= VITE_APP_TITLE %></div>\n</div>\n"
  },
  {
    "path": "internal/vite-config/src/plugins/inject-app-loading/default-loading.html",
    "content": "<style data-app-loading=\"inject-css\">\n  html {\n    /* same as ant-design-vue/dist/reset.css setting, avoid the title line-height changed */\n    line-height: 1.15;\n  }\n\n  .loading {\n    position: fixed;\n    top: 0;\n    left: 0;\n    z-index: 9999;\n    display: flex;\n    flex-direction: column;\n    align-items: center;\n    justify-content: center;\n    width: 100%;\n    height: 100%;\n    overflow: hidden;\n    background-color: #f4f7f9;\n\n    /* transition: all 0.8s ease-out; */\n  }\n\n  .loading.hidden {\n    pointer-events: none;\n    visibility: hidden;\n    opacity: 0;\n    transition: all 0.8s ease-out;\n  }\n\n  .dark .loading {\n    background: #0d0d10;\n  }\n\n  .title {\n    margin-top: 66px;\n    font-size: 28px;\n    font-weight: 600;\n    color: rgb(0 0 0 / 85%);\n  }\n\n  .dark .title {\n    color: #fff;\n  }\n\n  .loader {\n    position: relative;\n    width: 48px;\n    height: 48px;\n  }\n\n  .loader::before {\n    position: absolute;\n    top: 60px;\n    left: 0;\n    width: 48px;\n    height: 5px;\n    content: '';\n    background: hsl(var(--primary, 210 100% 50%) / 50%);\n    border-radius: 50%;\n    animation: shadow-ani 0.5s linear infinite;\n  }\n\n  .loader::after {\n    position: absolute;\n    top: 0;\n    left: 0;\n    width: 100%;\n    height: 100%;\n    content: '';\n    background: hsl(var(--primary, 210 100% 50%));\n    border-radius: 4px;\n    animation: jump-ani 0.5s linear infinite;\n  }\n\n  @keyframes jump-ani {\n    15% {\n      border-bottom-right-radius: 3px;\n    }\n\n    25% {\n      transform: translateY(9px) rotate(22.5deg);\n    }\n\n    50% {\n      border-bottom-right-radius: 40px;\n      transform: translateY(18px) scale(1, 0.9) rotate(45deg);\n    }\n\n    75% {\n      transform: translateY(9px) rotate(67.5deg);\n    }\n\n    100% {\n      transform: translateY(0) rotate(90deg);\n    }\n  }\n\n  @keyframes shadow-ani {\n    0%,\n    100% {\n      transform: scale(1, 1);\n    }\n\n    50% {\n      transform: scale(1.2, 1);\n    }\n  }\n</style>\n<div class=\"loading\" id=\"__app-loading__\">\n  <div class=\"loader\"></div>\n  <div class=\"title\"><%= VITE_APP_TITLE %></div>\n</div>\n"
  },
  {
    "path": "internal/vite-config/src/plugins/inject-app-loading/index.ts",
    "content": "import type { PluginOption } from 'vite';\n\nimport fs from 'node:fs';\nimport fsp from 'node:fs/promises';\nimport { join } from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\nimport { readPackageJSON } from '@vben/node-utils';\n\n/**\n * 用于生成将loading样式注入到项目中\n * 为多app提供loading样式，无需在每个 app -> index.html单独引入\n */\nasync function viteInjectAppLoadingPlugin(\n  isBuild: boolean,\n  env: Record<string, any> = {},\n  loadingTemplate = 'loading.html',\n): Promise<PluginOption | undefined> {\n  const loadingHtml = await getLoadingRawByHtmlTemplate(loadingTemplate);\n  const { version } = await readPackageJSON(process.cwd());\n  const envRaw = isBuild ? 'prod' : 'dev';\n  const cacheName = `'${env.VITE_APP_NAMESPACE}-${version}-${envRaw}-preferences-theme'`;\n\n  // 获取缓存的主题\n  // 保证黑暗主题下，刷新页面时，loading也是黑暗主题\n  const injectScript = `\n  <script data-app-loading=\"inject-js\">\n  var theme = localStorage.getItem(${cacheName});\n  document.documentElement.classList.toggle('dark', /dark/.test(theme));\n</script>\n`;\n\n  if (!loadingHtml) {\n    return;\n  }\n\n  return {\n    enforce: 'pre',\n    name: 'vite:inject-app-loading',\n    transformIndexHtml: {\n      handler(html) {\n        const re = /<body\\s*>/;\n        html = html.replace(re, `<body>${injectScript}${loadingHtml}`);\n        return html;\n      },\n      order: 'pre',\n    },\n  };\n}\n\n/**\n * 用于获取loading的html模板\n */\nasync function getLoadingRawByHtmlTemplate(loadingTemplate: string) {\n  // 支持在app内自定义loading模板，模版参考default-loading.html即可\n  let appLoadingPath = join(process.cwd(), loadingTemplate);\n\n  if (!fs.existsSync(appLoadingPath)) {\n    const __dirname = fileURLToPath(new URL('.', import.meta.url));\n    appLoadingPath = join(__dirname, './default-loading.html');\n  }\n\n  return await fsp.readFile(appLoadingPath, 'utf8');\n}\n\nexport { viteInjectAppLoadingPlugin };\n"
  },
  {
    "path": "internal/vite-config/src/plugins/inject-metadata.ts",
    "content": "import type { PluginOption } from 'vite';\n\nimport {\n  dateUtil,\n  findMonorepoRoot,\n  getPackages,\n  readPackageJSON,\n} from '@vben/node-utils';\n\nimport { readWorkspaceManifest } from '@pnpm/workspace.read-manifest';\n\nfunction resolvePackageVersion(\n  pkgsMeta: Record<string, string>,\n  name: string,\n  value: string,\n  catalog: Record<string, string>,\n) {\n  if (value.includes('catalog:')) {\n    return catalog[name];\n  }\n\n  if (value.includes('workspace')) {\n    return pkgsMeta[name];\n  }\n\n  return value;\n}\n\nasync function resolveMonorepoDependencies() {\n  const { packages } = await getPackages();\n  const manifest = await readWorkspaceManifest(findMonorepoRoot());\n  const catalog = manifest?.catalog || {};\n\n  const resultDevDependencies: Record<string, string | undefined> = {};\n  const resultDependencies: Record<string, string | undefined> = {};\n  const pkgsMeta: Record<string, string> = {};\n\n  for (const { packageJson } of packages) {\n    pkgsMeta[packageJson.name] = packageJson.version;\n  }\n\n  for (const { packageJson } of packages) {\n    const { dependencies = {}, devDependencies = {} } = packageJson;\n    for (const [key, value] of Object.entries(dependencies)) {\n      resultDependencies[key] = resolvePackageVersion(\n        pkgsMeta,\n        key,\n        value,\n        catalog,\n      );\n    }\n    for (const [key, value] of Object.entries(devDependencies)) {\n      resultDevDependencies[key] = resolvePackageVersion(\n        pkgsMeta,\n        key,\n        value,\n        catalog,\n      );\n    }\n  }\n  return {\n    dependencies: resultDependencies,\n    devDependencies: resultDevDependencies,\n  };\n}\n\n/**\n * 用于注入项目信息\n */\nasync function viteMetadataPlugin(\n  root = process.cwd(),\n): Promise<PluginOption | undefined> {\n  const { author, description, homepage, license, version } =\n    await readPackageJSON(root);\n\n  const buildTime = dateUtil().format('YYYY-MM-DD HH:mm:ss');\n\n  return {\n    async config() {\n      const { dependencies, devDependencies } =\n        await resolveMonorepoDependencies();\n\n      const isAuthorObject = typeof author === 'object';\n      const authorName = isAuthorObject ? author.name : author;\n      const authorEmail = isAuthorObject ? author.email : null;\n      const authorUrl = isAuthorObject ? author.url : null;\n\n      return {\n        define: {\n          __VBEN_ADMIN_METADATA__: JSON.stringify({\n            authorEmail,\n            authorName,\n            authorUrl,\n            buildTime,\n            dependencies,\n            description,\n            devDependencies,\n            homepage,\n            license,\n            version,\n          }),\n          'import.meta.env.VITE_APP_VERSION': JSON.stringify(version),\n        },\n      };\n    },\n    enforce: 'post',\n    name: 'vite:inject-metadata',\n  };\n}\n\nexport { viteMetadataPlugin };\n"
  },
  {
    "path": "internal/vite-config/src/plugins/license.ts",
    "content": "import type {\n  NormalizedOutputOptions,\n  OutputBundle,\n  OutputChunk,\n} from 'rollup';\nimport type { PluginOption } from 'vite';\n\nimport { EOL } from 'node:os';\n\nimport { dateUtil, readPackageJSON } from '@vben/node-utils';\n\n/**\n * 用于注入版权信息\n * @returns\n */\n\nasync function viteLicensePlugin(\n  root = process.cwd(),\n): Promise<PluginOption | undefined> {\n  const {\n    description = '',\n    homepage = '',\n    version = '',\n  } = await readPackageJSON(root);\n\n  return {\n    apply: 'build',\n    enforce: 'post',\n    generateBundle: {\n      handler: (_options: NormalizedOutputOptions, bundle: OutputBundle) => {\n        const date = dateUtil().format('YYYY-MM-DD ');\n        const copyrightText = `/*!\n  * ruoyi-ai Admin\n  * Version: ${version}\n  * Author: ageer\n  * Copyright (C) 2026 ruoyi-ai\n  * License: MIT License\n  * Description: ${description}\n  * Date Created: ${date}\n  * Homepage: ${homepage}\n  * Contact: ann.vben@gmail.com\n*/\n              `.trim();\n\n        for (const [, fileContent] of Object.entries(bundle)) {\n          if (fileContent.type === 'chunk' && fileContent.isEntry) {\n            const chunkContent = fileContent as OutputChunk;\n            // 插入版权信息\n            const content = chunkContent.code;\n            const updatedContent = `${copyrightText}${EOL}${content}`;\n\n            // 更新bundle\n            (fileContent as OutputChunk).code = updatedContent;\n          }\n        }\n      },\n      order: 'post',\n    },\n    name: 'vite:license',\n  };\n}\n\nexport { viteLicensePlugin };\n"
  },
  {
    "path": "internal/vite-config/src/plugins/nitro-mock.ts",
    "content": "import type { PluginOption } from 'vite';\n\nimport type { NitroMockPluginOptions } from '../typing';\n\nimport { colors, consola, getPackage } from '@vben/node-utils';\n\nimport getPort from 'get-port';\nimport { build, createDevServer, createNitro, prepare } from 'nitropack';\n\nconst hmrKeyRe = /^runtimeConfig\\.|routeRules\\./;\n\nexport const viteNitroMockPlugin = ({\n  mockServerPackage = '@vben/backend-mock',\n  port = 5320,\n  verbose = true,\n}: NitroMockPluginOptions = {}): PluginOption => {\n  return {\n    async configureServer(server) {\n      const availablePort = await getPort({ port });\n      if (availablePort !== port) {\n        return;\n      }\n\n      const pkg = await getPackage(mockServerPackage);\n      if (!pkg) {\n        consola.log(\n          `Package ${mockServerPackage} not found. Skip mock server.`,\n        );\n        return;\n      }\n\n      runNitroServer(pkg.dir, port, verbose);\n\n      const _printUrls = server.printUrls;\n      server.printUrls = () => {\n        _printUrls();\n\n        consola.log(\n          `  ${colors.green('➜')}  ${colors.bold('Nitro Mock Server')}: ${colors.cyan(`http://localhost:${port}/api`)}`,\n        );\n      };\n    },\n    enforce: 'pre',\n    name: 'vite:mock-server',\n  };\n};\n\nasync function runNitroServer(rootDir: string, port: number, verbose: boolean) {\n  let nitro: any;\n  const reload = async () => {\n    if (nitro) {\n      consola.info('Restarting dev server...');\n      if ('unwatch' in nitro.options._c12) {\n        await nitro.options._c12.unwatch();\n      }\n      await nitro.close();\n    }\n    nitro = await createNitro(\n      {\n        dev: true,\n        preset: 'nitro-dev',\n        rootDir,\n      },\n      {\n        c12: {\n          async onUpdate({ getDiff, newConfig }) {\n            const diff = getDiff();\n            if (diff.length === 0) {\n              return;\n            }\n            verbose &&\n              consola.info(\n                `Nitro config updated:\\n${diff\n                  .map((entry) => `  ${entry.toString()}`)\n                  .join('\\n')}`,\n              );\n            await (diff.every((e) => hmrKeyRe.test(e.key))\n              ? nitro.updateConfig(newConfig.config)\n              : reload());\n          },\n        },\n        watch: true,\n      },\n    );\n    nitro.hooks.hookOnce('restart', reload);\n\n    const server = createDevServer(nitro);\n    await server.listen(port, { showURL: false });\n    await prepare(nitro);\n    await build(nitro);\n\n    if (verbose) {\n      console.log('');\n      consola.success(colors.bold(colors.green('Nitro Mock Server started.')));\n    }\n  };\n  return await reload();\n}\n"
  },
  {
    "path": "internal/vite-config/src/plugins/print.ts",
    "content": "import type { PluginOption } from 'vite';\n\nimport type { PrintPluginOptions } from '../typing';\n\nimport { colors } from '@vben/node-utils';\n\nexport const vitePrintPlugin = (\n  options: PrintPluginOptions = {},\n): PluginOption => {\n  const { infoMap = {} } = options;\n\n  return {\n    configureServer(server) {\n      const _printUrls = server.printUrls;\n      server.printUrls = () => {\n        _printUrls();\n\n        for (const [key, value] of Object.entries(infoMap)) {\n          console.log(\n            `  ${colors.green('➜')}  ${colors.bold(key)}: ${colors.cyan(value)}`,\n          );\n        }\n      };\n    },\n    enforce: 'pre',\n    name: 'vite:print-info',\n  };\n};\n"
  },
  {
    "path": "internal/vite-config/src/plugins/vxe-table.ts",
    "content": "import type { PluginOption } from 'vite';\n\nimport { lazyImport, VxeResolver } from 'vite-plugin-lazy-import';\n\nasync function viteVxeTableImportsPlugin(): Promise<PluginOption> {\n  return [\n    lazyImport({\n      resolvers: [\n        VxeResolver({\n          libraryName: 'vxe-table',\n        }),\n        VxeResolver({\n          libraryName: 'vxe-pc-ui',\n        }),\n      ],\n    }),\n  ];\n}\n\nexport { viteVxeTableImportsPlugin };\n"
  },
  {
    "path": "internal/vite-config/src/typing.ts",
    "content": "import type { PluginVisualizerOptions } from 'rollup-plugin-visualizer';\nimport type { ConfigEnv, PluginOption, UserConfig } from 'vite';\nimport type { PluginOptions } from 'vite-plugin-dts';\nimport type { Options as PwaPluginOptions } from 'vite-plugin-pwa';\n\n/**\n * ImportMap 配置接口\n * @description 用于配置模块导入映射，支持自定义导入路径和范围\n * @example\n * ```typescript\n * {\n *   imports: {\n *     'vue': 'https://unpkg.com/vue@3.2.47/dist/vue.esm-browser.js'\n *   },\n *   scopes: {\n *     'https://site.com/': {\n *       'vue': 'https://unpkg.com/vue@3.2.47/dist/vue.esm-browser.js'\n *     }\n *   }\n * }\n * ```\n */\ninterface IImportMap {\n  /** 模块导入映射 */\n  imports?: Record<string, string>;\n  /** 作用域特定的导入映射 */\n  scopes?: {\n    [scope: string]: Record<string, string>;\n  };\n}\n\n/**\n * 打印插件配置选项\n * @description 用于配置控制台打印信息\n */\ninterface PrintPluginOptions {\n  /**\n   * 打印的数据映射\n   * @description 键值对形式的数据，将在控制台打印\n   * @example\n   * ```typescript\n   * {\n   *   'App Version': '1.0.0',\n   *   'Build Time': '2024-01-01'\n   * }\n   * ```\n   */\n  infoMap?: Record<string, string | undefined>;\n}\n\n/**\n * Nitro Mock 插件配置选项\n * @description 用于配置 Nitro Mock 服务器的行为\n */\ninterface NitroMockPluginOptions {\n  /**\n   * Mock 服务器包名\n   * @default '@vbenjs/nitro-mock'\n   */\n  mockServerPackage?: string;\n\n  /**\n   * Mock 服务端口\n   * @default 3000\n   */\n  port?: number;\n\n  /**\n   * 是否打印 Mock 日志\n   * @default false\n   */\n  verbose?: boolean;\n}\n\n/**\n * 归档插件配置选项\n * @description 用于配置构建产物的压缩归档\n */\ninterface ArchiverPluginOptions {\n  /**\n   * 输出文件名\n   * @default 'dist'\n   */\n  name?: string;\n  /**\n   * 输出目录\n   * @default '.'\n   */\n  outputDir?: string;\n}\n\n/**\n * ImportMap 插件配置\n * @description 用于配置模块的 CDN 导入\n */\ninterface ImportmapPluginOptions {\n  /**\n   * CDN 供应商\n   * @default 'jspm.io'\n   * @description 支持 esm.sh 和 jspm.io 两种 CDN 供应商\n   */\n  defaultProvider?: 'esm.sh' | 'jspm.io';\n  /**\n   * ImportMap 配置数组\n   * @description 配置需要从 CDN 导入的包\n   * @example\n   * ```typescript\n   * [\n   *   { name: 'vue' },\n   *   { name: 'pinia', range: '^2.0.0' }\n   * ]\n   * ```\n   */\n  importmap?: Array<{ name: string; range?: string }>;\n  /**\n   * 手动配置 ImportMap\n   * @description 自定义 ImportMap 配置\n   */\n  inputMap?: IImportMap;\n}\n\n/**\n * 条件插件配置\n * @description 用于根据条件动态加载插件\n */\ninterface ConditionPlugin {\n  /**\n   * 判断条件\n   * @description 当条件为 true 时加载插件\n   */\n  condition?: boolean;\n  /**\n   * 插件对象\n   * @description 返回插件数组或 Promise\n   */\n  plugins: () => PluginOption[] | PromiseLike<PluginOption[]>;\n}\n\n/**\n * 通用插件配置选项\n * @description 所有插件共用的基础配置\n */\ninterface CommonPluginOptions {\n  /**\n   * 是否开启开发工具\n   * @default false\n   */\n  devtools?: boolean;\n  /**\n   * 环境变量\n   * @description 自定义环境变量\n   */\n  env?: Record<string, any>;\n  /**\n   * 是否注入元数据\n   * @default true\n   */\n  injectMetadata?: boolean;\n  /**\n   * 是否为构建模式\n   * @default false\n   */\n  isBuild?: boolean;\n  /**\n   * 构建模式\n   * @default 'development'\n   */\n  mode?: string;\n  /**\n   * 是否开启依赖分析\n   * @default false\n   * @description 使用 rollup-plugin-visualizer 分析依赖\n   */\n  visualizer?: boolean | PluginVisualizerOptions;\n}\n\n/**\n * 应用插件配置选项\n * @description 用于配置应用构建时的插件选项\n */\ninterface ApplicationPluginOptions extends CommonPluginOptions {\n  /**\n   * 是否开启压缩归档\n   * @default false\n   * @description 开启后会在打包目录生成 zip 文件\n   */\n  archiver?: boolean;\n  /**\n   * 压缩归档插件配置\n   * @description 配置压缩归档的行为\n   */\n  archiverPluginOptions?: ArchiverPluginOptions;\n  /**\n   * 是否开启压缩\n   * @default false\n   * @description 支持 gzip 和 brotli 压缩\n   */\n  compress?: boolean;\n  /**\n   * 压缩类型\n   * @default ['gzip']\n   * @description 可选的压缩类型\n   */\n  compressTypes?: ('brotli' | 'gzip')[];\n  /**\n   * 是否抽离配置文件\n   * @default false\n   * @description 在构建时抽离配置文件\n   */\n  extraAppConfig?: boolean;\n  /**\n   * 是否开启 HTML 插件\n   * @default true\n   */\n  html?: boolean;\n  /**\n   * 是否开启国际化\n   * @default false\n   */\n  i18n?: boolean;\n  /**\n   * 是否开启 ImportMap CDN\n   * @default false\n   */\n  importmap?: boolean;\n  /**\n   * ImportMap 插件配置\n   */\n  importmapOptions?: ImportmapPluginOptions;\n  /**\n   * 是否注入应用加载动画\n   * @default true\n   */\n  injectAppLoading?: boolean;\n  /**\n   * 是否注入全局 SCSS\n   * @default true\n   */\n  injectGlobalScss?: boolean;\n  /**\n   * 是否注入版权信息\n   * @default true\n   */\n  license?: boolean;\n  /**\n   * 是否开启 Nitro Mock\n   * @default false\n   */\n  nitroMock?: boolean;\n  /**\n   * Nitro Mock 插件配置\n   */\n  nitroMockOptions?: NitroMockPluginOptions;\n  /**\n   * 是否开启控制台打印\n   * @default false\n   */\n  print?: boolean;\n  /**\n   * 打印插件配置\n   */\n  printInfoMap?: PrintPluginOptions['infoMap'];\n  /**\n   * 是否开启 PWA\n   * @default false\n   */\n  pwa?: boolean;\n  /**\n   * PWA 插件配置\n   */\n  pwaOptions?: Partial<PwaPluginOptions>;\n  /**\n   * 是否开启 VXE Table 懒加载\n   * @default false\n   */\n  vxeTableLazyImport?: boolean;\n}\n\n/**\n * 库插件配置选项\n * @description 用于配置库构建时的插件选项\n */\ninterface LibraryPluginOptions extends CommonPluginOptions {\n  /**\n   * 是否开启 DTS 输出\n   * @default true\n   * @description 生成 TypeScript 类型声明文件\n   */\n  dts?: boolean | PluginOptions;\n}\n\n/**\n * 应用配置选项类型\n */\ntype ApplicationOptions = ApplicationPluginOptions;\n\n/**\n * 库配置选项类型\n */\ntype LibraryOptions = LibraryPluginOptions;\n\n/**\n * 应用配置定义函数类型\n * @description 用于定义应用构建配置\n */\ntype DefineApplicationOptions = (config?: ConfigEnv) => Promise<{\n  /** 应用插件配置 */\n  application?: ApplicationOptions;\n  /** Vite 配置 */\n  vite?: UserConfig;\n}>;\n\n/**\n * 库配置定义函数类型\n * @description 用于定义库构建配置\n */\ntype DefineLibraryOptions = (config?: ConfigEnv) => Promise<{\n  /** 库插件配置 */\n  library?: LibraryOptions;\n  /** Vite 配置 */\n  vite?: UserConfig;\n}>;\n\n/**\n * 配置定义类型\n * @description 应用或库的配置定义\n */\ntype DefineConfig = DefineApplicationOptions | DefineLibraryOptions;\n\nexport type {\n  ApplicationPluginOptions,\n  ArchiverPluginOptions,\n  CommonPluginOptions,\n  ConditionPlugin,\n  DefineApplicationOptions,\n  DefineConfig,\n  DefineLibraryOptions,\n  IImportMap,\n  ImportmapPluginOptions,\n  LibraryPluginOptions,\n  NitroMockPluginOptions,\n  PrintPluginOptions,\n};\n"
  },
  {
    "path": "internal/vite-config/src/utils/env.ts",
    "content": "import type { ApplicationPluginOptions } from '../typing';\n\nimport { existsSync } from 'node:fs';\nimport { join } from 'node:path';\n\nimport { fs } from '@vben/node-utils';\nimport dotenv from 'dotenv';\n\nconst getBoolean = (value: string | undefined) => value === 'true';\n\nconst getString = (value: string | undefined, fallback: string) =>\n  value ?? fallback;\n\nconst getNumber = (value: string | undefined, fallback: number) =>\n  Number(value) || fallback;\n\n/**\n * 获取当前环境下生效的配置文件名\n */\nfunction getConfFiles() {\n  const script = process.env.npm_lifecycle_script as string;\n  const reg = /--mode ([\\d_a-z]+)/;\n  const result = reg.exec(script);\n  let mode = 'production';\n  if (result) {\n    mode = result[1] as string;\n  }\n  return ['.env', '.env.local', `.env.${mode}`, `.env.${mode}.local`];\n}\n\n/**\n * Get the environment variables starting with the specified prefix\n * @param match prefix\n * @param confFiles ext\n */\nasync function loadEnv<T = Record<string, string>>(\n  match = 'VITE_GLOB_',\n  confFiles = getConfFiles(),\n) {\n  let envConfig = {};\n\n  for (const confFile of confFiles) {\n    try {\n      const confFilePath = join(process.cwd(), confFile);\n      if (existsSync(confFilePath)) {\n        const envPath = await fs.readFile(confFilePath, {\n          encoding: 'utf8',\n        });\n        const env = dotenv.parse(envPath);\n        envConfig = { ...envConfig, ...env };\n      }\n    } catch (error) {\n      console.error(`Error while parsing ${confFile}`, error);\n    }\n  }\n  const reg = new RegExp(`^(${match})`);\n  Object.keys(envConfig).forEach((key) => {\n    if (!reg.test(key)) {\n      Reflect.deleteProperty(envConfig, key);\n    }\n  });\n  return envConfig as T;\n}\n\nasync function loadAndConvertEnv(\n  match = 'VITE_',\n  confFiles = getConfFiles(),\n): Promise<\n  {\n    appTitle: string;\n    base: string;\n    port: number;\n  } & Partial<ApplicationPluginOptions>\n> {\n  const envConfig = await loadEnv(match, confFiles);\n\n  const {\n    VITE_APP_TITLE,\n    VITE_ARCHIVER,\n    VITE_BASE,\n    VITE_COMPRESS,\n    VITE_DEVTOOLS,\n    VITE_INJECT_APP_LOADING,\n    VITE_NITRO_MOCK,\n    VITE_PORT,\n    VITE_PWA,\n    VITE_VISUALIZER,\n  } = envConfig;\n\n  const compressTypes = (VITE_COMPRESS ?? '')\n    .split(',')\n    .filter((item) => item === 'brotli' || item === 'gzip');\n\n  return {\n    appTitle: getString(VITE_APP_TITLE, 'Vben Admin'),\n    archiver: getBoolean(VITE_ARCHIVER),\n    base: getString(VITE_BASE, '/'),\n    compress: compressTypes.length > 0,\n    compressTypes,\n    devtools: getBoolean(VITE_DEVTOOLS),\n    injectAppLoading: getBoolean(VITE_INJECT_APP_LOADING),\n    nitroMock: getBoolean(VITE_NITRO_MOCK),\n    port: getNumber(VITE_PORT, 5173),\n    pwa: getBoolean(VITE_PWA),\n    visualizer: getBoolean(VITE_VISUALIZER),\n  };\n}\n\nexport { loadAndConvertEnv, loadEnv };\n"
  },
  {
    "path": "internal/vite-config/tsconfig.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/tsconfig\",\n  \"extends\": \"@vben/tsconfig/node.json\",\n  \"include\": [\"src\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"vben-admin-monorepo\",\n  \"version\": \"5.5.9\",\n  \"private\": true,\n  \"keywords\": [\n    \"monorepo\",\n    \"turbo\",\n    \"vben\",\n    \"vben admin\",\n    \"vben pro\",\n    \"vue\",\n    \"vue admin\",\n    \"vue vben admin\",\n    \"vue vben admin pro\",\n    \"vue3\"\n  ],\n  \"homepage\": \"https://github.com/vbenjs/vue-vben-admin\",\n  \"bugs\": \"https://github.com/vbenjs/vue-vben-admin/issues\",\n  \"repository\": \"vbenjs/vue-vben-admin.git\",\n  \"license\": \"MIT\",\n  \"author\": {\n    \"name\": \"vben\",\n    \"email\": \"ann.vben@gmail.com\",\n    \"url\": \"https://github.com/anncwb\"\n  },\n  \"type\": \"module\",\n  \"scripts\": {\n    \"build\": \"cross-env NODE_OPTIONS=--max-old-space-size=8192 turbo build\",\n    \"build:analyze\": \"turbo build:analyze\",\n    \"build:antd\": \"pnpm run build --filter=@vben/web-antd build:prod\",\n    \"build:antd:test\": \"pnpm run build --filter=@vben/web-antd build:test\",\n    \"build:docker\": \"./scripts/deploy/build-local-docker-image.sh\",\n    \"build:docs\": \"pnpm run build --filter=@vben/docs\",\n    \"build:play\": \"pnpm run build --filter=@vben/playground\",\n    \"changeset\": \"pnpm exec changeset\",\n    \"check\": \"pnpm run check:circular && pnpm run check:dep && pnpm run check:type && pnpm check:cspell\",\n    \"check:circular\": \"vsh check-circular\",\n    \"check:cspell\": \"cspell lint **/*.ts **/README.md .changeset/*.md --no-progress\",\n    \"check:dep\": \"vsh check-dep\",\n    \"check:type\": \"turbo run typecheck\",\n    \"clean\": \"node ./scripts/clean.mjs\",\n    \"commit\": \"czg\",\n    \"dev\": \"turbo-run dev\",\n    \"dev:antd\": \"pnpm -F @vben/web-antd run dev\",\n    \"dev:docs\": \"pnpm -F @vben/docs run dev\",\n    \"dev:play\": \"pnpm -F @vben/playground run dev\",\n    \"format\": \"vsh lint --format\",\n    \"lint\": \"vsh lint\",\n    \"postinstall\": \"pnpm -r run stub --if-present\",\n    \"preinstall\": \"npx only-allow pnpm\",\n    \"preview\": \"turbo-run preview\",\n    \"publint\": \"vsh publint\",\n    \"reinstall\": \"pnpm clean --del-lock && pnpm install\",\n    \"test:unit\": \"vitest run --dom\",\n    \"test:e2e\": \"turbo run test:e2e\",\n    \"update:deps\": \"npx taze -r -w\",\n    \"version\": \"pnpm exec changeset version && pnpm install --no-frozen-lockfile\",\n    \"catalog\": \"pnpx codemod pnpm/catalog\"\n  },\n  \"devDependencies\": {\n    \"@changesets/changelog-github\": \"catalog:\",\n    \"@changesets/cli\": \"catalog:\",\n    \"@playwright/test\": \"catalog:\",\n    \"@types/node\": \"catalog:\",\n    \"@vben/commitlint-config\": \"workspace:*\",\n    \"@vben/eslint-config\": \"workspace:*\",\n    \"@vben/prettier-config\": \"workspace:*\",\n    \"@vben/stylelint-config\": \"workspace:*\",\n    \"@vben/tailwind-config\": \"workspace:*\",\n    \"@vben/tsconfig\": \"workspace:*\",\n    \"@vben/turbo-run\": \"workspace:*\",\n    \"@vben/vite-config\": \"workspace:*\",\n    \"@vben/vsh\": \"workspace:*\",\n    \"@vitejs/plugin-vue\": \"catalog:\",\n    \"@vitejs/plugin-vue-jsx\": \"catalog:\",\n    \"@vue/test-utils\": \"catalog:\",\n    \"autoprefixer\": \"catalog:\",\n    \"cross-env\": \"catalog:\",\n    \"cspell\": \"catalog:\",\n    \"happy-dom\": \"catalog:\",\n    \"is-ci\": \"catalog:\",\n    \"lefthook\": \"catalog:\",\n    \"playwright\": \"catalog:\",\n    \"rimraf\": \"catalog:\",\n    \"tailwindcss\": \"catalog:\",\n    \"turbo\": \"catalog:\",\n    \"typescript\": \"catalog:\",\n    \"unbuild\": \"catalog:\",\n    \"vite\": \"catalog:\",\n    \"vitest\": \"catalog:\",\n    \"vue\": \"catalog:\",\n    \"vue-tsc\": \"catalog:\"\n  },\n  \"engines\": {\n    \"node\": \">=20.10.0\",\n    \"pnpm\": \">=9.12.0\"\n  },\n  \"packageManager\": \"pnpm@10.14.0\",\n  \"pnpm\": {\n    \"peerDependencyRules\": {\n      \"allowedVersions\": {\n        \"eslint\": \"*\"\n      }\n    },\n    \"overrides\": {\n      \"@ast-grep/napi\": \"catalog:\",\n      \"@ctrl/tinycolor\": \"catalog:\",\n      \"clsx\": \"catalog:\",\n      \"esbuild\": \"0.25.3\",\n      \"pinia\": \"catalog:\",\n      \"vue\": \"catalog:\"\n    },\n    \"neverBuiltDependencies\": [\n      \"canvas\",\n      \"node-gyp\"\n    ]\n  }\n}\n"
  },
  {
    "path": "packages/@core/README.md",
    "content": "# @vben-core\n\n系统一些比较基础的SDK和UI组件库，该目录后续完善后，可能会迁移出去或者发布到npm，请勿将任何业务逻辑和业务包放在该目录。\n"
  },
  {
    "path": "packages/@core/base/README.md",
    "content": "# base\n\n基础共享包，请勿引入 workspace 依赖\n\n-\n"
  },
  {
    "path": "packages/@core/base/design/package.json",
    "content": "{\n  \"name\": \"@vben-core/design\",\n  \"version\": \"5.5.9\",\n  \"homepage\": \"https://github.com/vbenjs/vue-vben-admin\",\n  \"bugs\": \"https://github.com/vbenjs/vue-vben-admin/issues\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/vbenjs/vue-vben-admin.git\",\n    \"directory\": \"packages/@vben-core/base/design\"\n  },\n  \"license\": \"MIT\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"build\": \"pnpm vite build\",\n    \"prepublishOnly\": \"npm run build\"\n  },\n  \"files\": [\n    \"dist\",\n    \"src\"\n  ],\n  \"main\": \"./dist/index.mjs\",\n  \"module\": \"./dist/index.mjs\",\n  \"exports\": {\n    \"./bem\": {\n      \"development\": \"./src/scss-bem/bem.scss\",\n      \"default\": \"./dist/bem.scss\"\n    },\n    \".\": {\n      \"types\": \"./src/index.ts\",\n      \"development\": \"./src/index.ts\",\n      \"default\": \"./dist/design.css\"\n    }\n  },\n  \"publishConfig\": {\n    \"exports\": {\n      \".\": {\n        \"default\": \"./dist/index.mjs\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "packages/@core/base/design/src/css/global.css",
    "content": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n@layer base {\n  *,\n  ::after,\n  ::before {\n    @apply border-border;\n\n    box-sizing: border-box;\n    border-style: solid;\n    border-width: 0;\n  }\n\n  html {\n    @apply text-foreground bg-background font-sans text-[100%];\n\n    font-variation-settings: normal;\n    line-height: 1.15;\n    text-size-adjust: 100%;\n    font-synthesis-weight: none;\n    scroll-behavior: smooth;\n    text-rendering: optimizelegibility;\n    -webkit-tap-highlight-color: transparent;\n\n    /* -webkit-font-smoothing: antialiased;\n    -moz-osx-font-smoothing: grayscale; */\n  }\n\n  #app,\n  body,\n  html {\n    @apply size-full;\n\n    /* scrollbar-gutter: stable; */\n  }\n\n  body {\n    min-height: 100vh;\n\n    /* pointer-events: auto !important; */\n\n    /* overflow: overlay; */\n\n    /* -webkit-font-smoothing: antialiased; */\n\n    /* -moz-osx-font-smoothing: grayscale; */\n  }\n\n  a,\n  a:active,\n  a:hover,\n  a:link,\n  a:visited {\n    @apply no-underline;\n  }\n\n  ::view-transition-new(root),\n  ::view-transition-old(root) {\n    @apply animate-none mix-blend-normal;\n  }\n\n  ::view-transition-old(root) {\n    @apply z-[1];\n  }\n\n  ::view-transition-new(root) {\n    @apply z-[2147483646];\n  }\n\n  html.dark::view-transition-old(root) {\n    @apply z-[2147483646];\n  }\n\n  html.dark::view-transition-new(root) {\n    @apply z-[1];\n  }\n\n  input::placeholder,\n  textarea::placeholder {\n    @apply opacity-100;\n  }\n\n  /* input:-webkit-autofill {\n    @apply border-none;\n\n    box-shadow: 0 0 0 1000px transparent inset;\n  } */\n\n  input[type='number']::-webkit-inner-spin-button,\n  input[type='number']::-webkit-outer-spin-button {\n    @apply m-0 appearance-none;\n  }\n\n  /* 只有非mac下才进行调整，mac下使用默认滚动条 */\n  html:not([data-platform='macOs']) {\n    ::-webkit-scrollbar {\n      @apply h-[10px] w-[10px];\n    }\n\n    ::-webkit-scrollbar-thumb {\n      @apply bg-border rounded-sm border-none;\n    }\n\n    ::-webkit-scrollbar-track {\n      @apply rounded-sm border-none bg-transparent shadow-none;\n    }\n\n    ::-webkit-scrollbar-button {\n      @apply hidden;\n    }\n  }\n}\n\n@layer components {\n  .flex-center {\n    @apply flex items-center justify-center;\n  }\n\n  .flex-col-center {\n    @apply flex flex-col items-center justify-center;\n  }\n\n  .outline-box {\n    @apply outline-border relative cursor-pointer rounded-md p-1 outline outline-1;\n  }\n\n  .outline-box::after {\n    @apply absolute left-1/2 top-1/2 z-20 h-0 w-[1px] rounded-sm opacity-0 outline outline-2 outline-transparent transition-all duration-300 content-[\"\"];\n  }\n\n  .outline-box.outline-box-active {\n    @apply outline-primary outline outline-2;\n  }\n\n  .outline-box.outline-box-active::after {\n    display: none;\n  }\n\n  .outline-box:not(.outline-box-active):hover::after {\n    @apply outline-primary left-0 top-0 h-full w-full p-1 opacity-100;\n  }\n\n  .vben-link {\n    @apply text-primary hover:text-primary-hover active:text-primary-active cursor-pointer;\n  }\n\n  .card-box {\n    @apply bg-card text-card-foreground border-border rounded-xl border;\n  }\n}\n\nhtml.invert-mode {\n  @apply invert;\n}\n\nhtml.grayscale-mode {\n  @apply grayscale;\n}\n"
  },
  {
    "path": "packages/@core/base/design/src/css/nprogress.css",
    "content": "/* Make clicks pass-through */\n#nprogress {\n  @apply pointer-events-none;\n}\n\n#nprogress .bar {\n  @apply bg-primary fixed left-0 top-0 z-[1031] h-[2px] w-full;\n}\n\n/* Fancy blur effect */\n#nprogress .peg {\n  @apply absolute right-0 block h-full w-[100px];\n\n  box-shadow:\n    0 0 10px hsl(var(--primary)),\n    0 0 5px hsl(var(--primary));\n  opacity: 1;\n  transform: rotate(3deg) translate(0, -4px);\n}\n\n/* Remove these to get rid of the spinner */\n#nprogress .spinner {\n  @apply fixed right-4 top-4 z-[1031] block;\n}\n\n#nprogress .spinner-icon {\n  @apply border-t-primary border-l-primary size-4 rounded-full border-[2px] border-solid border-transparent;\n\n  animation: nprogress-spinner 400ms linear infinite;\n}\n\n.nprogress-custom-parent {\n  @apply relative overflow-hidden;\n}\n\n.nprogress-custom-parent #nprogress .spinner,\n.nprogress-custom-parent #nprogress .bar {\n  @apply absolute;\n}\n\n@keyframes nprogress-spinner {\n  0% {\n    transform: rotate(0deg);\n  }\n\n  100% {\n    transform: rotate(360deg);\n  }\n}\n\n@keyframes nprogress-spinner {\n  0% {\n    transform: rotate(0deg);\n  }\n\n  100% {\n    transform: rotate(360deg);\n  }\n}\n"
  },
  {
    "path": "packages/@core/base/design/src/css/transition.css",
    "content": ".slide-up-enter-active,\n.slide-up-leave-active {\n  transition: 0.25s cubic-bezier(0.25, 0.8, 0.5, 1);\n}\n\n.slide-up-move {\n  transition: transform 0.3s;\n}\n\n.slide-up-enter-from,\n.slide-up-leave-to {\n  opacity: 0;\n  transform: translateY(-15px);\n}\n\n.slide-down-enter-active,\n.slide-down-leave-active {\n  transition: 0.25s cubic-bezier(0.25, 0.8, 0.5, 1);\n}\n\n.slide-down-move {\n  transition: transform 0.3s;\n}\n\n.slide-down-enter-from,\n.slide-down-leave-to {\n  opacity: 0;\n  transform: translateY(15px);\n}\n\n.slide-left-enter-active,\n.slide-left-leave-active {\n  transition: 0.25s cubic-bezier(0.25, 0.8, 0.5, 1);\n}\n\n.slide-left-move {\n  transition: transform 0.3s;\n}\n\n.slide-left-enter-from,\n.slide-left-leave-to {\n  opacity: 0;\n  transform: translate(-15px);\n}\n\n.slide-right-enter-active,\n.slide-right-leave-active {\n  transition: 0.25s cubic-bezier(0.25, 0.8, 0.5, 1);\n}\n\n.slide-right-move {\n  transition: transform 0.3s;\n}\n\n.slide-right-enter-from,\n.slide-right-leave-to {\n  opacity: 0;\n  transform: translate(15px);\n}\n\n.fade-transition-enter-active,\n.fade-transition-leave-active {\n  transition: opacity 0.2s ease-in-out;\n}\n\n.fade-transition-enter-from,\n.fade-transition-leave-to {\n  opacity: 0;\n}\n\n.fade-enter-active,\n.fade-leave-active {\n  transition: opacity 0.2s ease-in-out;\n}\n\n.fade-enter-from,\n.fade-leave-to {\n  opacity: 0;\n}\n\n.fade-slide-leave-active,\n.fade-slide-enter-active {\n  transition: all 0.3s;\n}\n\n.fade-slide-enter-from {\n  opacity: 0;\n  transform: translate(-30px);\n}\n\n.fade-slide-leave-to {\n  opacity: 0;\n  transform: translate(30px);\n}\n\n.fade-down-enter-active,\n.fade-down-leave-active {\n  transition:\n    opacity 0.25s,\n    transform 0.3s;\n}\n\n.fade-down-enter-from {\n  opacity: 0;\n  transform: translateY(-10%);\n}\n\n.fade-down-leave-to {\n  opacity: 0;\n  transform: translateY(10%);\n}\n\n.fade-scale-leave-active,\n.fade-scale-enter-active {\n  transition: all 0.28s;\n}\n\n.fade-scale-enter-from {\n  opacity: 0;\n  transform: scale(1.2);\n}\n\n.fade-scale-leave-to {\n  opacity: 0;\n  transform: scale(0.8);\n}\n\n.fade-up-enter-active,\n.fade-up-leave-active {\n  transition:\n    opacity 0.2s,\n    transform 0.25s;\n}\n\n.fade-up-enter-from {\n  opacity: 0;\n  transform: translateY(10%);\n}\n\n.fade-up-leave-to {\n  opacity: 0;\n  transform: translateY(-10%);\n}\n\n@keyframes fade-slide {\n  0% {\n    opacity: 0;\n    transform: translate(-30px);\n  }\n\n  50% {\n    opacity: 1;\n  }\n\n  100% {\n    opacity: 0;\n    transform: translate(30px);\n  }\n}\n\n@keyframes fade {\n  0% {\n    opacity: 0;\n  }\n\n  50% {\n    opacity: 1;\n  }\n\n  100% {\n    opacity: 0;\n  }\n}\n\n@keyframes fade-up {\n  0% {\n    opacity: 0;\n    transform: translateY(10%);\n  }\n\n  50% {\n    opacity: 1;\n  }\n\n  100% {\n    opacity: 0;\n    transform: translateY(-10%);\n  }\n}\n\n@keyframes fade-down {\n  0% {\n    opacity: 0;\n    transform: translateY(-10%);\n  }\n\n  50% {\n    opacity: 1;\n  }\n\n  100% {\n    opacity: 0;\n    transform: translateY(10%);\n  }\n}\n\n.fade-slow {\n  animation: fade 3s infinite;\n}\n\n.fade-slide-slow {\n  animation: fade-slide 3s infinite;\n}\n\n.fade-up-slow {\n  animation: fade-up 3s infinite;\n}\n\n.fade-down-slow {\n  animation: fade-down 3s infinite;\n}\n\n.collapse-transition {\n  transition:\n    0.2s height ease-in-out,\n    0.2s padding-top ease-in-out,\n    0.2s padding-bottom ease-in-out;\n}\n\n.collapse-transition-leave-active,\n.collapse-transition-enter-active {\n  transition:\n    0.2s max-height ease-in-out,\n    0.2s padding-top ease-in-out,\n    0.2s margin-top ease-in-out;\n}\n"
  },
  {
    "path": "packages/@core/base/design/src/css/ui.css",
    "content": ".side-content {\n  animation-duration: 0.2s;\n  animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);\n}\n\n.side-content[data-side='top'] {\n  animation-name: slide-up;\n}\n\n.side-content[data-side='bottom'] {\n  animation-name: slide-down;\n}\n\n.side-content[data-side='left'] {\n  animation-name: slide-left;\n}\n\n.side-content[data-side='right'] {\n  animation-name: slide-right;\n}\n\n.breadcrumb-transition-enter-active {\n  transition:\n    transform 0.4s cubic-bezier(0.76, 0, 0.24, 1),\n    opacity 0.4s cubic-bezier(0.76, 0, 0.24, 1);\n}\n\n.breadcrumb-transition-leave-active {\n  display: none;\n}\n\n.breadcrumb-transition-enter-from {\n  opacity: 0;\n  transform: translateX(30px) skewX(-30deg);\n}\n\n@keyframes slide-down {\n  from {\n    opacity: 0;\n    transform: translateY(-10px);\n  }\n\n  to {\n    opacity: 1;\n    transform: translateY(0);\n  }\n}\n\n@keyframes slide-left {\n  from {\n    opacity: 0;\n    transform: translateX(-10px);\n  }\n\n  to {\n    opacity: 1;\n    transform: translateX(0);\n  }\n}\n\n@keyframes slide-right {\n  from {\n    opacity: 0;\n    transform: translateX(-10px);\n  }\n\n  to {\n    opacity: 1;\n    transform: translateX(0);\n  }\n}\n\n@keyframes slide-up {\n  from {\n    opacity: 0;\n    transform: translateY(10px);\n  }\n\n  to {\n    opacity: 1;\n    transform: translateY(0);\n  }\n}\n\n.z-popup {\n  z-index: var(--popup-z-index);\n}\n\n@keyframes shrink {\n  0% {\n    transform: scale(1);\n  }\n\n  50% {\n    transform: scale(0.9);\n  }\n\n  100% {\n    transform: scale(1);\n  }\n}\n"
  },
  {
    "path": "packages/@core/base/design/src/design-tokens/dark.css",
    "content": ".dark,\n.dark[data-theme='custom'],\n.dark[data-theme='default'] {\n  /* Default background color of <body />...etc */\n  --background: 222.34deg 10.43% 12.27%;\n\n  /* 主体区域背景色 */\n  --background-deep: 220deg 13.06% 9%;\n  --foreground: 0 0% 95%;\n\n  /* Background color for <Card /> */\n  --card: 222.34deg 10.43% 12.27%;\n\n  /* --card: 222.2 84% 4.9%; */\n  --card-foreground: 210 40% 98%;\n\n  /* Background color for popovers such as <DropdownMenu />, <HoverCard />, <Popover /> */\n\n  /* --popover: 222.82deg 8.43% 12.27%; */\n\n  /* 弹出层的背景色与主题区域背景色太过接近  */\n  --popover: 0 0% 14.2%;\n  --popover-foreground: 210 40% 98%;\n\n  /* Muted backgrounds such as <TabsList />, <Skeleton /> and <Switch /> */\n\n  /* --muted: 220deg 6.82% 17.25%; */\n\n  /* --muted-foreground: 215 20.2% 65.1%; */\n\n  --muted: 240 3.7% 15.9%;\n  --muted-foreground: 240 5% 64.9%;\n\n  /* 主题颜色 */\n\n  /* --primary: 245 82% 67%; */\n  --primary-foreground: 0 0% 98%;\n\n  /* Used for destructive actions such as <Button variant=\"destructive\"> */\n\n  --destructive: 359.21 68.47% 56.47%;\n  --destructive-foreground: 0 0% 98%;\n\n  /* Used for success actions such as <message> */\n\n  --info: 180, 1.54%, 12.75%;\n  --info-foreground: 220, 4%, 58%;\n\n  /* Used for success actions such as <message> */\n\n  --success: 144 57% 58%;\n  --success-foreground: 0 0% 98%;\n\n  /* Used for warning actions such as <message> */\n\n  --warning: 42 84% 61%;\n  --warning-foreground: 0 0% 98%;\n\n  /* 颜色次要 */\n  --secondary: 240 5% 17%;\n  --secondary-foreground: 0 0% 98%;\n\n  /* Used for accents such as hover effects on <DropdownMenuItem>, <SelectItem>...etc */\n  --accent: 216 5% 19%;\n  --accent-dark: 240 0% 22%;\n  --accent-darker: 240 0% 26%;\n  --accent-lighter: 216 5% 12%;\n  --accent-hover: 216 5% 24%;\n  --accent-foreground: 0 0% 98%;\n\n  /* Darker color */\n  --heavy: 216 5% 24%;\n  --heavy-foreground: var(--accent-foreground);\n\n  /* Default border color */\n  --border: 240 3.7% 22%;\n\n  /* Border color for inputs such as <Input />, <Select />, <Textarea /> */\n  --input: 0deg 0% 100% / 10%;\n  --input-placeholder: 218deg 11% 65%;\n  --input-background: 0deg 0% 100% / 5%;\n\n  /* Used for focus ring */\n  --ring: 222.2 84% 4.9%;\n\n  /* 基本圆角大小 */\n  --radius: 0.5rem;\n\n  /* ============= Custom ============= */\n\n  /* 遮罩颜色 */\n  --overlay: 0deg 0% 0% / 40%;\n  --overlay-content: 0deg 0% 0% / 40%;\n\n  /* 基本文字大小 */\n  --font-size-base: 16px;\n\n  /* =============component & UI============= */\n\n  --sidebar: 222.34deg 10.43% 12.27%;\n  --sidebar-deep: 220deg 13.06% 9%;\n  --menu: var(--sidebar);\n\n  /* header */\n  --header: 222.34deg 10.43% 12.27%;\n\n  color-scheme: dark;\n}\n\n.dark[data-theme='violet'],\n[data-theme='violet'] .dark {\n  --background: 224 71.4% 4.1%;\n  --background-deep: var(--background);\n  --foreground: 210 20% 98%;\n  --card: 224 71.4% 4.1%;\n  --card-foreground: 210 20% 98%;\n  --popover: 224 71.4% 4.1%;\n  --popover-foreground: 210 20% 98%;\n  --primary-foreground: 210 20% 98%;\n  --secondary: 215 27.9% 16.9%;\n  --secondary-foreground: 210 20% 98%;\n  --muted: 215 27.9% 16.9%;\n  --muted-foreground: 217.9 10.6% 64.9%;\n  --accent: 215 27.9% 16.9%;\n  --accent-foreground: 210 20% 98%;\n  --destructive: 359.21 68.47% 56.47%;\n  --destructive-foreground: 210 20% 98%;\n  --border: 215 27.9% 16.9%;\n  --input: 215 27.9% 16.9%;\n  --ring: 263.4 70% 50.4%;\n  --sidebar: 224 71.4% 4.1%;\n  --sidebar-deep: 224 71.4% 4.1%;\n  --header: 224 71.4% 4.1%;\n}\n\n.dark[data-theme='pink'],\n[data-theme='pink'] .dark {\n  --background: 20 14.3% 4.1%;\n  --background-deep: var(--background);\n  --foreground: 0 0% 95%;\n  --card: 0 0% 9%;\n  --card-foreground: 0 0% 95%;\n  --popover: 0 0% 9%;\n  --popover-foreground: 0 0% 95%;\n  --primary-foreground: 355.7 100% 97.3%;\n  --secondary: 240 3.7% 15.9%;\n  --secondary-foreground: 0 0% 98%;\n  --muted: 0 0% 15%;\n  --muted-foreground: 240 5% 64.9%;\n  --accent: 12 6.5% 15.1%;\n  --accent-foreground: 0 0% 98%;\n  --destructive: 359.21 68.47% 56.47%;\n  --destructive-foreground: 0 85.7% 97.3%;\n  --border: 240 3.7% 15.9%;\n  --input: 240 3.7% 15.9%;\n  --ring: 346.8 77.2% 49.8%;\n  --sidebar: 20 14.3% 4.1%;\n  --sidebar-deep: 20 14.3% 4.1%;\n  --header: 20 14.3% 4.1%;\n}\n\n.dark[data-theme='rose'],\n[data-theme='rose'] .dark {\n  --background: 0 0% 3.9%;\n  --background-deep: var(--background);\n  --foreground: 0 0% 98%;\n  --card: 0 0% 3.9%;\n  --card-foreground: 0 0% 98%;\n  --popover: 0 0% 3.9%;\n  --popover-foreground: 0 0% 98%;\n  --primary-foreground: 0 85.7% 97.3%;\n  --secondary: 0 0% 14.9%;\n  --secondary-foreground: 0 0% 98%;\n  --muted: 0 0% 14.9%;\n  --muted-foreground: 0 0% 63.9%;\n  --accent: 0 0% 14.9%;\n  --accent-foreground: 0 0% 98%;\n  --destructive: 359.21 68.47% 56.47%;\n  --destructive-foreground: 0 0% 98%;\n  --border: 0 0% 14.9%;\n  --input: 0 0% 14.9%;\n  --ring: 0 72.2% 50.6%;\n  --sidebar: 0 0% 3.9%;\n  --sidebar-deep: 0 0% 3.9%;\n  --header: 0 0% 3.9%;\n}\n\n.dark[data-theme='sky-blue'],\n[data-theme='sky-blue'] .dark {\n  --background: 222.2 84% 4.9%;\n  --background-deep: var(--background);\n  --foreground: 210 40% 98%;\n  --card: 222.2 84% 4.9%;\n  --card-foreground: 210 40% 98%;\n  --popover: 222.2 84% 4.9%;\n  --popover-foreground: 210 40% 98%;\n  --primary-foreground: 210 20% 98%;\n  --secondary: 217.2 32.6% 17.5%;\n  --secondary-foreground: 210 40% 98%;\n  --muted: 217.2 32.6% 17.5%;\n  --muted-foreground: 215 20.2% 65.1%;\n  --accent: 217.2 32.6% 17.5%;\n  --accent-foreground: 210 40% 98%;\n  --destructive: 359.21 68.47% 56.47%;\n  --destructive-foreground: 210 40% 98%;\n  --border: 217.2 32.6% 17.5%;\n  --input: 217.2 32.6% 17.5%;\n  --ring: 224.3 76.3% 48%;\n  --sidebar: 222.2 84% 4.9%;\n  --sidebar-deep: 222.2 84% 4.9%;\n  --header: 222.2 84% 4.9%;\n}\n\n.dark[data-theme='deep-blue'],\n[data-theme='deep-blue'] .dark {\n  --background: 222.2 84% 4.9%;\n  --background-deep: var(--background);\n  --foreground: 210 40% 98%;\n  --card: 222.2 84% 4.9%;\n  --card-foreground: 210 40% 98%;\n  --popover: 222.2 84% 4.9%;\n  --popover-foreground: 210 40% 98%;\n  --primary-foreground: 210 20% 98%;\n  --secondary: 217.2 32.6% 17.5%;\n  --secondary-foreground: 210 40% 98%;\n  --muted: 217.2 32.6% 17.5%;\n  --muted-foreground: 215 20.2% 65.1%;\n  --accent: 217.2 32.6% 17.5%;\n  --accent-foreground: 210 40% 98%;\n  --destructive: 359.21 68.47% 56.47%;\n  --destructive-foreground: 210 40% 98%;\n  --border: 217.2 32.6% 17.5%;\n  --input: 217.2 32.6% 17.5%;\n  --ring: 224.3 76.3% 48%;\n  --sidebar: 222.2 84% 4.9%;\n  --sidebar-deep: 222.2 84% 4.9%;\n  --header: 222.2 84% 4.9%;\n}\n\n.dark[data-theme='green'],\n[data-theme='green'] .dark {\n  --background: 20 14.3% 4.1%;\n  --background-deep: var(--background);\n  --foreground: 0 0% 95%;\n  --card: 24 9.8% 6%;\n  --card-foreground: 0 0% 95%;\n  --popover: 0 0% 9%;\n  --popover-foreground: 0 0% 95%;\n  --primary-foreground: 210 20% 98%;\n  --secondary: 240 3.7% 15.9%;\n  --secondary-foreground: 0 0% 98%;\n  --muted: 0 0% 15%;\n  --muted-foreground: 240 5% 64.9%;\n  --accent: 12 6.5% 15.1%;\n  --accent-foreground: 0 0% 98%;\n  --destructive: 359.21 68.47% 56.47%;\n  --destructive-foreground: 0 85.7% 97.3%;\n  --border: 240 3.7% 15.9%;\n  --input: 240 3.7% 15.9%;\n  --ring: 142.4 71.8% 29.2%;\n  --sidebar: 20 14.3% 4.1%;\n  --sidebar-deep: 20 14.3% 4.1%;\n  --header: 20 14.3% 4.1%;\n}\n\n.dark[data-theme='deep-green'],\n[data-theme='deep-green'] .dark {\n  --background: 20 14.3% 4.1%;\n  --background-deep: var(--background);\n  --foreground: 0 0% 95%;\n  --card: 24 9.8% 6%;\n  --card-foreground: 0 0% 95%;\n  --popover: 0 0% 9%;\n  --popover-foreground: 0 0% 95%;\n  --primary-foreground: 210 20% 98%;\n  --secondary: 240 3.7% 15.9%;\n  --secondary-foreground: 0 0% 98%;\n  --muted: 0 0% 15%;\n  --muted-foreground: 240 5% 64.9%;\n  --accent: 12 6.5% 15.1%;\n  --accent-foreground: 0 0% 98%;\n  --destructive: 359.21 68.47% 56.47%;\n  --destructive-foreground: 0 85.7% 97.3%;\n  --border: 240 3.7% 15.9%;\n  --input: 240 3.7% 15.9%;\n  --ring: 142.4 71.8% 29.2%;\n  --sidebar: 20 14.3% 4.1%;\n  --sidebar-deep: 20 14.3% 4.1%;\n  --header: 20 14.3% 4.1%;\n}\n\n.dark[data-theme='orange'],\n[data-theme='orange'] .dark {\n  --background: 20 14.3% 4.1%;\n  --background-deep: var(--background);\n  --foreground: 60 9.1% 97.8%;\n  --card: 20 14.3% 4.1%;\n  --card-foreground: 60 9.1% 97.8%;\n  --popover: 20 14.3% 4.1%;\n  --popover-foreground: 60 9.1% 97.8%;\n  --primary-foreground: 60 9.1% 97.8%;\n  --secondary: 12 6.5% 15.1%;\n  --secondary-foreground: 60 9.1% 97.8%;\n  --muted: 12 6.5% 15.1%;\n  --muted-foreground: 24 5.4% 63.9%;\n  --accent: 12 6.5% 15.1%;\n  --accent-foreground: 60 9.1% 97.8%;\n  --destructive: 0 72.2% 50.6%;\n  --destructive-foreground: 60 9.1% 97.8%;\n  --border: 12 6.5% 15.1%;\n  --input: 12 6.5% 15.1%;\n  --ring: 20.5 90.2% 48.2%;\n  --sidebar: 20 14.3% 4.1%;\n  --sidebar-deep: 20 14.3% 4.1%;\n  --header: 20 14.3% 4.1%;\n}\n\n.dark[data-theme='yellow'],\n[data-theme='yellow'] .dark {\n  --background: 20 14.3% 4.1%;\n  --background-deep: var(--background);\n  --foreground: 60 9.1% 97.8%;\n  --card: 20 14.3% 4.1%;\n  --card-foreground: 60 9.1% 97.8%;\n  --popover: 20 14.3% 4.1%;\n  --popover-foreground: 60 9.1% 97.8%;\n  --primary-foreground: 26 83.3% 14.1%;\n  --secondary: 12 6.5% 15.1%;\n  --secondary-foreground: 60 9.1% 97.8%;\n  --muted: 12 6.5% 15.1%;\n  --muted-foreground: 24 5.4% 63.9%;\n  --accent: 12 6.5% 15.1%;\n  --accent-foreground: 60 9.1% 97.8%;\n  --destructive: 359.21 68.47% 56.47%;\n  --destructive-foreground: 60 9.1% 97.8%;\n  --border: 12 6.5% 15.1%;\n  --input: 12 6.5% 15.1%;\n  --ring: 35.5 91.7% 32.9%;\n  --sidebar: 20 14.3% 4.1%;\n  --sidebar-deep: 20 14.3% 4.1%;\n  --header: 20 14.3% 4.1%;\n}\n\n.dark[data-theme='zinc'],\n[data-theme='zinc'] .dark {\n  --background: 240 10% 3.9%;\n  --background-deep: var(--background);\n  --foreground: 0 0% 98%;\n  --card: 240 10% 3.9%;\n  --card-foreground: 0 0% 98%;\n  --popover: 240 10% 3.9%;\n  --popover-foreground: 0 0% 98%;\n  --primary-foreground: 240 5.9% 10%;\n  --secondary: 240 3.7% 15.9%;\n  --secondary-foreground: 0 0% 98%;\n  --muted: 240 3.7% 15.9%;\n  --muted-foreground: 240 5% 64.9%;\n  --accent: 240 3.7% 15.9%;\n  --accent-foreground: 0 0% 98%;\n  --destructive: 359.21 68.47% 56.47%;\n  --destructive-foreground: 0 0% 98%;\n  --border: 240 3.7% 15.9%;\n  --input: 240 3.7% 15.9%;\n  --ring: 240 4.9% 83.9%;\n  --sidebar: 240 10% 3.9%;\n  --sidebar-deep: 240 10% 3.9%;\n  --header: 240 10% 3.9%;\n}\n\n.dark[data-theme='neutral'],\n[data-theme='neutral'] .dark {\n  --background: 0 0% 3.9%;\n  --background-deep: var(--background);\n  --foreground: 0 0% 98%;\n  --card: 0 0% 3.9%;\n  --card-foreground: 0 0% 98%;\n  --popover: 0 0% 3.9%;\n  --popover-foreground: 0 0% 98%;\n  --primary-foreground: 0 0% 9%;\n  --secondary: 0 0% 14.9%;\n  --secondary-foreground: 0 0% 98%;\n  --muted: 0 0% 14.9%;\n  --muted-foreground: 0 0% 63.9%;\n  --accent: 0 0% 14.9%;\n  --accent-foreground: 0 0% 98%;\n  --destructive: 359.21 68.47% 56.47%;\n  --destructive-foreground: 0 0% 98%;\n  --border: 0 0% 14.9%;\n  --input: 0 0% 14.9%;\n  --ring: 0 0% 83.1%;\n  --sidebar: 0 0% 3.9%;\n  --sidebar-deep: 0 0% 3.9%;\n  --header: 0 0% 3.9%;\n}\n\n.dark[data-theme='slate'],\n[data-theme='slate'] .dark {\n  --background: 222.2 84% 4.9%;\n  --background-deep: var(--background);\n  --foreground: 210 40% 98%;\n  --card: 222.2 84% 4.9%;\n  --card-foreground: 210 40% 98%;\n  --popover: 222.2 84% 4.9%;\n  --popover-foreground: 210 40% 98%;\n  --primary-foreground: 222.2 47.4% 11.2%;\n  --secondary: 217.2 32.6% 17.5%;\n  --secondary-foreground: 210 40% 98%;\n  --muted: 217.2 32.6% 17.5%;\n  --muted-foreground: 215 20.2% 65.1%;\n  --accent: 217.2 32.6% 17.5%;\n  --accent-foreground: 210 40% 98%;\n  --destructive: 359.21 68.47% 56.47%;\n  --destructive-foreground: 210 40% 98%;\n  --border: 217.2 32.6% 17.5%;\n  --input: 217.2 32.6% 17.5%;\n  --ring: 212.7 26.8% 83.9;\n  --sidebar: 222.2 84% 4.9%;\n  --sidebar-deep: 222.2 84% 4.9%;\n  --header: 222.2 84% 4.9%;\n}\n\n.dark[data-theme='gray'],\n[data-theme='gray'] .dark {\n  --background: 224 71.4% 4.1%;\n  --background-deep: var(--background);\n  --foreground: 210 20% 98%;\n  --card: 224 71.4% 4.1%;\n  --card-foreground: 210 20% 98%;\n  --popover: 224 71.4% 4.1%;\n  --popover-foreground: 210 20% 98%;\n  --primary-foreground: 220.9 39.3% 11%;\n  --secondary: 215 27.9% 16.9%;\n  --secondary-foreground: 210 20% 98%;\n  --muted: 215 27.9% 16.9%;\n  --muted-foreground: 217.9 10.6% 64.9%;\n  --accent: 215 27.9% 16.9%;\n  --accent-foreground: 210 20% 98%;\n  --destructive: 359.21 68.47% 56.47%;\n  --destructive-foreground: 210 20% 98%;\n  --border: 215 27.9% 16.9%;\n  --input: 215 27.9% 16.9%;\n  --ring: 216 12.2% 83.9%;\n  --sidebar: 224 71.4% 4.1%;\n  --sidebar-deep: 224 71.4% 4.1%;\n  --header: 224 71.4% 4.1%;\n}\n"
  },
  {
    "path": "packages/@core/base/design/src/design-tokens/default.css",
    "content": ":root {\n  /** 弹出层的基础层级 **/\n  --popup-z-index: 2000;\n  --font-family:\n    -apple-system, blinkmacsystemfont, 'Segoe UI', roboto, 'Helvetica Neue',\n    arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',\n    'Segoe UI Symbol', 'Noto Color Emoji';\n\n  /* Default background color of <body />...etc */\n  --background: 0 0% 100%;\n\n  /* 主体区域背景色 */\n  --background-deep: 216 20.11% 95.47%;\n  --foreground: 210 6% 21%;\n\n  /* Background color for <Card /> */\n  --card: 0 0% 100%;\n  --card-foreground: 222.2 84% 4.9%;\n\n  /* Background color for popovers such as <DropdownMenu />, <HoverCard />, <Popover /> */\n  --popover: 0 0% 100%;\n  --popover-foreground: 222.2 84% 4.9%;\n\n  /* Muted backgrounds such as <TabsList />, <Skeleton /> and <Switch /> */\n\n  /* --muted: 210 40% 96.1%;\n  --muted-foreground: 215.4 16.3% 46.9%; */\n\n  --muted: 240 4.8% 95.9%;\n  --muted-foreground: 240 3.8% 46.1%;\n\n  /* 主题颜色 */\n\n  --primary: 215 100% 54%;\n  --primary-foreground: 0 0% 98%;\n\n  /* Used for destructive actions such as <Button variant=\"destructive\"> */\n\n  --destructive: 359.33 100% 65.1%;\n  --destructive-foreground: 0 0% 98%;\n\n  /* Used for success actions such as <message> */\n\n  --info: 240, 5%, 96%;\n  --info-foreground: 220, 4%, 58%;\n\n  /* Used for success actions such as <message> */\n\n  --success: 144 57% 58%;\n  --success-foreground: 0 0% 98%;\n\n  /* Used for warning actions such as <message> */\n\n  --warning: 42 84% 61%;\n  --warning-foreground: 0 0% 98%;\n\n  /* Secondary colors for <Button /> */\n\n  --secondary: 240 5% 96%;\n  --secondary-foreground: 240 6% 10%;\n\n  /* Used for accents such as hover effects on <DropdownMenuItem>, <SelectItem>...etc */\n  --accent: 240 5% 96%;\n  --accent-dark: 216 14% 93%;\n  --accent-darker: 216 11% 91%;\n  --accent-lighter: 240 0% 98%;\n  --accent-hover: 200deg 10% 90%;\n  --accent-foreground: 240 6% 10%;\n\n  /* Darker color */\n  --heavy: 192deg 9.43% 89.61%;\n  --heavy-foreground: var(--accent-foreground);\n\n  /* Default border color */\n  --border: 240 5.9% 90%;\n\n  /* Border color for inputs such as <Input />, <Select />, <Textarea /> */\n  --input: 240deg 5.88% 90%;\n  --input-placeholder: 217 10.6% 65%;\n  --input-background: 0 0% 100%;\n\n  /* Used for focus ring */\n  --ring: 222.2 84% 4.9%;\n\n  /* Border radius for card, input and buttons */\n  --radius: 0.5rem;\n\n  /* ============= custom ============= */\n\n  /* 遮罩颜色 */\n  --overlay: 0 0% 0% / 45%;\n  --overlay-content: 0 0% 95% / 45%;\n\n  /* 基本文字大小 */\n  --font-size-base: 16px;\n\n  /* =============component & UI============= */\n\n  /* menu */\n  --sidebar: 0 0% 100%;\n  --sidebar-deep: 0 0% 100%;\n  --menu: var(--sidebar);\n\n  /* header */\n  --header: 0 0% 100%;\n\n  accent-color: var(--primary);\n  color-scheme: light;\n}\n\n[data-theme='violet'] {\n  /* --background: 0 0% 100%; */\n  --foreground: 224 71.4% 4.1%;\n  --card: 0 0% 100%;\n  --card-foreground: 224 71.4% 4.1%;\n  --popover: 0 0% 100%;\n  --popover-foreground: 224 71.4% 4.1%;\n  --primary-foreground: 210 20% 98%;\n  --secondary: 220 14.3% 95.9%;\n  --secondary-foreground: 220.9 39.3% 11%;\n  --muted: 220 14.3% 95.9%;\n  --muted-foreground: 220 8.9% 46.1%;\n  --accent: 220 14.3% 95.9%;\n  --accent-foreground: 220.9 39.3% 11%;\n  --destructive: 0 84.2% 60.2%;\n  --destructive-foreground: 210 20% 98%;\n  --border: 220 13% 91%;\n  --input: 220 13% 91%;\n  --ring: 262.1 83.3% 57.8%;\n}\n\n[data-theme='pink'] {\n  /* --background: 0 0% 100%; */\n  --foreground: 240 10% 3.9%;\n  --card: 0 0% 100%;\n  --card-foreground: 240 10% 3.9%;\n  --popover: 0 0% 100%;\n  --popover-foreground: 240 10% 3.9%;\n  --primary-foreground: 355.7 100% 97.3%;\n  --secondary: 240 4.8% 95.9%;\n  --secondary-foreground: 240 5.9% 10%;\n  --muted: 240 4.8% 95.9%;\n  --muted-foreground: 240 3.8% 46.1%;\n  --accent: 240 4.8% 95.9%;\n  --accent-foreground: 240 5.9% 10%;\n  --destructive: 0 84.2% 60.2%;\n  --destructive-foreground: 0 0% 98%;\n  --border: 240 5.9% 90%;\n  --input: 240 5.9% 90%;\n  --ring: 346.8 77.2% 49.8%;\n}\n\n[data-theme='rose'] {\n  /* --background: 0 0% 100%; */\n  --foreground: 240 10% 3.9%;\n  --card: 0 0% 100%;\n  --card-foreground: 240 10% 3.9%;\n  --popover: 0 0% 100%;\n  --popover-foreground: 240 10% 3.9%;\n  --primary-foreground: 355.7 100% 97.3%;\n  --secondary: 240 4.8% 95.9%;\n  --secondary-foreground: 240 5.9% 10%;\n  --muted: 240 4.8% 95.9%;\n  --muted-foreground: 240 3.8% 46.1%;\n  --accent: 240 4.8% 95.9%;\n  --accent-foreground: 240 5.9% 10%;\n  --destructive: 0 84.2% 60.2%;\n  --destructive-foreground: 0 0% 98%;\n  --border: 240 5.9% 90%;\n  --input: 240 5.9% 90%;\n  --ring: 346.8 77.2% 49.8%;\n}\n\n[data-theme='sky-blue'] {\n  /* --background: 0 0% 100%; */\n  --foreground: 222.2 84% 4.9%;\n  --card: 0 0% 100%;\n  --card-foreground: 222.2 84% 4.9%;\n  --popover: 0 0% 100%;\n  --popover-foreground: 222.2 84% 4.9%;\n  --primary-foreground: 210 40% 98%;\n  --secondary: 210 40% 96.1%;\n  --secondary-foreground: 222.2 47.4% 11.2%;\n  --muted: 210 40% 96.1%;\n  --muted-foreground: 215.4 16.3% 46.9%;\n  --accent: 210 40% 96.1%;\n  --accent-foreground: 222.2 47.4% 11.2%;\n  --destructive: 0 84.2% 60.2%;\n  --destructive-foreground: 210 40% 98%;\n  --border: 214.3 31.8% 91.4%;\n  --input: 214.3 31.8% 91.4%;\n  --ring: 221.2 83.2% 53.3%;\n}\n\n[data-theme='deep-blue'] {\n  /* --background: 0 0% 100%; */\n  --foreground: 222.2 84% 4.9%;\n  --card: 0 0% 100%;\n  --card-foreground: 222.2 84% 4.9%;\n  --popover: 0 0% 100%;\n  --popover-foreground: 222.2 84% 4.9%;\n  --primary-foreground: 210 40% 98%;\n  --secondary: 210 40% 96.1%;\n  --secondary-foreground: 222.2 47.4% 11.2%;\n  --muted: 210 40% 96.1%;\n  --muted-foreground: 215.4 16.3% 46.9%;\n  --accent: 210 40% 96.1%;\n  --accent-foreground: 222.2 47.4% 11.2%;\n  --destructive: 0 84.2% 60.2%;\n  --destructive-foreground: 210 40% 98%;\n  --border: 214.3 31.8% 91.4%;\n  --input: 214.3 31.8% 91.4%;\n  --ring: 221.2 83.2% 53.3%;\n}\n\n[data-theme='green'] {\n  /* --background: 0 0% 100%; */\n  --foreground: 240 10% 3.9%;\n  --card: 0 0% 100%;\n  --card-foreground: 240 10% 3.9%;\n  --popover: 0 0% 100%;\n  --popover-foreground: 240 10% 3.9%;\n  --primary-foreground: 355.7 100% 97.3%;\n  --secondary: 240 4.8% 95.9%;\n  --secondary-foreground: 240 5.9% 10%;\n  --muted: 240 4.8% 95.9%;\n  --muted-foreground: 240 3.8% 46.1%;\n  --accent: 240 4.8% 95.9%;\n  --accent-foreground: 240 5.9% 10%;\n  --destructive: 0 84.2% 60.2%;\n  --destructive-foreground: 0 0% 98%;\n  --border: 240 5.9% 90%;\n  --input: 240 5.9% 90%;\n  --ring: 142.1 76.2% 36.3%;\n}\n\n[data-theme='deep-green'] {\n  /* --background: 0 0% 100%; */\n  --foreground: 240 10% 3.9%;\n  --card: 0 0% 100%;\n  --card-foreground: 240 10% 3.9%;\n  --popover: 0 0% 100%;\n  --popover-foreground: 240 10% 3.9%;\n  --primary-foreground: 355.7 100% 97.3%;\n  --secondary: 240 4.8% 95.9%;\n  --secondary-foreground: 240 5.9% 10%;\n  --muted: 240 4.8% 95.9%;\n  --muted-foreground: 240 3.8% 46.1%;\n  --accent: 240 4.8% 95.9%;\n  --accent-foreground: 240 5.9% 10%;\n  --destructive: 0 84.2% 60.2%;\n  --destructive-foreground: 0 0% 98%;\n  --border: 240 5.9% 90%;\n  --input: 240 5.9% 90%;\n  --ring: 142.1 76.2% 36.3%;\n}\n\n[data-theme='orange'] {\n  /* --background: 0 0% 100%; */\n  --foreground: 20 14.3% 4.1%;\n  --card: 0 0% 100%;\n  --card-foreground: 20 14.3% 4.1%;\n  --popover: 0 0% 100%;\n  --popover-foreground: 20 14.3% 4.1%;\n  --primary-foreground: 60 9.1% 97.8%;\n  --secondary: 60 4.8% 95.9%;\n  --secondary-foreground: 24 9.8% 10%;\n  --muted: 60 4.8% 95.9%;\n  --muted-foreground: 25 5.3% 44.7%;\n  --accent: 60 4.8% 95.9%;\n  --accent-foreground: 24 9.8% 10%;\n  --destructive: 0 84.2% 60.2%;\n  --destructive-foreground: 60 9.1% 97.8%;\n  --border: 20 5.9% 90%;\n  --input: 20 5.9% 90%;\n  --ring: 24.6 95% 53.1%;\n}\n\n[data-theme='yellow'] {\n  /* --background: 0 0% 100%; */\n  --foreground: 20 14.3% 4.1%;\n  --card: 0 0% 100%;\n  --card-foreground: 20 14.3% 4.1%;\n  --popover: 0 0% 100%;\n  --popover-foreground: 20 14.3% 4.1%;\n  --primary-foreground: 26 83.3% 14.1%;\n  --secondary: 60 4.8% 95.9%;\n  --secondary-foreground: 24 9.8% 10%;\n  --muted: 60 4.8% 95.9%;\n  --muted-foreground: 25 5.3% 44.7%;\n  --accent: 60 4.8% 95.9%;\n  --accent-foreground: 24 9.8% 10%;\n  --destructive: 0 84.2% 60.2%;\n  --destructive-foreground: 60 9.1% 97.8%;\n  --border: 20 5.9% 90%;\n  --input: 20 5.9% 90%;\n  --ring: 20 14.3% 4.1%;\n}\n\n[data-theme='zinc'] {\n  /* --background: 0 0% 100%; */\n  --foreground: 240 10% 3.9%;\n  --card: 0 0% 100%;\n  --card-foreground: 240 10% 3.9%;\n  --popover: 0 0% 100%;\n  --popover-foreground: 240 10% 3.9%;\n  --primary-foreground: 0 0% 98%;\n  --secondary: 240 4.8% 95.9%;\n  --secondary-foreground: 240 5.9% 10%;\n  --muted: 240 4.8% 95.9%;\n  --muted-foreground: 240 3.8% 46.1%;\n  --accent: 240 4.8% 95.9%;\n  --accent-foreground: 240 5.9% 10%;\n  --destructive: 0 84.2% 60.2%;\n  --destructive-foreground: 0 0% 98%;\n  --border: 240 5.9% 90%;\n  --input: 240 5.9% 90%;\n  --ring: 240 5.9% 10%;\n}\n\n[data-theme='neutral'] {\n  /* --background: 0 0% 100%; */\n  --foreground: 0 0% 3.9%;\n  --card: 0 0% 100%;\n  --card-foreground: 0 0% 3.9%;\n  --popover: 0 0% 100%;\n  --popover-foreground: 0 0% 3.9%;\n  --primary-foreground: 0 0% 98%;\n  --secondary: 0 0% 96.1%;\n  --secondary-foreground: 0 0% 9%;\n  --muted: 0 0% 96.1%;\n  --muted-foreground: 0 0% 45.1%;\n  --accent: 0 0% 96.1%;\n  --accent-foreground: 0 0% 9%;\n  --destructive: 0 84.2% 60.2%;\n  --destructive-foreground: 0 0% 98%;\n  --border: 0 0% 89.8%;\n  --input: 0 0% 89.8%;\n  --ring: 0 0% 3.9%;\n}\n\n[data-theme='slate'] {\n  /* --background: 0 0% 100%; */\n  --foreground: 222.2 84% 4.9%;\n  --card: 0 0% 100%;\n  --card-foreground: 222.2 84% 4.9%;\n  --popover: 0 0% 100%;\n  --popover-foreground: 222.2 84% 4.9%;\n  --primary-foreground: 210 40% 98%;\n  --secondary: 210 40% 96.1%;\n  --secondary-foreground: 222.2 47.4% 11.2%;\n  --muted: 210 40% 96.1%;\n  --muted-foreground: 215.4 16.3% 46.9%;\n  --accent: 210 40% 96.1%;\n  --accent-foreground: 222.2 47.4% 11.2%;\n  --destructive: 0 84.2% 60.2%;\n  --destructive-foreground: 210 40% 98%;\n  --border: 214.3 31.8% 91.4%;\n  --input: 214.3 31.8% 91.4%;\n  --ring: 222.2 84% 4.9%;\n}\n\n[data-theme='gray'] {\n  /* --background: 0 0% 100%; */\n  --foreground: 224 71.4% 4.1%;\n  --card: 0 0% 100%;\n  --card-foreground: 224 71.4% 4.1%;\n  --popover: 0 0% 100%;\n  --popover-foreground: 224 71.4% 4.1%;\n  --primary-foreground: 210 20% 98%;\n  --secondary: 220 14.3% 95.9%;\n  --secondary-foreground: 220.9 39.3% 11%;\n  --muted: 220 14.3% 95.9%;\n  --muted-foreground: 220 8.9% 46.1%;\n  --accent: 220 14.3% 95.9%;\n  --accent-foreground: 220.9 39.3% 11%;\n  --destructive: 0 84.2% 60.2%;\n  --destructive-foreground: 210 20% 98%;\n  --border: 220 13% 91%;\n  --input: 220 13% 91%;\n  --ring: 224 71.4% 4.1%;\n}\n"
  },
  {
    "path": "packages/@core/base/design/src/design-tokens/index.ts",
    "content": "import './default.css';\nimport './dark.css';\n\nexport {};\n"
  },
  {
    "path": "packages/@core/base/design/src/index.ts",
    "content": "import './design-tokens';\n\nimport './css/global.css';\nimport './css/transition.css';\nimport './css/nprogress.css';\nimport './css/ui.css';\n\nexport {};\n"
  },
  {
    "path": "packages/@core/base/design/src/scss-bem/bem.scss",
    "content": "@forward './constants';\n\n@mixin b($block) {\n  $B: $namespace + '-' + $block !global;\n\n  .#{$B} {\n    @content;\n  }\n}\n\n@mixin e($name) {\n  @at-root {\n    &#{$element-separator}#{$name} {\n      @content;\n    }\n  }\n}\n\n@mixin m($name) {\n  @at-root {\n    &#{$modifier-separator}#{$name} {\n      @content;\n    }\n  }\n}\n\n// block__element.is-active {}\n@mixin is($state, $prefix: $state-prefix) {\n  @at-root {\n    &.#{$prefix}-#{$state} {\n      @content;\n    }\n  }\n}\n"
  },
  {
    "path": "packages/@core/base/design/src/scss-bem/constants.scss",
    "content": "$namespace: 'vben' !default;\n$common-separator: '-' !default;\n$element-separator: '__' !default;\n$modifier-separator: '--' !default;\n$state-prefix: 'is' !default;\n"
  },
  {
    "path": "packages/@core/base/design/tsconfig.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/tsconfig\",\n  \"extends\": \"@vben/tsconfig/web.json\",\n  \"include\": [\"src\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "packages/@core/base/design/vite.config.mts",
    "content": "import { defineConfig } from '@vben/vite-config';\n\nexport default defineConfig(async () => {\n  return {\n    vite: {\n      publicDir: 'src/scss-bem',\n    },\n  };\n});\n"
  },
  {
    "path": "packages/@core/base/icons/build.config.ts",
    "content": "import { defineBuildConfig } from 'unbuild';\n\nexport default defineBuildConfig({\n  clean: true,\n  declaration: true,\n  entries: ['src/index'],\n});\n"
  },
  {
    "path": "packages/@core/base/icons/package.json",
    "content": "{\n  \"name\": \"@vben-core/icons\",\n  \"version\": \"5.5.9\",\n  \"homepage\": \"https://github.com/vbenjs/vue-vben-admin\",\n  \"bugs\": \"https://github.com/vbenjs/vue-vben-admin/issues\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/vbenjs/vue-vben-admin.git\",\n    \"directory\": \"packages/@vben-core/base/icons\"\n  },\n  \"license\": \"MIT\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"build\": \"pnpm unbuild\"\n  },\n  \"files\": [\n    \"dist\"\n  ],\n  \"main\": \"./dist/index.mjs\",\n  \"module\": \"./dist/index.mjs\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./src/index.ts\",\n      \"development\": \"./src/index.ts\",\n      \"default\": \"./dist/index.mjs\"\n    }\n  },\n  \"publishConfig\": {\n    \"exports\": {\n      \".\": {\n        \"types\": \"./dist/index.d.ts\",\n        \"default\": \"./dist/index.mjs\"\n      }\n    }\n  },\n  \"dependencies\": {\n    \"@iconify/vue\": \"catalog:\",\n    \"lucide-vue-next\": \"catalog:\",\n    \"vue\": \"catalog:\"\n  }\n}\n"
  },
  {
    "path": "packages/@core/base/icons/src/create-icon.ts",
    "content": "import { defineComponent, h } from 'vue';\n\nimport { addIcon, Icon, type IconifyIcon } from '@iconify/vue';\n\nfunction createIconifyIcon(icon: string) {\n  return defineComponent({\n    name: `Icon-${icon}`,\n    setup(props, { attrs }) {\n      return () => h(Icon, { icon, ...props, ...attrs });\n    },\n  });\n}\n\n/**\n * 创建离线图标\n * @param icon 图标名称 建议与iconify的名称保持一致\n * @param iconComponent 从@iconify/icon-xxx/xxx导入的图标\n * @returns IconComponent\n */\nfunction createIconifyOfflineIcon(icon: string, iconComponent: IconifyIcon) {\n  return defineComponent({\n    name: `Icon-${icon}`,\n    setup(props, { attrs }) {\n      addIcon(icon, iconComponent);\n      return () => h(Icon, { icon, ...props, ...attrs });\n    },\n  });\n}\n\nexport { createIconifyIcon, createIconifyOfflineIcon };\n"
  },
  {
    "path": "packages/@core/base/icons/src/index.ts",
    "content": "export * from './create-icon';\n\nexport * from './lucide';\n\nexport type { IconifyIcon as IconifyIconStructure } from '@iconify/vue';\nexport {\n  addCollection,\n  addIcon,\n  Icon as IconifyIcon,\n  listIcons,\n} from '@iconify/vue';\n\n/**\n * 从@iconify/vue/dist/offline'导出的组件为离线ICON 不支持在线\n * 从@iconify/vue'导出的组件为在能找到本地图标为离线 否则会在线获取(适用性更强)\n */\n"
  },
  {
    "path": "packages/@core/base/icons/src/lucide.ts",
    "content": "export {\n  ArrowDown,\n  ArrowLeft,\n  ArrowLeftToLine,\n  ArrowRightLeft,\n  ArrowRightToLine,\n  ArrowUp,\n  ArrowUpToLine,\n  Bell,\n  BookOpenText,\n  Check,\n  ChevronDown,\n  ChevronLeft,\n  ChevronRight,\n  ChevronsLeft,\n  ChevronsRight,\n  Circle,\n  CircleAlert,\n  CircleCheckBig,\n  CircleHelp,\n  CircleX,\n  Copy,\n  CornerDownLeft,\n  Ellipsis,\n  Expand,\n  ExternalLink,\n  Eye,\n  EyeOff,\n  FoldHorizontal,\n  Fullscreen,\n  Github,\n  Grip,\n  GripVertical,\n  Menu as IconDefault,\n  Inbox,\n  Info,\n  InspectionPanel,\n  Languages,\n  LoaderCircle,\n  LockKeyhole,\n  LogOut,\n  MailCheck,\n  Maximize,\n  ArrowRightFromLine as MdiMenuClose,\n  ArrowLeftFromLine as MdiMenuOpen,\n  Menu,\n  Minimize,\n  Minimize2,\n  MoonStar,\n  Palette,\n  PanelLeft,\n  PanelRight,\n  Pin,\n  PinOff,\n  Plus,\n  RotateCw,\n  Search,\n  SearchX,\n  Settings,\n  Shrink,\n  Square,\n  SquareCheckBig,\n  SquareMinus,\n  Sun,\n  SunMoon,\n  SwatchBook,\n  UserRoundPen,\n  X,\n} from 'lucide-vue-next';\n"
  },
  {
    "path": "packages/@core/base/icons/tsconfig.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/tsconfig\",\n  \"extends\": \"@vben/tsconfig/web.json\",\n  \"include\": [\"src\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "packages/@core/base/shared/build.config.ts",
    "content": "import { defineBuildConfig } from 'unbuild';\n\nexport default defineBuildConfig({\n  clean: true,\n  declaration: true,\n  entries: [\n    'src/store',\n    'src/constants/index',\n    'src/utils/index',\n    'src/color/index',\n    'src/cache/index',\n    'src/global-state',\n  ],\n});\n"
  },
  {
    "path": "packages/@core/base/shared/package.json",
    "content": "{\n  \"name\": \"@vben-core/shared\",\n  \"version\": \"5.5.9\",\n  \"homepage\": \"https://github.com/vbenjs/vue-vben-admin\",\n  \"bugs\": \"https://github.com/vbenjs/vue-vben-admin/issues\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/vbenjs/vue-vben-admin.git\",\n    \"directory\": \"packages/@vben-core/base/shared\"\n  },\n  \"license\": \"MIT\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"build\": \"pnpm unbuild\",\n    \"stub\": \"pnpm unbuild --stub\"\n  },\n  \"files\": [\n    \"dist\"\n  ],\n  \"sideEffects\": false,\n  \"exports\": {\n    \"./constants\": {\n      \"types\": \"./src/constants/index.ts\",\n      \"development\": \"./src/constants/index.ts\",\n      \"default\": \"./dist/constants/index.mjs\"\n    },\n    \"./utils\": {\n      \"types\": \"./src/utils/index.ts\",\n      \"development\": \"./src/utils/index.ts\",\n      \"default\": \"./dist/utils/index.mjs\"\n    },\n    \"./color\": {\n      \"types\": \"./src/color/index.ts\",\n      \"development\": \"./src/color/index.ts\",\n      \"default\": \"./dist/color/index.mjs\"\n    },\n    \"./cache\": {\n      \"types\": \"./src/cache/index.ts\",\n      \"development\": \"./src/cache/index.ts\",\n      \"default\": \"./dist/cache/index.mjs\"\n    },\n    \"./store\": {\n      \"types\": \"./src/store.ts\",\n      \"development\": \"./src/store.ts\",\n      \"default\": \"./dist/store.mjs\"\n    },\n    \"./global-state\": {\n      \"types\": \"./src/global-state.ts\",\n      \"development\": \"./src/global-state.ts\",\n      \"default\": \"./dist/global-state.mjs\"\n    }\n  },\n  \"publishConfig\": {\n    \"exports\": {\n      \"./constants\": {\n        \"types\": \"./dist/constants/index.d.ts\",\n        \"default\": \"./dist/constants/index.mjs\"\n      },\n      \"./utils\": {\n        \"types\": \"./dist/utils/index.d.ts\",\n        \"default\": \"./dist/utils/index.mjs\"\n      },\n      \"./color\": {\n        \"types\": \"./dist/color/index.d.ts\",\n        \"default\": \"./dist/color/index.mjs\"\n      },\n      \"./cache\": {\n        \"types\": \"./dist/cache/index.d.ts\",\n        \"default\": \"./dist/cache/index.mjs\"\n      },\n      \"./store\": {\n        \"types\": \"./dist/store.d.ts\",\n        \"default\": \"./dist/store.mjs\"\n      },\n      \"./global-state\": {\n        \"types\": \"./dist/global-state.d.ts\",\n        \"default\": \"./dist/global-state.mjs\"\n      }\n    }\n  },\n  \"dependencies\": {\n    \"@ctrl/tinycolor\": \"catalog:\",\n    \"@tanstack/vue-store\": \"catalog:\",\n    \"@vue/shared\": \"catalog:\",\n    \"clsx\": \"catalog:\",\n    \"dayjs\": \"catalog:\",\n    \"defu\": \"catalog:\",\n    \"lodash.clonedeep\": \"catalog:\",\n    \"lodash.get\": \"catalog:\",\n    \"lodash.isequal\": \"catalog:\",\n    \"lodash.set\": \"catalog:\",\n    \"nprogress\": \"catalog:\",\n    \"tailwind-merge\": \"catalog:\",\n    \"theme-colors\": \"catalog:\"\n  },\n  \"devDependencies\": {\n    \"@types/lodash.clonedeep\": \"catalog:\",\n    \"@types/lodash.get\": \"catalog:\",\n    \"@types/lodash.isequal\": \"catalog:\",\n    \"@types/lodash.set\": \"catalog:\",\n    \"@types/nprogress\": \"catalog:\"\n  }\n}\n"
  },
  {
    "path": "packages/@core/base/shared/src/cache/__tests__/storage-manager.test.ts",
    "content": "import { beforeEach, describe, expect, it, vi } from 'vitest';\n\nimport { StorageManager } from '../storage-manager';\n\ndescribe('storageManager', () => {\n  let storageManager: StorageManager;\n\n  beforeEach(() => {\n    vi.useFakeTimers();\n    localStorage.clear();\n    storageManager = new StorageManager({\n      prefix: 'test_',\n    });\n  });\n\n  it('should set and get an item', () => {\n    storageManager.setItem('user', { age: 30, name: 'John Doe' });\n    const user = storageManager.getItem('user');\n    expect(user).toEqual({ age: 30, name: 'John Doe' });\n  });\n\n  it('should return default value if item does not exist', () => {\n    const user = storageManager.getItem('nonexistent', {\n      age: 0,\n      name: 'Default User',\n    });\n    expect(user).toEqual({ age: 0, name: 'Default User' });\n  });\n\n  it('should remove an item', () => {\n    storageManager.setItem('user', { age: 30, name: 'John Doe' });\n    storageManager.removeItem('user');\n    const user = storageManager.getItem('user');\n    expect(user).toBeNull();\n  });\n\n  it('should clear all items with the prefix', () => {\n    storageManager.setItem('user1', { age: 30, name: 'John Doe' });\n    storageManager.setItem('user2', { age: 25, name: 'Jane Doe' });\n    storageManager.clear();\n    expect(storageManager.getItem('user1')).toBeNull();\n    expect(storageManager.getItem('user2')).toBeNull();\n  });\n\n  it('should clear expired items', () => {\n    storageManager.setItem('user', { age: 30, name: 'John Doe' }, 1000); // 1秒过期\n    vi.advanceTimersByTime(1001); // 快进时间\n    storageManager.clearExpiredItems();\n    const user = storageManager.getItem('user');\n    expect(user).toBeNull();\n  });\n\n  it('should not clear non-expired items', () => {\n    storageManager.setItem('user', { age: 30, name: 'John Doe' }, 10_000); // 10秒过期\n    vi.advanceTimersByTime(5000); // 快进时间\n    storageManager.clearExpiredItems();\n    const user = storageManager.getItem('user');\n    expect(user).toEqual({ age: 30, name: 'John Doe' });\n  });\n\n  it('should handle JSON parse errors gracefully', () => {\n    localStorage.setItem('test_user', '{ invalid JSON }');\n    const user = storageManager.getItem('user', {\n      age: 0,\n      name: 'Default User',\n    });\n    expect(user).toEqual({ age: 0, name: 'Default User' });\n  });\n  it('should return null for non-existent items without default value', () => {\n    const user = storageManager.getItem('nonexistent');\n    expect(user).toBeNull();\n  });\n\n  it('should overwrite existing items', () => {\n    storageManager.setItem('user', { age: 30, name: 'John Doe' });\n    storageManager.setItem('user', { age: 25, name: 'Jane Doe' });\n    const user = storageManager.getItem('user');\n    expect(user).toEqual({ age: 25, name: 'Jane Doe' });\n  });\n\n  it('should handle items without expiry correctly', () => {\n    storageManager.setItem('user', { age: 30, name: 'John Doe' });\n    vi.advanceTimersByTime(5000);\n    const user = storageManager.getItem('user');\n    expect(user).toEqual({ age: 30, name: 'John Doe' });\n  });\n\n  it('should remove expired items when accessed', () => {\n    storageManager.setItem('user', { age: 30, name: 'John Doe' }, 1000); // 1秒过期\n    vi.advanceTimersByTime(1001); // 快进时间\n    const user = storageManager.getItem('user');\n    expect(user).toBeNull();\n  });\n\n  it('should not remove non-expired items when accessed', () => {\n    storageManager.setItem('user', { age: 30, name: 'John Doe' }, 10_000); // 10秒过期\n    vi.advanceTimersByTime(5000); // 快进时间\n    const user = storageManager.getItem('user');\n    expect(user).toEqual({ age: 30, name: 'John Doe' });\n  });\n\n  it('should handle multiple items with different expiry times', () => {\n    storageManager.setItem('user1', { age: 30, name: 'John Doe' }, 1000); // 1秒过期\n    storageManager.setItem('user2', { age: 25, name: 'Jane Doe' }, 2000); // 2秒过期\n    vi.advanceTimersByTime(1500); // 快进时间\n    storageManager.clearExpiredItems();\n    const user1 = storageManager.getItem('user1');\n    const user2 = storageManager.getItem('user2');\n    expect(user1).toBeNull();\n    expect(user2).toEqual({ age: 25, name: 'Jane Doe' });\n  });\n\n  it('should handle items with no expiry', () => {\n    storageManager.setItem('user', { age: 30, name: 'John Doe' });\n    vi.advanceTimersByTime(10_000); // 快进时间\n    storageManager.clearExpiredItems();\n    const user = storageManager.getItem('user');\n    expect(user).toEqual({ age: 30, name: 'John Doe' });\n  });\n\n  it('should clear all items correctly', () => {\n    storageManager.setItem('user1', { age: 30, name: 'John Doe' });\n    storageManager.setItem('user2', { age: 25, name: 'Jane Doe' });\n    storageManager.clear();\n    const user1 = storageManager.getItem('user1');\n    const user2 = storageManager.getItem('user2');\n    expect(user1).toBeNull();\n    expect(user2).toBeNull();\n  });\n});\n"
  },
  {
    "path": "packages/@core/base/shared/src/cache/index.ts",
    "content": "export * from './storage-manager';\n"
  },
  {
    "path": "packages/@core/base/shared/src/cache/storage-manager.ts",
    "content": "type StorageType = 'localStorage' | 'sessionStorage';\n\ninterface StorageManagerOptions {\n  prefix?: string;\n  storageType?: StorageType;\n}\n\ninterface StorageItem<T> {\n  expiry?: number;\n  value: T;\n}\n\nclass StorageManager {\n  private prefix: string;\n  private storage: Storage;\n\n  constructor({\n    prefix = '',\n    storageType = 'localStorage',\n  }: StorageManagerOptions = {}) {\n    this.prefix = prefix;\n    this.storage =\n      storageType === 'localStorage'\n        ? window.localStorage\n        : window.sessionStorage;\n  }\n\n  /**\n   * 获取完整的存储键\n   * @param key 原始键\n   * @returns 带前缀的完整键\n   */\n  private getFullKey(key: string): string {\n    return `${this.prefix}-${key}`;\n  }\n\n  /**\n   * 清除所有带前缀的存储项\n   */\n  clear(): void {\n    const keysToRemove: string[] = [];\n    for (let i = 0; i < this.storage.length; i++) {\n      const key = this.storage.key(i);\n      if (key && key.startsWith(this.prefix)) {\n        keysToRemove.push(key);\n      }\n    }\n    keysToRemove.forEach((key) => this.storage.removeItem(key));\n  }\n\n  /**\n   * 清除所有过期的存储项\n   */\n  clearExpiredItems(): void {\n    for (let i = 0; i < this.storage.length; i++) {\n      const key = this.storage.key(i);\n      if (key && key.startsWith(this.prefix)) {\n        const shortKey = key.replace(this.prefix, '');\n        this.getItem(shortKey); // 调用 getItem 方法检查并移除过期项\n      }\n    }\n  }\n\n  /**\n   * 获取存储项\n   * @param key 键\n   * @param defaultValue 当项不存在或已过期时返回的默认值\n   * @returns 值，如果项已过期或解析错误则返回默认值\n   */\n  getItem<T>(key: string, defaultValue: null | T = null): null | T {\n    const fullKey = this.getFullKey(key);\n    const itemStr = this.storage.getItem(fullKey);\n    if (!itemStr) {\n      return defaultValue;\n    }\n\n    try {\n      const item: StorageItem<T> = JSON.parse(itemStr);\n      if (item.expiry && Date.now() > item.expiry) {\n        this.storage.removeItem(fullKey);\n        return defaultValue;\n      }\n      return item.value;\n    } catch (error) {\n      console.error(`Error parsing item with key \"${fullKey}\":`, error);\n      this.storage.removeItem(fullKey); // 如果解析失败，删除该项\n      return defaultValue;\n    }\n  }\n\n  /**\n   * 移除存储项\n   * @param key 键\n   */\n  removeItem(key: string): void {\n    const fullKey = this.getFullKey(key);\n    this.storage.removeItem(fullKey);\n  }\n\n  /**\n   * 设置存储项\n   * @param key 键\n   * @param value 值\n   * @param ttl 存活时间（毫秒）\n   */\n  setItem<T>(key: string, value: T, ttl?: number): void {\n    const fullKey = this.getFullKey(key);\n    const expiry = ttl ? Date.now() + ttl : undefined;\n    const item: StorageItem<T> = { expiry, value };\n    try {\n      this.storage.setItem(fullKey, JSON.stringify(item));\n    } catch (error) {\n      console.error(`Error setting item with key \"${fullKey}\":`, error);\n    }\n  }\n}\n\nexport { StorageManager };\n"
  },
  {
    "path": "packages/@core/base/shared/src/cache/types.ts",
    "content": "type StorageType = 'localStorage' | 'sessionStorage';\n\ninterface StorageValue<T> {\n  data: T;\n  expiry: null | number;\n}\n\ninterface IStorageCache {\n  clear(): void;\n  getItem<T>(key: string): null | T;\n  key(index: number): null | string;\n  length(): number;\n  removeItem(key: string): void;\n  setItem<T>(key: string, value: T, expiryInMinutes?: number): void;\n}\n\nexport type { IStorageCache, StorageType, StorageValue };\n"
  },
  {
    "path": "packages/@core/base/shared/src/color/__tests__/convert.test.ts",
    "content": "import { describe, expect, it } from 'vitest';\n\nimport {\n  convertToHsl,\n  convertToHslCssVar,\n  convertToRgb,\n  isValidColor,\n} from '../convert';\n\ndescribe('color conversion functions', () => {\n  it('should correctly convert color to HSL format', () => {\n    const color = '#ff0000';\n    const expectedHsl = 'hsl(0 100% 50%)';\n    expect(convertToHsl(color)).toEqual(expectedHsl);\n  });\n\n  it('should correctly convert color with alpha to HSL format', () => {\n    const color = 'rgba(255, 0, 0, 0.5)';\n    const expectedHsl = 'hsl(0 100% 50%) 0.5';\n    expect(convertToHsl(color)).toEqual(expectedHsl);\n  });\n\n  it('should correctly convert color to HSL CSS variable format', () => {\n    const color = '#ff0000';\n    const expectedHsl = '0 100% 50%';\n    expect(convertToHslCssVar(color)).toEqual(expectedHsl);\n  });\n\n  it('should correctly convert color with alpha to HSL CSS variable format', () => {\n    const color = 'rgba(255, 0, 0, 0.5)';\n    const expectedHsl = '0 100% 50% / 0.5';\n    expect(convertToHslCssVar(color)).toEqual(expectedHsl);\n  });\n\n  it('should correctly convert color to RGB CSS variable format', () => {\n    const color = 'hsl(284, 100%, 50%)';\n    const expectedRgb = 'rgb(187, 0, 255)';\n    expect(convertToRgb(color)).toEqual(expectedRgb);\n  });\n\n  it('should correctly convert color with alpha to RGBA CSS variable format', () => {\n    const color = 'hsla(284, 100%, 50%, 0.92)';\n    const expectedRgba = 'rgba(187, 0, 255, 0.92)';\n    expect(convertToRgb(color)).toEqual(expectedRgba);\n  });\n});\n\ndescribe('isValidColor', () => {\n  it('isValidColor function', () => {\n    // 测试有效颜色\n    expect(isValidColor('blue')).toBe(true);\n    expect(isValidColor('#000000')).toBe(true);\n\n    // 测试无效颜色\n    expect(isValidColor('invalid color')).toBe(false);\n    expect(isValidColor()).toBe(false);\n  });\n});\n"
  },
  {
    "path": "packages/@core/base/shared/src/color/color.ts",
    "content": "import { TinyColor } from '@ctrl/tinycolor';\n\nexport function isDarkColor(color: string) {\n  return new TinyColor(color).isDark();\n}\n\nexport function isLightColor(color: string) {\n  return new TinyColor(color).isLight();\n}\n"
  },
  {
    "path": "packages/@core/base/shared/src/color/convert.ts",
    "content": "import { TinyColor } from '@ctrl/tinycolor';\n\n/**\n * 将颜色转换为HSL格式。\n *\n * HSL是一种颜色模型，包括色相(Hue)、饱和度(Saturation)和亮度(Lightness)三个部分。\n *\n * @param {string} color 输入的颜色。\n * @returns {string} HSL格式的颜色字符串。\n */\nfunction convertToHsl(color: string): string {\n  const { a, h, l, s } = new TinyColor(color).toHsl();\n  const hsl = `hsl(${Math.round(h)} ${Math.round(s * 100)}% ${Math.round(l * 100)}%)`;\n  return a < 1 ? `${hsl} ${a}` : hsl;\n}\n\n/**\n * 将颜色转换为HSL CSS变量。\n *\n * 这个函数与convertToHsl函数类似，但是返回的字符串格式稍有不同，\n * 以便可以作为CSS变量使用。\n *\n * @param {string} color 输入的颜色。\n * @returns {string} 可以作为CSS变量使用的HSL格式的颜色字符串。\n */\nfunction convertToHslCssVar(color: string): string {\n  const { a, h, l, s } = new TinyColor(color).toHsl();\n  const hsl = `${Math.round(h)} ${Math.round(s * 100)}% ${Math.round(l * 100)}%`;\n  return a < 1 ? `${hsl} / ${a}` : hsl;\n}\n\n/**\n * 将颜色转换为RGB颜色字符串\n * TinyColor无法处理hsl内包含'deg'、'grad'、'rad'或'turn'的字符串\n * 比如 hsl(231deg 98% 65%)将被解析为rgb(0, 0, 0)\n * 这里在转换之前先将这些单位去掉\n * @param str 表示HLS颜色值的字符串\n * @returns 如果颜色值有效，则返回对应的RGB颜色字符串；如果无效，则返回rgb(0, 0, 0)\n */\nfunction convertToRgb(str: string): string {\n  return new TinyColor(str.replaceAll(/deg|grad|rad|turn/g, '')).toRgbString();\n}\n\n/**\n * 检查颜色是否有效\n * @param {string} color - 待检查的颜色\n * 如果颜色有效返回true，否则返回false\n */\nfunction isValidColor(color?: string) {\n  if (!color) {\n    return false;\n  }\n  return new TinyColor(color).isValid;\n}\n\nexport {\n  convertToHsl,\n  convertToHslCssVar,\n  convertToRgb,\n  isValidColor,\n  TinyColor,\n};\n"
  },
  {
    "path": "packages/@core/base/shared/src/color/generator.ts",
    "content": "import { getColors } from 'theme-colors';\n\nimport { convertToHslCssVar, TinyColor } from './convert';\n\ninterface ColorItem {\n  alias?: string;\n  color: string;\n  name: string;\n}\n\nfunction generatorColorVariables(colorItems: ColorItem[]) {\n  const colorVariables: Record<string, string> = {};\n\n  colorItems.forEach(({ alias, color, name }) => {\n    if (color) {\n      const colorsMap = getColors(new TinyColor(color).toHexString());\n\n      let mainColor = colorsMap['500'];\n\n      const colorKeys = Object.keys(colorsMap);\n\n      colorKeys.forEach((key) => {\n        const colorValue = colorsMap[key];\n\n        if (colorValue) {\n          const hslColor = convertToHslCssVar(colorValue);\n          colorVariables[`--${name}-${key}`] = hslColor;\n          if (alias) {\n            colorVariables[`--${alias}-${key}`] = hslColor;\n          }\n\n          if (key === '500') {\n            mainColor = hslColor;\n          }\n        }\n      });\n      if (alias && mainColor) {\n        colorVariables[`--${alias}`] = mainColor;\n      }\n    }\n  });\n  return colorVariables;\n}\n\nexport { generatorColorVariables };\n"
  },
  {
    "path": "packages/@core/base/shared/src/color/index.ts",
    "content": "export * from './color';\nexport * from './convert';\nexport * from './generator';\n"
  },
  {
    "path": "packages/@core/base/shared/src/constants/dict-enum.ts",
    "content": "export const DictEnum = {\n  SYS_COMMON_STATUS: 'sys_common_status',\n  SYS_DEVICE_TYPE: 'sys_device_type', // 设备类型\n  SYS_GRANT_TYPE: 'sys_grant_type', // 授权类型\n  SYS_NORMAL_DISABLE: 'sys_normal_disable',\n  SYS_NOTICE_STATUS: 'sys_notice_status', // 通知状态\n  SYS_NOTICE_TYPE: 'sys_notice_type', // 通知类型\n  SYS_OPER_TYPE: 'sys_oper_type', // 操作类型\n  SYS_OSS_ACCESS_POLICY: 'oss_access_policy', // oss权限桶类型\n  SYS_SHOW_HIDE: 'sys_show_hide', // 显示状态\n  SYS_USER_SEX: 'sys_user_sex', // 性别\n  SYS_YES_NO: 'sys_yes_no', // 是否\n  WF_BUSINESS_STATUS: 'wf_business_status', // 业务状态\n  WF_FORM_TYPE: 'wf_form_type', // 表单类型\n  WF_TASK_STATUS: 'wf_task_status', // 任务状态\n  SYS_MODEL_BILLING: 'sys_model_billing', // 计费方式\n  CHAT_MODEL_CATEGORY: 'chat_model_category', // 模型分类\n} as const;\n\nexport type DictEnumKey = keyof typeof DictEnum;\n"
  },
  {
    "path": "packages/@core/base/shared/src/constants/globals.ts",
    "content": "/** layout content 组件的高度 */\nexport const CSS_VARIABLE_LAYOUT_CONTENT_HEIGHT = `--vben-content-height`;\n/** layout content 组件的宽度 */\nexport const CSS_VARIABLE_LAYOUT_CONTENT_WIDTH = `--vben-content-width`;\n/** layout header 组件的高度 */\nexport const CSS_VARIABLE_LAYOUT_HEADER_HEIGHT = `--vben-header-height`;\n/** layout footer 组件的高度 */\nexport const CSS_VARIABLE_LAYOUT_FOOTER_HEIGHT = `--vben-footer-height`;\n\n/** 内容区域的组件ID */\nexport const ELEMENT_ID_MAIN_CONTENT = `__vben_main_content`;\n\n/**\n * @zh_CN 默认命名空间\n */\nexport const DEFAULT_NAMESPACE = 'vben';\n"
  },
  {
    "path": "packages/@core/base/shared/src/constants/index.ts",
    "content": "export * from './dict-enum';\nexport * from './globals';\nexport * from './vben';\n"
  },
  {
    "path": "packages/@core/base/shared/src/constants/vben.ts",
    "content": "/**\n * @zh_CN GITHUB 仓库地址\n */\nexport const VBEN_GITHUB_URL = 'https://github.com/vbenjs/vue-vben-admin';\n\n/**\n * @zh_CN 文档地址\n */\nexport const VBEN_DOC_URL = 'https://doc.vben.pro';\n\n/**\n * @zh_CN Vben Logo\n */\nexport const VBEN_LOGO_URL =\n  'https://ruoyiai-1254149996.cos.ap-guangzhou.myqcloud.com/2026/03/15/4b7e93a72bf04805ae59985cc0845ef1.png';\n\n/**\n * @zh_CN Vben Admin 首页地址\n */\nexport const VBEN_PREVIEW_URL = 'https://www.vben.pro';\n\nexport const VBEN_ELE_PREVIEW_URL = 'https://ele.vben.pro';\n\nexport const VBEN_NAIVE_PREVIEW_URL = 'https://naive.vben.pro';\n\n"
  },
  {
    "path": "packages/@core/base/shared/src/global-state.ts",
    "content": "/**\n * 全局复用的变量、组件、配置，各个模块之间共享\n * 通过单例模式实现,单例必须注意不受请求影响，例如用户信息这些需要根据请求获取的。后续如果有ssr需求，也不会影响\n */\n\ninterface ComponentsState {\n  [key: string]: any;\n}\n\ninterface MessageState {\n  copyPreferencesSuccess?: (title: string, content?: string) => void;\n}\n\nexport interface IGlobalSharedState {\n  components: ComponentsState;\n  message: MessageState;\n}\n\nclass GlobalShareState {\n  #components: ComponentsState = {};\n  #message: MessageState = {};\n\n  /**\n   * 定义框架内部各个场景的消息提示\n   */\n  public defineMessage({ copyPreferencesSuccess }: MessageState) {\n    this.#message = {\n      copyPreferencesSuccess,\n    };\n  }\n\n  public getComponents(): ComponentsState {\n    return this.#components;\n  }\n\n  public getMessage(): MessageState {\n    return this.#message;\n  }\n\n  public setComponents(value: ComponentsState) {\n    this.#components = value;\n  }\n}\n\nexport const globalShareState = new GlobalShareState();\n"
  },
  {
    "path": "packages/@core/base/shared/src/store.ts",
    "content": "export * from '@tanstack/vue-store';\n"
  },
  {
    "path": "packages/@core/base/shared/src/utils/__tests__/diff.test.ts",
    "content": "import { describe, expect, it } from 'vitest';\n\nimport { diff } from '../diff';\n\ndescribe('diff function', () => {\n  it('should return an empty object when comparing identical objects', () => {\n    const obj1 = { a: 1, b: { c: 2 } };\n    const obj2 = { a: 1, b: { c: 2 } };\n    expect(diff(obj1, obj2)).toEqual(undefined);\n  });\n\n  it('should detect simple changes in primitive values', () => {\n    const obj1 = { a: 1, b: 2 };\n    const obj2 = { a: 1, b: 3 };\n    expect(diff(obj1, obj2)).toEqual({ b: 3 });\n  });\n\n  it('should detect nested object changes', () => {\n    const obj1 = { a: 1, b: { c: 2, d: 4 } };\n    const obj2 = { a: 1, b: { c: 3, d: 4 } };\n    expect(diff(obj1, obj2)).toEqual({ b: { c: 3 } });\n  });\n\n  it('should handle array changes', () => {\n    const obj1 = { a: [1, 2, 3], b: 2 };\n    const obj2 = { a: [1, 2, 4], b: 2 };\n    expect(diff(obj1, obj2)).toEqual({ a: [1, 2, 4] });\n  });\n\n  it('should handle added keys', () => {\n    const obj1 = { a: 1 };\n    const obj2 = { a: 1, b: 2 };\n    expect(diff(obj1, obj2)).toEqual({ b: 2 });\n  });\n\n  it('should handle removed keys', () => {\n    const obj1 = { a: 1, b: 2 };\n    const obj2 = { a: 1 };\n    expect(diff(obj1, obj2)).toEqual(undefined);\n  });\n\n  it('should handle boolean value changes', () => {\n    const obj1 = { a: true, b: false };\n    const obj2 = { a: true, b: true };\n    expect(diff(obj1, obj2)).toEqual({ b: true });\n  });\n\n  it('should handle null and undefined values', () => {\n    const obj1 = { a: null, b: undefined };\n    const obj2: any = { a: 1, b: undefined };\n    expect(diff(obj1, obj2)).toEqual({ a: 1 });\n  });\n});\n"
  },
  {
    "path": "packages/@core/base/shared/src/utils/__tests__/dom.test.ts",
    "content": "import { beforeEach, describe, expect, it, vi } from 'vitest';\n\nimport { getElementVisibleRect } from '../dom';\n\ndescribe('getElementVisibleRect', () => {\n  // 设置浏览器视口尺寸的 mock\n  beforeEach(() => {\n    vi.spyOn(document.documentElement, 'clientHeight', 'get').mockReturnValue(\n      800,\n    );\n    vi.spyOn(window, 'innerHeight', 'get').mockReturnValue(800);\n    vi.spyOn(document.documentElement, 'clientWidth', 'get').mockReturnValue(\n      1000,\n    );\n    vi.spyOn(window, 'innerWidth', 'get').mockReturnValue(1000);\n  });\n\n  it('should return default rect if element is undefined', () => {\n    expect(getElementVisibleRect()).toEqual({\n      bottom: 0,\n      height: 0,\n      left: 0,\n      right: 0,\n      top: 0,\n      width: 0,\n    });\n  });\n\n  it('should return default rect if element is null', () => {\n    expect(getElementVisibleRect(null)).toEqual({\n      bottom: 0,\n      height: 0,\n      left: 0,\n      right: 0,\n      top: 0,\n      width: 0,\n    });\n  });\n\n  it('should return correct visible rect when element is fully visible', () => {\n    const element = {\n      getBoundingClientRect: () => ({\n        bottom: 400,\n        height: 300,\n        left: 200,\n        right: 600,\n        top: 100,\n        width: 400,\n      }),\n    } as HTMLElement;\n\n    expect(getElementVisibleRect(element)).toEqual({\n      bottom: 400,\n      height: 300,\n      left: 200,\n      right: 600,\n      top: 100,\n      width: 400,\n    });\n  });\n\n  it('should return correct visible rect when element is partially off-screen at the top', () => {\n    const element = {\n      getBoundingClientRect: () => ({\n        bottom: 200,\n        height: 250,\n        left: 100,\n        right: 500,\n        top: -50,\n        width: 400,\n      }),\n    } as HTMLElement;\n\n    expect(getElementVisibleRect(element)).toEqual({\n      bottom: 200,\n      height: 200,\n      left: 100,\n      right: 500,\n      top: 0,\n      width: 400,\n    });\n  });\n\n  it('should return correct visible rect when element is partially off-screen at the right', () => {\n    const element = {\n      getBoundingClientRect: () => ({\n        bottom: 400,\n        height: 300,\n        left: 800,\n        right: 1200,\n        top: 100,\n        width: 400,\n      }),\n    } as HTMLElement;\n\n    expect(getElementVisibleRect(element)).toEqual({\n      bottom: 400,\n      height: 300,\n      left: 800,\n      right: 1000,\n      top: 100,\n      width: 200,\n    });\n  });\n\n  it('should return all zeros when element is completely off-screen', () => {\n    const element = {\n      getBoundingClientRect: () => ({\n        bottom: 1200,\n        height: 300,\n        left: 1100,\n        right: 1400,\n        top: 900,\n        width: 300,\n      }),\n    } as HTMLElement;\n\n    expect(getElementVisibleRect(element)).toEqual({\n      bottom: 800,\n      height: 0,\n      left: 1100,\n      right: 1000,\n      top: 900,\n      width: 0,\n    });\n  });\n});\n"
  },
  {
    "path": "packages/@core/base/shared/src/utils/__tests__/inference.test.ts",
    "content": "import { describe, expect, it } from 'vitest';\n\nimport {\n  getFirstNonNullOrUndefined,\n  isBoolean,\n  isEmpty,\n  isHttpUrl,\n  isObject,\n  isUndefined,\n  isWindow,\n} from '../inference';\n\ndescribe('isHttpUrl', () => {\n  it(\"should return true when given 'http://example.com'\", () => {\n    expect(isHttpUrl('http://example.com')).toBe(true);\n  });\n\n  it(\"should return true when given 'https://example.com'\", () => {\n    expect(isHttpUrl('https://example.com')).toBe(true);\n  });\n\n  it(\"should return false when given 'ftp://example.com'\", () => {\n    expect(isHttpUrl('ftp://example.com')).toBe(false);\n  });\n\n  it(\"should return false when given 'example.com'\", () => {\n    expect(isHttpUrl('example.com')).toBe(false);\n  });\n});\n\ndescribe('isUndefined', () => {\n  it('isUndefined should return true for undefined values', () => {\n    expect(isUndefined()).toBe(true);\n  });\n\n  it('isUndefined should return false for null values', () => {\n    expect(isUndefined(null)).toBe(false);\n  });\n\n  it('isUndefined should return false for defined values', () => {\n    expect(isUndefined(0)).toBe(false);\n    expect(isUndefined('')).toBe(false);\n    expect(isUndefined(false)).toBe(false);\n  });\n\n  it('isUndefined should return false for objects and arrays', () => {\n    expect(isUndefined({})).toBe(false);\n    expect(isUndefined([])).toBe(false);\n  });\n});\n\ndescribe('isEmpty', () => {\n  it('should return true for empty string', () => {\n    expect(isEmpty('')).toBe(true);\n  });\n\n  it('should return true for empty array', () => {\n    expect(isEmpty([])).toBe(true);\n  });\n\n  it('should return true for empty object', () => {\n    expect(isEmpty({})).toBe(true);\n  });\n\n  it('should return false for non-empty string', () => {\n    expect(isEmpty('hello')).toBe(false);\n  });\n\n  it('should return false for non-empty array', () => {\n    expect(isEmpty([1, 2, 3])).toBe(false);\n  });\n\n  it('should return false for non-empty object', () => {\n    expect(isEmpty({ a: 1 })).toBe(false);\n  });\n\n  it('should return true for null or undefined', () => {\n    expect(isEmpty(null)).toBe(true);\n    expect(isEmpty()).toBe(true);\n  });\n\n  it('should return false for number or boolean', () => {\n    expect(isEmpty(0)).toBe(false);\n    expect(isEmpty(true)).toBe(false);\n  });\n});\n\ndescribe('isWindow', () => {\n  it('should return true for the window object', () => {\n    expect(isWindow(window)).toBe(true);\n  });\n\n  it('should return false for other objects', () => {\n    expect(isWindow({})).toBe(false);\n    expect(isWindow([])).toBe(false);\n    expect(isWindow(null)).toBe(false);\n  });\n});\n\ndescribe('isBoolean', () => {\n  it('should return true for boolean values', () => {\n    expect(isBoolean(true)).toBe(true);\n    expect(isBoolean(false)).toBe(true);\n  });\n\n  it('should return false for non-boolean values', () => {\n    expect(isBoolean(null)).toBe(false);\n    expect(isBoolean(42)).toBe(false);\n    expect(isBoolean('string')).toBe(false);\n    expect(isBoolean({})).toBe(false);\n    expect(isBoolean([])).toBe(false);\n  });\n});\n\ndescribe('isObject', () => {\n  it('should return true for objects', () => {\n    expect(isObject({})).toBe(true);\n    expect(isObject({ a: 1 })).toBe(true);\n  });\n\n  it('should return false for non-objects', () => {\n    expect(isObject(null)).toBe(false);\n    expect(isObject(42)).toBe(false);\n    expect(isObject('string')).toBe(false);\n    expect(isObject(true)).toBe(false);\n    expect(isObject([1, 2, 3])).toBe(true);\n    expect(isObject(new Date())).toBe(true);\n    expect(isObject(/regex/)).toBe(true);\n  });\n});\n\ndescribe('getFirstNonNullOrUndefined', () => {\n  describe('getFirstNonNullOrUndefined', () => {\n    it('should return the first non-null and non-undefined value for a number array', () => {\n      expect(getFirstNonNullOrUndefined<number>(undefined, null, 0, 42)).toBe(\n        0,\n      );\n      expect(getFirstNonNullOrUndefined<number>(null, undefined, 42, 123)).toBe(\n        42,\n      );\n    });\n\n    it('should return the first non-null and non-undefined value for a string array', () => {\n      expect(\n        getFirstNonNullOrUndefined<string>(undefined, null, '', 'hello'),\n      ).toBe('');\n      expect(\n        getFirstNonNullOrUndefined<string>(null, undefined, 'test', 'world'),\n      ).toBe('test');\n    });\n\n    it('should return undefined if all values are null or undefined', () => {\n      expect(getFirstNonNullOrUndefined(undefined, null)).toBeUndefined();\n      expect(getFirstNonNullOrUndefined(null)).toBeUndefined();\n    });\n\n    it('should work with a single value', () => {\n      expect(getFirstNonNullOrUndefined(42)).toBe(42);\n      expect(getFirstNonNullOrUndefined()).toBeUndefined();\n      expect(getFirstNonNullOrUndefined(null)).toBeUndefined();\n    });\n\n    it('should handle mixed types correctly', () => {\n      expect(\n        getFirstNonNullOrUndefined<number | object | string>(\n          undefined,\n          null,\n          'test',\n          123,\n          { key: 'value' },\n        ),\n      ).toBe('test');\n      expect(\n        getFirstNonNullOrUndefined<number | object | string>(\n          null,\n          undefined,\n          [1, 2, 3],\n          'string',\n        ),\n      ).toEqual([1, 2, 3]);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/@core/base/shared/src/utils/__tests__/letter.test.ts",
    "content": "import { describe, expect, it } from 'vitest';\n\nimport {\n  capitalizeFirstLetter,\n  kebabToCamelCase,\n  toCamelCase,\n  toLowerCaseFirstLetter,\n} from '../letter';\n\ndescribe('capitalizeFirstLetter', () => {\n  it('should capitalize the first letter of a string', () => {\n    expect(capitalizeFirstLetter('hello')).toBe('Hello');\n    expect(capitalizeFirstLetter('world')).toBe('World');\n  });\n\n  it('should handle empty strings', () => {\n    expect(capitalizeFirstLetter('')).toBe('');\n  });\n\n  it('should handle single character strings', () => {\n    expect(capitalizeFirstLetter('a')).toBe('A');\n    expect(capitalizeFirstLetter('b')).toBe('B');\n  });\n\n  it('should not change the case of other characters', () => {\n    expect(capitalizeFirstLetter('hElLo')).toBe('HElLo');\n  });\n});\n\ndescribe('toLowerCaseFirstLetter', () => {\n  it('should convert the first letter to lowercase', () => {\n    expect(toLowerCaseFirstLetter('CommonAppName')).toBe('commonAppName');\n    expect(toLowerCaseFirstLetter('AnotherKeyExample')).toBe(\n      'anotherKeyExample',\n    );\n  });\n\n  it('should return the same string if the first letter is already lowercase', () => {\n    expect(toLowerCaseFirstLetter('alreadyLowerCase')).toBe('alreadyLowerCase');\n  });\n\n  it('should handle empty strings', () => {\n    expect(toLowerCaseFirstLetter('')).toBe('');\n  });\n\n  it('should handle single character strings', () => {\n    expect(toLowerCaseFirstLetter('A')).toBe('a');\n    expect(toLowerCaseFirstLetter('a')).toBe('a');\n  });\n\n  it('should handle strings with only one uppercase letter', () => {\n    expect(toLowerCaseFirstLetter('A')).toBe('a');\n  });\n\n  it('should handle strings with special characters', () => {\n    expect(toLowerCaseFirstLetter('!Special')).toBe('!Special');\n    expect(toLowerCaseFirstLetter('123Number')).toBe('123Number');\n  });\n});\n\ndescribe('toCamelCase', () => {\n  it('should return the key if parentKey is empty', () => {\n    expect(toCamelCase('child', '')).toBe('child');\n  });\n\n  it('should combine parentKey and key in camel case', () => {\n    expect(toCamelCase('child', 'parent')).toBe('parentChild');\n  });\n\n  it('should handle empty key and parentKey', () => {\n    expect(toCamelCase('', '')).toBe('');\n  });\n\n  it('should handle key with capital letters', () => {\n    expect(toCamelCase('Child', 'parent')).toBe('parentChild');\n    expect(toCamelCase('Child', 'Parent')).toBe('ParentChild');\n  });\n});\n\ndescribe('kebabToCamelCase', () => {\n  it('should convert kebab-case to camelCase correctly', () => {\n    expect(kebabToCamelCase('my-component-name')).toBe('myComponentName');\n  });\n\n  it('should handle multiple consecutive hyphens', () => {\n    expect(kebabToCamelCase('my--component--name')).toBe('myComponentName');\n  });\n\n  it('should trim leading and trailing hyphens', () => {\n    expect(kebabToCamelCase('-my-component-name-')).toBe('myComponentName');\n  });\n\n  it('should preserve the case of the first word', () => {\n    expect(kebabToCamelCase('My-component-name')).toBe('MyComponentName');\n  });\n\n  it('should convert a single word correctly', () => {\n    expect(kebabToCamelCase('component')).toBe('component');\n  });\n\n  it('should return an empty string if input is empty', () => {\n    expect(kebabToCamelCase('')).toBe('');\n  });\n\n  it('should handle strings with no hyphens', () => {\n    expect(kebabToCamelCase('mycomponentname')).toBe('mycomponentname');\n  });\n\n  it('should handle strings with only hyphens', () => {\n    expect(kebabToCamelCase('---')).toBe('');\n  });\n\n  it('should handle mixed case inputs', () => {\n    expect(kebabToCamelCase('my-Component-Name')).toBe('myComponentName');\n  });\n});\n"
  },
  {
    "path": "packages/@core/base/shared/src/utils/__tests__/resources.test.ts",
    "content": "import { beforeEach, describe, expect, it } from 'vitest';\n\nimport { loadScript } from '../resources';\n\nconst testJsPath =\n  'https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js';\n\ndescribe('loadScript', () => {\n  beforeEach(() => {\n    // 每个测试前清空 head，保证环境干净\n    document.head.innerHTML = '';\n  });\n\n  it('should resolve when the script loads successfully', async () => {\n    const promise = loadScript(testJsPath);\n\n    // 此时脚本元素已被创建并插入\n    const script = document.querySelector(\n      `script[src=\"${testJsPath}\"]`,\n    ) as HTMLScriptElement;\n    expect(script).toBeTruthy();\n\n    // 模拟加载成功\n    script.dispatchEvent(new Event('load'));\n\n    // 等待 promise resolve\n    await expect(promise).resolves.toBeUndefined();\n  });\n\n  it('should not insert duplicate script and resolve immediately if already loaded', async () => {\n    // 先手动插入一个相同 src 的 script\n    const existing = document.createElement('script');\n    existing.src = 'bar.js';\n    document.head.append(existing);\n\n    // 再次调用\n    const promise = loadScript('bar.js');\n\n    // 立即 resolve\n    await expect(promise).resolves.toBeUndefined();\n\n    // head 中只保留一个\n    const scripts = document.head.querySelectorAll('script[src=\"bar.js\"]');\n    expect(scripts).toHaveLength(1);\n  });\n\n  it('should reject when the script fails to load', async () => {\n    const promise = loadScript('error.js');\n\n    const script = document.querySelector(\n      'script[src=\"error.js\"]',\n    ) as HTMLScriptElement;\n    expect(script).toBeTruthy();\n\n    // 模拟加载失败\n    script.dispatchEvent(new Event('error'));\n\n    await expect(promise).rejects.toThrow('Failed to load script: error.js');\n  });\n\n  it('should handle multiple concurrent calls and only insert one script tag', async () => {\n    const p1 = loadScript(testJsPath);\n    const p2 = loadScript(testJsPath);\n\n    const script = document.querySelector(\n      `script[src=\"${testJsPath}\"]`,\n    ) as HTMLScriptElement;\n    expect(script).toBeTruthy();\n\n    // 触发一次 load，两个 promise 都应该 resolve\n    script.dispatchEvent(new Event('load'));\n\n    await expect(p1).resolves.toBeUndefined();\n    await expect(p2).resolves.toBeUndefined();\n\n    // 只插入一次\n    const scripts = document.head.querySelectorAll(\n      `script[src=\"${testJsPath}\"]`,\n    );\n    expect(scripts).toHaveLength(1);\n  });\n});\n"
  },
  {
    "path": "packages/@core/base/shared/src/utils/__tests__/state-handler.test.ts",
    "content": "import { describe, expect, it } from 'vitest';\n\nimport { StateHandler } from '../state-handler';\n\ndescribe('stateHandler', () => {\n  it('should resolve when condition is set to true', async () => {\n    const handler = new StateHandler();\n\n    // 模拟异步设置 condition 为 true\n    setTimeout(() => {\n      handler.setConditionTrue(); // 明确触发 condition 为 true\n    }, 10);\n\n    // 等待条件被设置为 true\n    await handler.waitForCondition();\n    expect(handler.isConditionTrue()).toBe(true);\n  });\n\n  it('should resolve immediately if condition is already true', async () => {\n    const handler = new StateHandler();\n    handler.setConditionTrue(); // 提前设置为 true\n\n    // 立即 resolve，因为 condition 已经是 true\n    await handler.waitForCondition();\n    expect(handler.isConditionTrue()).toBe(true);\n  });\n\n  it('should reject when condition is set to false after waiting', async () => {\n    const handler = new StateHandler();\n\n    // 模拟异步设置 condition 为 false\n    setTimeout(() => {\n      handler.setConditionFalse(); // 明确触发 condition 为 false\n    }, 10);\n\n    // 等待过程中，期望 Promise 被 reject\n    await expect(handler.waitForCondition()).rejects.toThrow();\n    expect(handler.isConditionTrue()).toBe(false);\n  });\n\n  it('should reset condition to false', () => {\n    const handler = new StateHandler();\n    handler.setConditionTrue(); // 设置为 true\n    handler.reset(); // 重置为 false\n\n    expect(handler.isConditionTrue()).toBe(false);\n  });\n\n  it('should resolve when condition is set to true after reset', async () => {\n    const handler = new StateHandler();\n    handler.reset(); // 确保初始为 false\n\n    setTimeout(() => {\n      handler.setConditionTrue(); // 重置后设置为 true\n    }, 10);\n\n    await handler.waitForCondition();\n    expect(handler.isConditionTrue()).toBe(true);\n  });\n});\n"
  },
  {
    "path": "packages/@core/base/shared/src/utils/__tests__/tree.test.ts",
    "content": "import { describe, expect, it } from 'vitest';\n\nimport { filterTree, mapTree, traverseTreeValues } from '../tree';\n\ndescribe('traverseTreeValues', () => {\n  interface Node {\n    children?: Node[];\n    name: string;\n  }\n\n  type NodeValue = string;\n\n  const sampleTree: Node[] = [\n    {\n      name: 'A',\n      children: [\n        { name: 'B' },\n        {\n          name: 'C',\n          children: [{ name: 'D' }, { name: 'E' }],\n        },\n      ],\n    },\n    {\n      name: 'F',\n      children: [\n        { name: 'G' },\n        {\n          name: 'H',\n          children: [{ name: 'I' }],\n        },\n      ],\n    },\n  ];\n\n  it('traverses tree and returns all node values', () => {\n    const values = traverseTreeValues<Node, NodeValue>(\n      sampleTree,\n      (node) => node.name,\n      {\n        childProps: 'children',\n      },\n    );\n    expect(values).toEqual(['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I']);\n  });\n\n  it('handles empty tree', () => {\n    const values = traverseTreeValues<Node, NodeValue>([], (node) => node.name);\n    expect(values).toEqual([]);\n  });\n\n  it('handles tree with only root node', () => {\n    const rootNode = { name: 'A' };\n    const values = traverseTreeValues<Node, NodeValue>(\n      [rootNode],\n      (node) => node.name,\n    );\n    expect(values).toEqual(['A']);\n  });\n\n  it('handles tree with only leaf nodes', () => {\n    const leafNodes = [{ name: 'A' }, { name: 'B' }, { name: 'C' }];\n    const values = traverseTreeValues<Node, NodeValue>(\n      leafNodes,\n      (node) => node.name,\n    );\n    expect(values).toEqual(['A', 'B', 'C']);\n  });\n});\n\ndescribe('filterTree', () => {\n  const tree = [\n    {\n      id: 1,\n      children: [\n        { id: 2 },\n        { id: 3, children: [{ id: 4 }, { id: 5 }, { id: 6 }] },\n        { id: 7 },\n      ],\n    },\n    { id: 8, children: [{ id: 9 }, { id: 10 }] },\n    { id: 11 },\n  ];\n\n  it('should return all nodes when condition is always true', () => {\n    const result = filterTree(tree, () => true, { childProps: 'children' });\n    expect(result).toEqual(tree);\n  });\n\n  it('should return only root nodes when condition is always false', () => {\n    const result = filterTree(tree, () => false);\n    expect(result).toEqual([]);\n  });\n\n  it('should return nodes with even id values', () => {\n    const result = filterTree(tree, (node) => node.id % 2 === 0);\n    expect(result).toEqual([{ id: 8, children: [{ id: 10 }] }]);\n  });\n\n  it('should return nodes with odd id values and their ancestors', () => {\n    const result = filterTree(tree, (node) => node.id % 2 === 1);\n    expect(result).toEqual([\n      {\n        id: 1,\n        children: [{ id: 3, children: [{ id: 5 }] }, { id: 7 }],\n      },\n      { id: 11 },\n    ]);\n  });\n\n  it('should return nodes with \"leaf\" in their name', () => {\n    const tree = [\n      {\n        name: 'root',\n        children: [\n          { name: 'leaf 1' },\n          {\n            name: 'branch',\n            children: [{ name: 'leaf 2' }, { name: 'leaf 3' }],\n          },\n          { name: 'leaf 4' },\n        ],\n      },\n    ];\n    const result = filterTree(\n      tree,\n      (node) => node.name.includes('leaf') || node.name === 'root',\n    );\n    expect(result).toEqual([\n      {\n        name: 'root',\n        children: [{ name: 'leaf 1' }, { name: 'leaf 4' }],\n      },\n    ]);\n  });\n});\n\ndescribe('mapTree', () => {\n  it('map infinite depth tree using mapTree', () => {\n    const tree = [\n      {\n        id: 1,\n        name: 'node1',\n        children: [\n          { id: 2, name: 'node2' },\n          { id: 3, name: 'node3' },\n          {\n            id: 4,\n            name: 'node4',\n            children: [\n              {\n                id: 5,\n                name: 'node5',\n                children: [\n                  { id: 6, name: 'node6' },\n                  { id: 7, name: 'node7' },\n                ],\n              },\n              { id: 8, name: 'node8' },\n            ],\n          },\n        ],\n      },\n    ];\n    const newTree = mapTree(tree, (node) => ({\n      ...node,\n      name: `${node.name}-new`,\n    }));\n\n    expect(newTree).toEqual([\n      {\n        id: 1,\n        name: 'node1-new',\n        children: [\n          { id: 2, name: 'node2-new' },\n          { id: 3, name: 'node3-new' },\n          {\n            id: 4,\n            name: 'node4-new',\n            children: [\n              {\n                id: 5,\n                name: 'node5-new',\n                children: [\n                  { id: 6, name: 'node6-new' },\n                  { id: 7, name: 'node7-new' },\n                ],\n              },\n              { id: 8, name: 'node8-new' },\n            ],\n          },\n        ],\n      },\n    ]);\n  });\n});\n"
  },
  {
    "path": "packages/@core/base/shared/src/utils/__tests__/unique.test.ts",
    "content": "import { describe, expect, it } from 'vitest';\n\nimport { uniqueByField } from '../unique';\n\ndescribe('uniqueByField', () => {\n  it('should return an array with unique items based on id field', () => {\n    const items = [\n      { id: 1, name: 'Item 1' },\n      { id: 2, name: 'Item 2' },\n      { id: 3, name: 'Item 3' },\n      { id: 1, name: 'Duplicate Item' },\n    ];\n\n    const uniqueItems = uniqueByField(items, 'id');\n\n    expect(uniqueItems).toHaveLength(3);\n    expect(uniqueItems).toEqual([\n      { id: 1, name: 'Item 1' },\n      { id: 2, name: 'Item 2' },\n      { id: 3, name: 'Item 3' },\n    ]);\n  });\n\n  it('should return an empty array when input array is empty', () => {\n    const items: any[] = []; // Empty array\n\n    const uniqueItems = uniqueByField(items, 'id');\n\n    // Assert expected results\n    expect(uniqueItems).toEqual([]);\n  });\n\n  it('should handle arrays with only one item correctly', () => {\n    const items = [{ id: 1, name: 'Item 1' }];\n\n    const uniqueItems = uniqueByField(items, 'id');\n\n    // Assert expected results\n    expect(uniqueItems).toHaveLength(1);\n    expect(uniqueItems).toEqual([{ id: 1, name: 'Item 1' }]);\n  });\n\n  it('should preserve the order of the first occurrence of each item', () => {\n    const items = [\n      { id: 2, name: 'Item 2' },\n      { id: 1, name: 'Item 1' },\n      { id: 3, name: 'Item 3' },\n      { id: 1, name: 'Duplicate Item' },\n    ];\n\n    const uniqueItems = uniqueByField(items, 'id');\n\n    // Assert expected results (order of first occurrences preserved)\n    expect(uniqueItems).toEqual([\n      { id: 2, name: 'Item 2' },\n      { id: 1, name: 'Item 1' },\n      { id: 3, name: 'Item 3' },\n    ]);\n  });\n});\n"
  },
  {
    "path": "packages/@core/base/shared/src/utils/__tests__/update-css-variables.test.ts",
    "content": "import { expect, it } from 'vitest';\n\nimport { updateCSSVariables } from '../update-css-variables';\n\nit('updateCSSVariables should update CSS variables in :root selector', () => {\n  // 模拟初始的内联样式表内容\n  const initialStyleContent = ':root { --primaryColor: red; }';\n  document.head.innerHTML = `<style id=\"custom-styles\">${initialStyleContent}</style>`;\n\n  // 要更新的CSS变量和它们的新值\n  const updatedVariables = {\n    fontSize: '16px',\n    primaryColor: 'blue',\n    secondaryColor: 'green',\n  };\n\n  // 调用函数来更新CSS变量\n  updateCSSVariables(updatedVariables, 'custom-styles');\n\n  // 获取更新后的样式内容\n  const styleElement = document.querySelector('#custom-styles');\n  const updatedStyleContent = styleElement ? styleElement.textContent : '';\n\n  // 检查更新后的样式内容是否包含正确的更新值\n  expect(\n    updatedStyleContent?.includes('primaryColor: blue;') &&\n      updatedStyleContent?.includes('secondaryColor: green;') &&\n      updatedStyleContent?.includes('fontSize: 16px;'),\n  ).toBe(true);\n});\n"
  },
  {
    "path": "packages/@core/base/shared/src/utils/__tests__/util.test.ts",
    "content": "import { describe, expect, it } from 'vitest';\n\nimport { bindMethods, getNestedValue } from '../util';\n\nclass TestClass {\n  public value: string;\n\n  constructor(value: string) {\n    this.value = value;\n    bindMethods(this); // 调用通用方法\n  }\n\n  getValue() {\n    return this.value;\n  }\n\n  setValue(newValue: string) {\n    this.value = newValue;\n  }\n}\n\ndescribe('bindMethods', () => {\n  it('should bind methods to the instance correctly', () => {\n    const instance = new TestClass('initial');\n\n    // 解构方法\n    const { getValue } = instance;\n\n    // 检查 getValue 是否能正确调用，并且 this 绑定了 instance\n    expect(getValue()).toBe('initial');\n  });\n\n  it('should bind multiple methods', () => {\n    const instance = new TestClass('initial');\n\n    const { getValue, setValue } = instance;\n\n    // 检查 getValue 和 setValue 方法是否正确绑定了 this\n    setValue('newValue');\n    expect(getValue()).toBe('newValue');\n  });\n\n  it('should not bind non-function properties', () => {\n    const instance = new TestClass('initial');\n\n    // 检查普通属性是否保持原样\n    expect(instance.value).toBe('initial');\n  });\n\n  it('should not bind constructor method', () => {\n    const instance = new TestClass('test');\n\n    // 检查 constructor 是否没有被绑定\n    expect(instance.constructor.name).toBe('TestClass');\n  });\n\n  it('should not bind getter/setter properties', () => {\n    class TestWithGetterSetter {\n      private _value: string = 'test';\n\n      constructor() {\n        bindMethods(this);\n      }\n\n      get value() {\n        return this._value;\n      }\n\n      set value(newValue: string) {\n        this._value = newValue;\n      }\n    }\n\n    const instance = new TestWithGetterSetter();\n    const { value } = instance;\n\n    // Getter 和 setter 不应被绑定\n    expect(value).toBe('test');\n  });\n});\n\ndescribe('getNestedValue', () => {\n  interface UserProfile {\n    age: number;\n    name: string;\n  }\n\n  interface UserSettings {\n    theme: string;\n  }\n\n  interface Data {\n    user: {\n      profile: UserProfile;\n      settings: UserSettings;\n    };\n  }\n\n  const data: Data = {\n    user: {\n      profile: {\n        age: 25,\n        name: 'Alice',\n      },\n      settings: {\n        theme: 'dark',\n      },\n    },\n  };\n\n  it('should get a nested value when the path is valid', () => {\n    const result = getNestedValue(data, 'user.profile.name');\n    expect(result).toBe('Alice');\n  });\n\n  it('should return undefined for non-existent property', () => {\n    const result = getNestedValue(data, 'user.profile.gender');\n    expect(result).toBeUndefined();\n  });\n\n  it('should return undefined when accessing a non-existent deep path', () => {\n    const result = getNestedValue(data, 'user.nonexistent.field');\n    expect(result).toBeUndefined();\n  });\n\n  it('should return undefined if a middle level is undefined', () => {\n    const result = getNestedValue({ user: undefined }, 'user.profile.name');\n    expect(result).toBeUndefined();\n  });\n\n  it('should return the correct value for a nested setting', () => {\n    const result = getNestedValue(data, 'user.settings.theme');\n    expect(result).toBe('dark');\n  });\n\n  it('should work for a single-level path', () => {\n    const result = getNestedValue({ a: 1, b: 2 }, 'b');\n    expect(result).toBe(2);\n  });\n\n  it('should return the entire object if path is empty', () => {\n    expect(() => getNestedValue(data, '')()).toThrow();\n  });\n\n  it('should handle paths with array indexes', () => {\n    const complexData = { list: [{ name: 'Item1' }, { name: 'Item2' }] };\n    const result = getNestedValue(complexData, 'list.1.name');\n    expect(result).toBe('Item2');\n  });\n\n  it('should return undefined when accessing an out-of-bounds array index', () => {\n    const complexData = { list: [{ name: 'Item1' }] };\n    const result = getNestedValue(complexData, 'list.2.name');\n    expect(result).toBeUndefined();\n  });\n});\n"
  },
  {
    "path": "packages/@core/base/shared/src/utils/__tests__/window.test.ts",
    "content": "import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';\n\nimport { openWindow } from '../window';\n\ndescribe('openWindow', () => {\n  // 保存原始的 window.open 函数\n  let originalOpen: typeof window.open;\n\n  beforeEach(() => {\n    originalOpen = window.open;\n  });\n\n  afterEach(() => {\n    window.open = originalOpen;\n  });\n\n  it('should call window.open with correct arguments', () => {\n    const url = 'https://example.com';\n    const options = { noopener: true, noreferrer: true, target: '_blank' };\n\n    window.open = vi.fn();\n\n    // 调用函数\n    openWindow(url, options);\n\n    // 验证 window.open 是否被正确地调用\n    expect(window.open).toHaveBeenCalledWith(\n      url,\n      options.target,\n      'noopener=yes,noreferrer=yes',\n    );\n  });\n});\n"
  },
  {
    "path": "packages/@core/base/shared/src/utils/cn.ts",
    "content": "import type { ClassValue } from 'clsx';\n\nimport { clsx } from 'clsx';\nimport { twMerge } from 'tailwind-merge';\n\nfunction cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}\n\nexport { cn };\n"
  },
  {
    "path": "packages/@core/base/shared/src/utils/date.ts",
    "content": "import dayjs from 'dayjs';\n\nexport function formatDate(time: number | string, format = 'YYYY-MM-DD') {\n  try {\n    const date = dayjs(time);\n    if (!date.isValid()) {\n      throw new Error('Invalid date');\n    }\n    return date.format(format);\n  } catch (error) {\n    console.error(`Error formatting date: ${error}`);\n    return time;\n  }\n}\n\nexport function formatDateTime(time: number | string) {\n  return formatDate(time, 'YYYY-MM-DD HH:mm:ss');\n}\n\nexport function isDate(value: any): value is Date {\n  return value instanceof Date;\n}\n\nexport function isDayjsObject(value: any): value is dayjs.Dayjs {\n  return dayjs.isDayjs(value);\n}\n"
  },
  {
    "path": "packages/@core/base/shared/src/utils/diff.ts",
    "content": "// type Diff<T = any> = T;\n\n// 比较两个数组是否相等\n\nfunction arraysEqual<T>(a: T[], b: T[]): boolean {\n  if (a.length !== b.length) return false;\n  const counter = new Map<T, number>();\n  for (const value of a) {\n    counter.set(value, (counter.get(value) || 0) + 1);\n  }\n  for (const value of b) {\n    const count = counter.get(value);\n    if (count === undefined || count === 0) {\n      return false;\n    }\n    counter.set(value, count - 1);\n  }\n  return true;\n}\n\n// 深度对比两个值\n// function deepEqual<T>(oldVal: T, newVal: T): boolean {\n//   if (\n//     typeof oldVal === 'object' &&\n//     oldVal !== null &&\n//     typeof newVal === 'object' &&\n//     newVal !== null\n//   ) {\n//     return Array.isArray(oldVal) && Array.isArray(newVal)\n//       ? arraysEqual(oldVal, newVal)\n//       : diff(oldVal as any, newVal as any) === null;\n//   } else {\n//     return oldVal === newVal;\n//   }\n// }\n\n// // diff 函数\n// function diff<T extends object>(\n//   oldObj: T,\n//   newObj: T,\n//   ignoreFields: (keyof T)[] = [],\n// ): { [K in keyof T]?: Diff<T[K]> } | null {\n//   const difference: { [K in keyof T]?: Diff<T[K]> } = {};\n\n//   for (const key in oldObj) {\n//     if (ignoreFields.includes(key)) continue;\n//     const oldValue = oldObj[key];\n//     const newValue = newObj[key];\n\n//     if (!deepEqual(oldValue, newValue)) {\n//       difference[key] = newValue;\n//     }\n//   }\n\n//   return Object.keys(difference).length === 0 ? null : difference;\n// }\n\ntype DiffResult<T> = Partial<{\n  [K in keyof T]: T[K] extends object ? DiffResult<T[K]> : T[K];\n}>;\n\nfunction diff<T extends Record<string, any>>(obj1: T, obj2: T): DiffResult<T> {\n  function findDifferences(o1: any, o2: any): any {\n    if (Array.isArray(o1) && Array.isArray(o2)) {\n      if (!arraysEqual(o1, o2)) {\n        return o2;\n      }\n      return undefined;\n    }\n\n    if (\n      typeof o1 === 'object' &&\n      typeof o2 === 'object' &&\n      o1 !== null &&\n      o2 !== null\n    ) {\n      const diffResult: any = {};\n\n      const keys = new Set([...Object.keys(o1), ...Object.keys(o2)]);\n      keys.forEach((key) => {\n        const valueDiff = findDifferences(o1[key], o2[key]);\n        if (valueDiff !== undefined) {\n          diffResult[key] = valueDiff;\n        }\n      });\n\n      return Object.keys(diffResult).length > 0 ? diffResult : undefined;\n    }\n\n    return o1 === o2 ? undefined : o2;\n  }\n\n  return findDifferences(obj1, obj2);\n}\n\nexport { arraysEqual, diff };\n"
  },
  {
    "path": "packages/@core/base/shared/src/utils/dom.ts",
    "content": "export interface VisibleDomRect {\n  bottom: number;\n  height: number;\n  left: number;\n  right: number;\n  top: number;\n  width: number;\n}\n\n/**\n * 获取元素可见信息\n * @param element\n */\nexport function getElementVisibleRect(\n  element?: HTMLElement | null | undefined,\n): VisibleDomRect {\n  if (!element) {\n    return {\n      bottom: 0,\n      height: 0,\n      left: 0,\n      right: 0,\n      top: 0,\n      width: 0,\n    };\n  }\n  const rect = element.getBoundingClientRect();\n  const viewHeight = Math.max(\n    document.documentElement.clientHeight,\n    window.innerHeight,\n  );\n\n  const top = Math.max(rect.top, 0);\n  const bottom = Math.min(rect.bottom, viewHeight);\n\n  const viewWidth = Math.max(\n    document.documentElement.clientWidth,\n    window.innerWidth,\n  );\n\n  const left = Math.max(rect.left, 0);\n  const right = Math.min(rect.right, viewWidth);\n\n  return {\n    bottom,\n    height: Math.max(0, bottom - top),\n    left,\n    right,\n    top,\n    width: Math.max(0, right - left),\n  };\n}\n\nexport function getScrollbarWidth() {\n  const scrollDiv = document.createElement('div');\n\n  scrollDiv.style.visibility = 'hidden';\n  scrollDiv.style.overflow = 'scroll';\n  scrollDiv.style.position = 'absolute';\n  scrollDiv.style.top = '-9999px';\n\n  document.body.append(scrollDiv);\n\n  const innerDiv = document.createElement('div');\n  scrollDiv.append(innerDiv);\n\n  const scrollbarWidth = scrollDiv.offsetWidth - innerDiv.offsetWidth;\n\n  scrollDiv.remove();\n  return scrollbarWidth;\n}\n\nexport function needsScrollbar() {\n  const doc = document.documentElement;\n  const body = document.body;\n\n  // 检查 body 的 overflow-y 样式\n  const overflowY = window.getComputedStyle(body).overflowY;\n\n  // 如果明确设置了需要滚动条的样式\n  if (overflowY === 'scroll' || overflowY === 'auto') {\n    return doc.scrollHeight > window.innerHeight;\n  }\n\n  // 在其他情况下，根据 scrollHeight 和 innerHeight 比较判断\n  return doc.scrollHeight > window.innerHeight;\n}\n\nexport function triggerWindowResize(): void {\n  // 创建一个新的 resize 事件\n  const resizeEvent = new Event('resize');\n\n  // 触发 window 的 resize 事件\n  window.dispatchEvent(resizeEvent);\n}\n"
  },
  {
    "path": "packages/@core/base/shared/src/utils/download.ts",
    "content": "import { openWindow } from './window';\n\ninterface DownloadOptions<T = string> {\n  fileName?: string;\n  source: T;\n  target?: string;\n}\n\nconst DEFAULT_FILENAME = 'downloaded_file';\n\n/**\n * 通过 URL 下载文件，支持跨域\n * @throws {Error} - 当下载失败时抛出错误\n */\nexport async function downloadFileFromUrl({\n  fileName,\n  source,\n  target = '_blank',\n}: DownloadOptions): Promise<void> {\n  if (!source || typeof source !== 'string') {\n    throw new Error('Invalid URL.');\n  }\n\n  const isChrome = window.navigator.userAgent.toLowerCase().includes('chrome');\n  const isSafari = window.navigator.userAgent.toLowerCase().includes('safari');\n\n  if (/iP/.test(window.navigator.userAgent)) {\n    console.error('Your browser does not support download!');\n    return;\n  }\n\n  if (isChrome || isSafari) {\n    triggerDownload(source, resolveFileName(source, fileName));\n    return;\n  }\n  if (!source.includes('?')) {\n    source += '?download';\n  }\n\n  openWindow(source, { target });\n}\n\n/**\n * 通过 Base64 下载文件\n */\nexport function downloadFileFromBase64({ fileName, source }: DownloadOptions) {\n  if (!source || typeof source !== 'string') {\n    throw new Error('Invalid Base64 data.');\n  }\n\n  const resolvedFileName = fileName || DEFAULT_FILENAME;\n  triggerDownload(source, resolvedFileName);\n}\n\n/**\n * 通过图片 URL 下载图片文件\n */\nexport async function downloadFileFromImageUrl({\n  fileName,\n  source,\n}: DownloadOptions) {\n  const base64 = await urlToBase64(source);\n  downloadFileFromBase64({ fileName, source: base64 });\n}\n\n/**\n * 通过 Blob 下载文件\n */\nexport function downloadFileFromBlob({\n  fileName = DEFAULT_FILENAME,\n  source,\n}: DownloadOptions<Blob>): void {\n  if (!(source instanceof Blob)) {\n    throw new TypeError('Invalid Blob data.');\n  }\n\n  const url = URL.createObjectURL(source);\n  triggerDownload(url, fileName);\n}\n\n/**\n * 下载文件，支持 Blob、字符串和其他 BlobPart 类型\n */\nexport function downloadFileFromBlobPart({\n  fileName = DEFAULT_FILENAME,\n  source,\n}: DownloadOptions<BlobPart>): void {\n  // 如果 data 不是 Blob，则转换为 Blob\n  const blob =\n    source instanceof Blob\n      ? source\n      : new Blob([source], { type: 'application/octet-stream' });\n\n  // 创建对象 URL 并触发下载\n  const url = URL.createObjectURL(blob);\n  triggerDownload(url, fileName);\n}\n\n/**\n * img url to base64\n * @param url\n */\nexport function urlToBase64(url: string, mineType?: string): Promise<string> {\n  return new Promise((resolve, reject) => {\n    let canvas = document.createElement('CANVAS') as HTMLCanvasElement | null;\n    const ctx = canvas?.getContext('2d');\n    const img = new Image();\n    img.crossOrigin = '';\n    img.addEventListener('load', () => {\n      if (!canvas || !ctx) {\n        return reject(new Error('Failed to create canvas.'));\n      }\n      canvas.height = img.height;\n      canvas.width = img.width;\n      ctx.drawImage(img, 0, 0);\n      const dataURL = canvas.toDataURL(mineType || 'image/png');\n      canvas = null;\n      resolve(dataURL);\n    });\n    img.src = url;\n  });\n}\n\n/**\n * 通用下载触发函数\n * @param href - 文件下载的 URL\n * @param fileName - 下载文件的名称，如果未提供则自动识别\n * @param revokeDelay - 清理 URL 的延迟时间 (毫秒)\n */\nexport function triggerDownload(\n  href: string,\n  fileName: string | undefined,\n  revokeDelay: number = 100,\n): void {\n  const defaultFileName = 'downloaded_file';\n  const finalFileName = fileName || defaultFileName;\n\n  const link = document.createElement('a');\n  link.href = href;\n  link.download = finalFileName;\n  link.style.display = 'none';\n\n  if (link.download === undefined) {\n    link.setAttribute('target', '_blank');\n  }\n\n  document.body.append(link);\n  link.click();\n  link.remove();\n\n  // 清理临时 URL 以释放内存\n  setTimeout(() => URL.revokeObjectURL(href), revokeDelay);\n}\n\nfunction resolveFileName(url: string, fileName?: string): string {\n  return fileName || url.slice(url.lastIndexOf('/') + 1) || DEFAULT_FILENAME;\n}\n"
  },
  {
    "path": "packages/@core/base/shared/src/utils/index.ts",
    "content": "export * from './cn';\nexport * from './date';\nexport * from './diff';\nexport * from './dom';\nexport * from './download';\nexport * from './inference';\nexport * from './letter';\nexport * from './merge';\nexport * from './nprogress';\nexport * from './resources';\nexport * from './state-handler';\nexport * from './to';\nexport * from './tree';\nexport * from './unique';\nexport * from './update-css-variables';\nexport * from './util';\nexport * from './window';\nexport { default as cloneDeep } from 'lodash.clonedeep';\nexport { default as get } from 'lodash.get';\nexport { default as isEqual } from 'lodash.isequal';\nexport { default as set } from 'lodash.set';\n"
  },
  {
    "path": "packages/@core/base/shared/src/utils/inference.ts",
    "content": "// eslint-disable-next-line vue/prefer-import-from-vue\nimport { isFunction, isObject, isString } from '@vue/shared';\n\n/**\n * 检查传入的值是否为undefined。\n *\n * @param {unknown} value 要检查的值。\n * @returns {boolean} 如果值是undefined，返回true，否则返回false。\n */\nfunction isUndefined(value?: unknown): value is undefined {\n  return value === undefined;\n}\n\n/**\n * 检查传入的值是否为boolean\n * @param value\n * @returns 如果值是布尔值，返回true，否则返回false。\n */\nfunction isBoolean(value: unknown): value is boolean {\n  return typeof value === 'boolean';\n}\n\n/**\n * 检查传入的值是否为空。\n *\n * 以下情况将被认为是空：\n * - 值为null。\n * - 值为undefined。\n * - 值为一个空字符串。\n * - 值为一个长度为0的数组。\n * - 值为一个没有元素的Map或Set。\n * - 值为一个没有属性的对象。\n *\n * @param {T} value 要检查的值。\n * @returns {boolean} 如果值为空，返回true，否则返回false。\n */\nfunction isEmpty<T = unknown>(value?: T): value is T {\n  if (value === null || value === undefined) {\n    return true;\n  }\n\n  if (Array.isArray(value) || isString(value)) {\n    return value.length === 0;\n  }\n\n  if (value instanceof Map || value instanceof Set) {\n    return value.size === 0;\n  }\n\n  if (isObject(value)) {\n    return Object.keys(value).length === 0;\n  }\n\n  return false;\n}\n\n/**\n * 检查传入的字符串是否为有效的HTTP或HTTPS URL。\n *\n * @param {string} url 要检查的字符串。\n * @return {boolean} 如果字符串是有效的HTTP或HTTPS URL，返回true，否则返回false。\n */\nfunction isHttpUrl(url?: string): boolean {\n  if (!url) {\n    return false;\n  }\n  // 使用正则表达式测试URL是否以http:// 或 https:// 开头\n  const httpRegex = /^https?:\\/\\/.*$/;\n  return httpRegex.test(url);\n}\n\n/**\n * 检查传入的值是否为window对象。\n *\n * @param {any} value 要检查的值。\n * @returns {boolean} 如果值是window对象，返回true，否则返回false。\n */\nfunction isWindow(value: any): value is Window {\n  return (\n    typeof window !== 'undefined' && value !== null && value === value.window\n  );\n}\n\n/**\n * 检查当前运行环境是否为Mac OS。\n *\n * 这个函数通过检查navigator.userAgent字符串来判断当前运行环境。\n * 如果userAgent字符串中包含\"macintosh\"或\"mac os x\"（不区分大小写），则认为当前环境是Mac OS。\n *\n * @returns {boolean} 如果当前环境是Mac OS，返回true，否则返回false。\n */\nfunction isMacOs(): boolean {\n  const macRegex = /macintosh|mac os x/i;\n  return macRegex.test(navigator.userAgent);\n}\n\n/**\n * 检查当前运行环境是否为Windows OS。\n *\n * 这个函数通过检查navigator.userAgent字符串来判断当前运行环境。\n * 如果userAgent字符串中包含\"windows\"或\"win32\"（不区分大小写），则认为当前环境是Windows OS。\n *\n * @returns {boolean} 如果当前环境是Windows OS，返回true，否则返回false。\n */\nfunction isWindowsOs(): boolean {\n  const windowsRegex = /windows|win32/i;\n  return windowsRegex.test(navigator.userAgent);\n}\n\n/**\n * 检查传入的值是否为数字\n * @param value\n */\nfunction isNumber(value: any): value is number {\n  return typeof value === 'number' && Number.isFinite(value);\n}\n\n/**\n * Returns the first value in the provided list that is neither `null` nor `undefined`.\n *\n * This function iterates over the input values and returns the first one that is\n * not strictly equal to `null` or `undefined`. If all values are either `null` or\n * `undefined`, it returns `undefined`.\n *\n * @template T - The type of the input values.\n * @param {...(T | null | undefined)[]} values - A list of values to evaluate.\n * @returns {T | undefined} - The first value that is not `null` or `undefined`, or `undefined` if none are found.\n *\n * @example\n * // Returns 42 because it is the first non-null, non-undefined value.\n * getFirstNonNullOrUndefined(undefined, null, 42, 'hello'); // 42\n *\n * @example\n * // Returns 'hello' because it is the first non-null, non-undefined value.\n * getFirstNonNullOrUndefined(null, undefined, 'hello', 123); // 'hello'\n *\n * @example\n * // Returns undefined because all values are either null or undefined.\n * getFirstNonNullOrUndefined(undefined, null); // undefined\n */\nfunction getFirstNonNullOrUndefined<T>(\n  ...values: (null | T | undefined)[]\n): T | undefined {\n  for (const value of values) {\n    if (value !== undefined && value !== null) {\n      return value;\n    }\n  }\n  return undefined;\n}\n\nexport {\n  getFirstNonNullOrUndefined,\n  isBoolean,\n  isEmpty,\n  isFunction,\n  isHttpUrl,\n  isMacOs,\n  isNumber,\n  isObject,\n  isString,\n  isUndefined,\n  isWindow,\n  isWindowsOs,\n};\n"
  },
  {
    "path": "packages/@core/base/shared/src/utils/letter.ts",
    "content": "/**\n * 将字符串的首字母大写\n * @param string\n */\nfunction capitalizeFirstLetter(string: string): string {\n  return string.charAt(0).toUpperCase() + string.slice(1);\n}\n\n/**\n * 将字符串的首字母转换为小写。\n *\n * @param str 要转换的字符串\n * @returns 首字母小写的字符串\n */\nfunction toLowerCaseFirstLetter(str: string): string {\n  if (!str) return str; // 如果字符串为空，直接返回\n  return str.charAt(0).toLowerCase() + str.slice(1);\n}\n\n/**\n *  生成驼峰命名法的键名\n * @param key\n * @param parentKey\n */\nfunction toCamelCase(key: string, parentKey: string): string {\n  if (!parentKey) {\n    return key;\n  }\n  return parentKey + key.charAt(0).toUpperCase() + key.slice(1);\n}\n\nfunction kebabToCamelCase(str: string): string {\n  return str\n    .split('-')\n    .filter(Boolean)\n    .map((word, index) =>\n      index === 0 ? word : word.charAt(0).toUpperCase() + word.slice(1),\n    )\n    .join('');\n}\n\nexport {\n  capitalizeFirstLetter,\n  kebabToCamelCase,\n  toCamelCase,\n  toLowerCaseFirstLetter,\n};\n"
  },
  {
    "path": "packages/@core/base/shared/src/utils/merge.ts",
    "content": "import { createDefu } from 'defu';\n\nexport { createDefu as createMerge, defu as merge } from 'defu';\n\nexport const mergeWithArrayOverride = createDefu((originObj, key, updates) => {\n  if (Array.isArray(originObj[key]) && Array.isArray(updates)) {\n    originObj[key] = updates;\n    return true;\n  }\n});\n"
  },
  {
    "path": "packages/@core/base/shared/src/utils/nprogress.ts",
    "content": "import type NProgress from 'nprogress';\n\n// 创建一个NProgress实例的变量，初始值为null\nlet nProgressInstance: null | typeof NProgress = null;\n\n/**\n * 动态加载NProgress库，并进行配置。\n * 此函数首先检查是否已经加载过NProgress库，如果已经加载过，则直接返回NProgress实例。\n * 否则，动态导入NProgress库，进行配置，然后返回NProgress实例。\n *\n * @returns  NProgress实例的Promise对象。\n */\nasync function loadNprogress() {\n  if (nProgressInstance) {\n    return nProgressInstance;\n  }\n  nProgressInstance = await import('nprogress');\n  nProgressInstance.configure({\n    showSpinner: true,\n    speed: 300,\n  });\n  return nProgressInstance;\n}\n\n/**\n * 开始显示进度条。\n * 此函数首先加载NProgress库，然后调用NProgress的start方法开始显示进度条。\n */\nasync function startProgress() {\n  const nprogress = await loadNprogress();\n  nprogress?.start();\n}\n\n/**\n * 停止显示进度条，并隐藏进度条。\n * 此函数首先加载NProgress库，然后调用NProgress的done方法停止并隐藏进度条。\n */\nasync function stopProgress() {\n  const nprogress = await loadNprogress();\n  nprogress?.done();\n}\n\nexport { startProgress, stopProgress };\n"
  },
  {
    "path": "packages/@core/base/shared/src/utils/resources.ts",
    "content": "/**\n * 加载js文件\n * @param src js文件地址\n */\nfunction loadScript(src: string) {\n  return new Promise<void>((resolve, reject) => {\n    if (document.querySelector(`script[src=\"${src}\"]`)) {\n      // 如果已经加载过，直接 resolve\n      return resolve();\n    }\n    const script = document.createElement('script');\n    script.src = src;\n    script.addEventListener('load', () => resolve());\n    script.addEventListener('error', () =>\n      reject(new Error(`Failed to load script: ${src}`)),\n    );\n    document.head.append(script);\n  });\n}\n\nexport { loadScript };\n"
  },
  {
    "path": "packages/@core/base/shared/src/utils/state-handler.ts",
    "content": "export class StateHandler {\n  private condition: boolean = false;\n  private rejectCondition: (() => void) | null = null;\n  private resolveCondition: (() => void) | null = null;\n\n  // 清理 resolve/reject 函数\n  private clearPromises() {\n    this.resolveCondition = null;\n    this.rejectCondition = null;\n  }\n\n  isConditionTrue(): boolean {\n    return this.condition;\n  }\n\n  reset() {\n    this.condition = false;\n    this.clearPromises();\n  }\n\n  // 触发状态为 false 时，reject\n  setConditionFalse() {\n    this.condition = false;\n    if (this.rejectCondition) {\n      this.rejectCondition();\n      this.clearPromises();\n    }\n  }\n\n  // 触发状态为 true 时，resolve\n  setConditionTrue() {\n    this.condition = true;\n    if (this.resolveCondition) {\n      this.resolveCondition();\n      this.clearPromises();\n    }\n  }\n\n  // 返回一个 Promise，等待 condition 变为 true\n  waitForCondition(): Promise<void> {\n    return new Promise((resolve, reject) => {\n      if (this.condition) {\n        resolve(); // 如果 condition 已经为 true，立即 resolve\n      } else {\n        this.resolveCondition = resolve;\n        this.rejectCondition = reject;\n      }\n    });\n  }\n}\n"
  },
  {
    "path": "packages/@core/base/shared/src/utils/to.ts",
    "content": "/**\n * @param { Readonly<Promise> } promise\n * @param {object=} errorExt - Additional Information you can pass to the err object\n * @return { Promise }\n */\nexport async function to<T, U = Error>(\n  promise: Readonly<Promise<T>>,\n  errorExt?: object,\n): Promise<[null, T] | [U, undefined]> {\n  try {\n    const data = await promise;\n    const result: [null, T] = [null, data];\n    return result;\n  } catch (error) {\n    if (errorExt) {\n      const parsedError = Object.assign({}, error, errorExt);\n      return [parsedError as U, undefined];\n    }\n    return [error as U, undefined];\n  }\n}\n"
  },
  {
    "path": "packages/@core/base/shared/src/utils/tree.ts",
    "content": "interface TreeConfigOptions {\n  // 子属性的名称，默认为'children'\n  childProps: string;\n}\n\n/**\n * @zh_CN 遍历树形结构，并返回所有节点中指定的值。\n * @param tree 树形结构数组\n * @param getValue 获取节点值的函数\n * @param options 作为子节点数组的可选属性名称。\n * @returns 所有节点中指定的值的数组\n */\nfunction traverseTreeValues<T, V>(\n  tree: T[],\n  getValue: (node: T) => V,\n  options?: TreeConfigOptions,\n): V[] {\n  const result: V[] = [];\n  const { childProps } = options || {\n    childProps: 'children',\n  };\n\n  const dfs = (treeNode: T) => {\n    const value = getValue(treeNode);\n    result.push(value);\n    const children = (treeNode as Record<string, any>)?.[childProps];\n    if (!children) {\n      return;\n    }\n    if (children.length > 0) {\n      for (const child of children) {\n        dfs(child);\n      }\n    }\n  };\n\n  for (const treeNode of tree) {\n    dfs(treeNode);\n  }\n  return result.filter(Boolean);\n}\n\n/**\n * 根据条件过滤给定树结构的节点，并以原有顺序返回所有匹配节点的数组。\n * @param tree 要过滤的树结构的根节点数组。\n * @param filter 用于匹配每个节点的条件。\n * @param options 作为子节点数组的可选属性名称。\n * @returns 包含所有匹配节点的数组。\n */\nfunction filterTree<T extends Record<string, any>>(\n  tree: T[],\n  filter: (node: T) => boolean,\n  options?: TreeConfigOptions,\n): T[] {\n  const { childProps } = options || {\n    childProps: 'children',\n  };\n\n  const _filterTree = (nodes: T[]): T[] => {\n    return nodes.filter((node: Record<string, any>) => {\n      if (filter(node as T)) {\n        if (node[childProps]) {\n          node[childProps] = _filterTree(node[childProps]);\n        }\n        return true;\n      }\n      return false;\n    });\n  };\n\n  return _filterTree(tree);\n}\n\n/**\n * 根据条件重新映射给定树结构的节\n * @param tree 要过滤的树结构的根节点数组。\n * @param mapper 用于map每个节点的条件。\n * @param options 作为子节点数组的可选属性名称。\n */\nfunction mapTree<T, V extends Record<string, any>>(\n  tree: T[],\n  mapper: (node: T) => V,\n  options?: TreeConfigOptions,\n): V[] {\n  const { childProps } = options || {\n    childProps: 'children',\n  };\n  return tree.map((node) => {\n    const mapperNode: Record<string, any> = mapper(node);\n    if (mapperNode[childProps]) {\n      mapperNode[childProps] = mapTree(mapperNode[childProps], mapper, options);\n    }\n    return mapperNode as V;\n  });\n}\n\nexport { filterTree, mapTree, traverseTreeValues };\n"
  },
  {
    "path": "packages/@core/base/shared/src/utils/unique.ts",
    "content": "/**\n * 根据指定字段对对象数组进行去重\n * @param arr 要去重的对象数组\n * @param key 去重依据的字段名\n * @returns 去重后的对象数组\n */\nfunction uniqueByField<T>(arr: T[], key: keyof T): T[] {\n  const seen = new Map<any, T>();\n  return arr.filter((item) => {\n    const value = item[key];\n    return seen.has(value) ? false : (seen.set(value, item), true);\n  });\n}\n\nexport { uniqueByField };\n"
  },
  {
    "path": "packages/@core/base/shared/src/utils/update-css-variables.ts",
    "content": "/**\n * 更新 CSS 变量的函数\n * @param variables 要更新的 CSS 变量与其新值的映射\n */\nfunction updateCSSVariables(\n  variables: { [key: string]: string },\n  id = '__vben-styles__',\n): void {\n  // 获取或创建内联样式表元素\n  const styleElement =\n    document.querySelector(`#${id}`) || document.createElement('style');\n\n  styleElement.id = id;\n\n  // 构建要更新的 CSS 变量的样式文本\n  let cssText = ':root {';\n  for (const key in variables) {\n    if (Object.prototype.hasOwnProperty.call(variables, key)) {\n      cssText += `${key}: ${variables[key]};`;\n    }\n  }\n  cssText += '}';\n\n  // 将样式文本赋值给内联样式表\n  styleElement.textContent = cssText;\n\n  // 将内联样式表添加到文档头部\n  if (!document.querySelector(`#${id}`)) {\n    setTimeout(() => {\n      document.head.append(styleElement);\n    });\n  }\n}\n\nexport { updateCSSVariables };\n"
  },
  {
    "path": "packages/@core/base/shared/src/utils/util.ts",
    "content": "export function bindMethods<T extends object>(instance: T): void {\n  const prototype = Object.getPrototypeOf(instance);\n  const propertyNames = Object.getOwnPropertyNames(prototype);\n\n  propertyNames.forEach((propertyName) => {\n    const descriptor = Object.getOwnPropertyDescriptor(prototype, propertyName);\n    const propertyValue = instance[propertyName as keyof T];\n\n    if (\n      typeof propertyValue === 'function' &&\n      propertyName !== 'constructor' &&\n      descriptor &&\n      !descriptor.get &&\n      !descriptor.set\n    ) {\n      instance[propertyName as keyof T] = propertyValue.bind(instance);\n    }\n  });\n}\n\n/**\n * 获取嵌套对象的字段值\n * @param obj - 要查找的对象\n * @param path - 用于查找字段的路径，使用小数点分隔\n * @returns 字段值，或者未找到时返回 undefined\n */\nexport function getNestedValue<T>(obj: T, path: string): any {\n  if (typeof path !== 'string' || path.length === 0) {\n    throw new Error('Path must be a non-empty string');\n  }\n  // 把路径字符串按 \".\" 分割成数组\n  const keys = path.split('.') as (number | string)[];\n\n  let current: any = obj;\n\n  for (const key of keys) {\n    if (current === null || current === undefined) {\n      return undefined;\n    }\n    current = current[key as keyof typeof current];\n  }\n\n  return current;\n}\n"
  },
  {
    "path": "packages/@core/base/shared/src/utils/window.ts",
    "content": "interface OpenWindowOptions {\n  noopener?: boolean;\n  noreferrer?: boolean;\n  target?: '_blank' | '_parent' | '_self' | '_top' | string;\n}\n\n/**\n * 新窗口打开URL。\n *\n * @param url - 需要打开的网址。\n * @param options - 打开窗口的选项。\n */\nfunction openWindow(url: string, options: OpenWindowOptions = {}): void {\n  // 解构并设置默认值\n  const { noopener = true, noreferrer = true, target = '_blank' } = options;\n\n  // 基于选项创建特性字符串\n  const features = [noopener && 'noopener=yes', noreferrer && 'noreferrer=yes']\n    .filter(Boolean)\n    .join(',');\n\n  // 打开窗口\n  window.open(url, target, features);\n}\n\n/**\n * 在新窗口中打开路由。\n * @param path\n */\nfunction openRouteInNewWindow(path: string) {\n  const { hash, origin } = location;\n  const fullPath = path.startsWith('/') ? path : `/${path}`;\n  const url = `${origin}${hash && !fullPath.startsWith('/#') ? '/#' : ''}${fullPath}`;\n  openWindow(url, { target: '_blank' });\n}\n\nexport { openRouteInNewWindow, openWindow };\n"
  },
  {
    "path": "packages/@core/base/shared/tsconfig.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/tsconfig\",\n  \"extends\": \"@vben/tsconfig/library.json\",\n  \"include\": [\"src\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "packages/@core/base/typings/build.config.ts",
    "content": "import { defineBuildConfig } from 'unbuild';\n\nexport default defineBuildConfig({\n  clean: true,\n  declaration: true,\n  entries: ['src/index'],\n});\n"
  },
  {
    "path": "packages/@core/base/typings/package.json",
    "content": "{\n  \"name\": \"@vben-core/typings\",\n  \"version\": \"5.5.9\",\n  \"homepage\": \"https://github.com/vbenjs/vue-vben-admin\",\n  \"bugs\": \"https://github.com/vbenjs/vue-vben-admin/issues\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/vbenjs/vue-vben-admin.git\",\n    \"directory\": \"packages/@vben-core/base/typings\"\n  },\n  \"license\": \"MIT\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"build\": \"pnpm unbuild\"\n  },\n  \"files\": [\n    \"dist\"\n  ],\n  \"main\": \"./dist/index.mjs\",\n  \"module\": \"./dist/index.mjs\",\n  \"types\": \"./dist/index.d.ts\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./src/index.ts\",\n      \"development\": \"./src/index.ts\",\n      \"default\": \"./dist/index.mjs\"\n    },\n    \"./vue-router\": {\n      \"types\": \"./vue-router.d.ts\"\n    }\n  },\n  \"publishConfig\": {\n    \"exports\": {\n      \".\": {\n        \"types\": \"./dist/index.d.ts\",\n        \"default\": \"./dist/index.mjs\"\n      }\n    }\n  },\n  \"dependencies\": {\n    \"vue\": \"catalog:\",\n    \"vue-router\": \"catalog:\"\n  }\n}\n"
  },
  {
    "path": "packages/@core/base/typings/src/app.d.ts",
    "content": "type LayoutType =\n  | 'full-content'\n  | 'header-mixed-nav'\n  | 'header-nav'\n  | 'header-sidebar-nav'\n  | 'mixed-nav'\n  | 'sidebar-mixed-nav'\n  | 'sidebar-nav';\n\ntype ThemeModeType = 'auto' | 'dark' | 'light';\n\n/**\n * 偏好设置按钮位置\n * fixed 固定在右侧\n * header 顶栏\n * auto 自动\n */\ntype PreferencesButtonPositionType = 'auto' | 'fixed' | 'header';\n\ntype BuiltinThemeType =\n  | 'custom'\n  | 'deep-blue'\n  | 'deep-green'\n  | 'default'\n  | 'gray'\n  | 'green'\n  | 'neutral'\n  | 'orange'\n  | 'pink'\n  | 'red'\n  | 'rose'\n  | 'sky-blue'\n  | 'slate'\n  | 'stone'\n  | 'violet'\n  | 'yellow'\n  | 'zinc'\n  | (Record<never, never> & string);\n\ntype ContentCompactType = 'compact' | 'wide';\n\ntype LayoutHeaderModeType = 'auto' | 'auto-scroll' | 'fixed' | 'static';\ntype LayoutHeaderMenuAlignType = 'center' | 'end' | 'start';\n\n/**\n * 登录过期模式\n * modal 弹窗模式\n * page 页面模式\n */\ntype LoginExpiredModeType = 'modal' | 'page';\n\n/**\n * 面包屑样式\n * background 背景\n * normal 默认\n */\ntype BreadcrumbStyleType = 'background' | 'normal';\n\n/**\n * 权限模式\n * backend 后端权限模式\n * frontend 前端权限模式\n * mixed 混合权限模式\n */\ntype AccessModeType = 'backend' | 'frontend' | 'mixed';\n\n/**\n * 导航风格\n * plain 朴素\n * rounded 圆润\n */\ntype NavigationStyleType = 'plain' | 'rounded';\n\n/**\n * 标签栏风格\n * brisk 轻快\n * card 卡片\n * chrome 谷歌\n * plain 朴素\n */\ntype TabsStyleType = 'brisk' | 'card' | 'chrome' | 'plain';\n\n/**\n * 页面切换动画\n */\ntype PageTransitionType = 'fade' | 'fade-down' | 'fade-slide' | 'fade-up';\n\n/**\n * 页面切换动画\n * panel-center 居中布局\n * panel-left 居左布局\n * panel-right 居右布局\n */\ntype AuthPageLayoutType = 'panel-center' | 'panel-left' | 'panel-right';\n\nexport type {\n  AccessModeType,\n  AuthPageLayoutType,\n  BreadcrumbStyleType,\n  BuiltinThemeType,\n  ContentCompactType,\n  LayoutHeaderMenuAlignType,\n  LayoutHeaderModeType,\n  LayoutType,\n  LoginExpiredModeType,\n  NavigationStyleType,\n  PageTransitionType,\n  PreferencesButtonPositionType,\n  TabsStyleType,\n  ThemeModeType,\n};\n"
  },
  {
    "path": "packages/@core/base/typings/src/basic.d.ts",
    "content": "interface BasicOption {\n  label: string;\n  value: string;\n}\n\ntype SelectOption = BasicOption;\n\ntype TabOption = BasicOption;\n\ninterface BasicUserInfo {\n  /**\n   * 头像\n   */\n  avatar: string;\n  /**\n   * 邮箱\n   */\n  email: string;\n  /**\n   * 用户权限\n   */\n  permissions: string[];\n  /**\n   * 用户昵称\n   */\n  realName: string;\n  /**\n   * 用户角色\n   */\n  roles: string[];\n  /**\n   * 用户id\n   */\n  userId: number | string;\n  /**\n   * 用户名\n   */\n  username: string;\n}\n\ntype ClassType = Array<object | string> | object | string;\n\nexport type { BasicOption, BasicUserInfo, ClassType, SelectOption, TabOption };\n"
  },
  {
    "path": "packages/@core/base/typings/src/helper.d.ts",
    "content": "import type { ComputedRef, MaybeRef } from 'vue';\n\n/**\n * 深层递归所有属性为可选\n */\ntype DeepPartial<T> = T extends object\n  ? {\n      [P in keyof T]?: DeepPartial<T[P]>;\n    }\n  : T;\n\n/**\n * 深层递归所有属性为只读\n */\ntype DeepReadonly<T> = {\n  readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P];\n};\n\n/**\n * 任意类型的异步函数\n */\n\ntype AnyPromiseFunction<T extends any[] = any[], R = void> = (\n  ...arg: T\n) => PromiseLike<R>;\n\n/**\n * 任意类型的普通函数\n */\ntype AnyNormalFunction<T extends any[] = any[], R = void> = (...arg: T) => R;\n\n/**\n * 任意类型的函数\n */\ntype AnyFunction<T extends any[] = any[], R = void> =\n  | AnyNormalFunction<T, R>\n  | AnyPromiseFunction<T, R>;\n\n/**\n *  T | null 包装\n */\ntype Nullable<T> = null | T;\n\n/**\n * T | Not null 包装\n */\ntype NonNullable<T> = T extends null | undefined ? never : T;\n\n/**\n * 字符串类型对象\n */\ntype Recordable<T> = Record<string, T>;\n\n/**\n * 字符串类型对象（只读）\n */\ninterface ReadonlyRecordable<T = any> {\n  readonly [key: string]: T;\n}\n\n/**\n * setTimeout 返回值类型\n */\ntype TimeoutHandle = ReturnType<typeof setTimeout>;\n\n/**\n * setInterval 返回值类型\n */\ntype IntervalHandle = ReturnType<typeof setInterval>;\n\n/**\n * 也许它是一个计算的 ref，或者一个 getter 函数\n *\n */\ntype MaybeReadonlyRef<T> = (() => T) | ComputedRef<T>;\n\n/**\n * 也许它是一个 ref，或者一个普通值，或者一个 getter 函数\n *\n */\ntype MaybeComputedRef<T> = MaybeReadonlyRef<T> | MaybeRef<T>;\n\ntype Merge<O extends object, T extends object> = {\n  [K in keyof O | keyof T]: K extends keyof T\n    ? T[K]\n    : K extends keyof O\n      ? O[K]\n      : never;\n};\n\n/**\n * T = [\n *  { name: string; age: number; },\n *  { sex: 'male' | 'female'; age: string }\n * ]\n * =>\n * MergeAll<T> = {\n *  name: string;\n *  sex: 'male' | 'female';\n *  age: string\n * }\n */\ntype MergeAll<\n  T extends object[],\n  R extends object = Record<string, any>,\n> = T extends [infer F extends object, ...infer Rest extends object[]]\n  ? MergeAll<Rest, Merge<R, F>>\n  : R;\n\ntype EmitType = (name: Name, ...args: any[]) => void;\n\ntype MaybePromise<T> = Promise<T> | T;\n\nexport type {\n  AnyFunction,\n  AnyNormalFunction,\n  AnyPromiseFunction,\n  DeepPartial,\n  DeepReadonly,\n  EmitType,\n  IntervalHandle,\n  MaybeComputedRef,\n  MaybePromise,\n  MaybeReadonlyRef,\n  Merge,\n  MergeAll,\n  NonNullable,\n  Nullable,\n  ReadonlyRecordable,\n  Recordable,\n  TimeoutHandle,\n};\n"
  },
  {
    "path": "packages/@core/base/typings/src/index.ts",
    "content": "export type * from './app';\nexport type * from './basic';\nexport type * from './helper';\nexport type * from './menu-record';\nexport type * from './tabs';\nexport type * from './vue-router';\n"
  },
  {
    "path": "packages/@core/base/typings/src/menu-record.ts",
    "content": "import type { Component } from 'vue';\nimport type { RouteRecordRaw } from 'vue-router';\n\n/**\n * 扩展路由原始对象\n */\ntype ExRouteRecordRaw = {\n  parent?: string;\n  parents?: string[];\n  path?: any;\n} & RouteRecordRaw;\n\ninterface MenuRecordBadgeRaw {\n  /**\n   * 徽标\n   */\n  badge?: string;\n  /**\n   * 徽标类型\n   */\n  badgeType?: 'dot' | 'normal';\n  /**\n   * 徽标颜色\n   */\n  badgeVariants?: 'destructive' | 'primary' | string;\n}\n\n/**\n * 菜单原始对象\n */\ninterface MenuRecordRaw extends MenuRecordBadgeRaw {\n  /**\n   * 激活时的图标名\n   */\n  activeIcon?: string;\n  /**\n   * 子菜单\n   */\n  children?: MenuRecordRaw[];\n  /**\n   * 是否禁用菜单\n   * @default false\n   */\n  disabled?: boolean;\n  /**\n   * 图标名\n   */\n  icon?: Component | string;\n  /**\n   * 菜单名\n   */\n  name: string;\n  /**\n   * 排序号\n   */\n  order?: number;\n  /**\n   * 父级路径\n   */\n  parent?: string;\n  /**\n   * 所有父级路径\n   */\n  parents?: string[];\n  /**\n   * 菜单路径，唯一，可当作key\n   */\n  path: string;\n  /**\n   * 是否显示菜单\n   * @default true\n   */\n  show?: boolean;\n}\n\nexport type { ExRouteRecordRaw, MenuRecordBadgeRaw, MenuRecordRaw };\n"
  },
  {
    "path": "packages/@core/base/typings/src/tabs.ts",
    "content": "import type { RouteLocationNormalized } from 'vue-router';\n\nexport interface TabDefinition extends RouteLocationNormalized {\n  /**\n   * 标签页的key\n   */\n  key?: string;\n}\n"
  },
  {
    "path": "packages/@core/base/typings/src/vue-router.d.ts",
    "content": "import type { Component } from 'vue';\nimport type { Router, RouteRecordRaw } from 'vue-router';\n\ninterface RouteMeta {\n  /**\n   * 激活图标（菜单/tab）\n   */\n  activeIcon?: string;\n  /**\n   * 当前激活的菜单，有时候不想激活现有菜单，需要激活父级菜单时使用\n   */\n  activePath?: string;\n  /**\n   * 是否固定标签页\n   * @default false\n   */\n  affixTab?: boolean;\n  /**\n   * 固定标签页的顺序\n   * @default 0\n   */\n  affixTabOrder?: number;\n  /**\n   * 需要特定的角色标识才可以访问\n   * @default []\n   */\n  authority?: string[];\n  /**\n   * 徽标\n   */\n  badge?: string;\n  /**\n   * 徽标类型\n   */\n  badgeType?: 'dot' | 'normal';\n  /**\n   * 徽标颜色\n   */\n  badgeVariants?:\n    | 'default'\n    | 'destructive'\n    | 'primary'\n    | 'success'\n    | 'warning'\n    | string;\n  /**\n   * 路由的完整路径作为key（默认true）\n   */\n  fullPathKey?: boolean;\n  /**\n   * 当前路由的子级在菜单中不展现\n   * @default false\n   */\n  hideChildrenInMenu?: boolean;\n  /**\n   * 当前路由在面包屑中不展现\n   * @default false\n   */\n  hideInBreadcrumb?: boolean;\n  /**\n   * 当前路由在菜单中不展现\n   * @default false\n   */\n  hideInMenu?: boolean;\n  /**\n   * 当前路由在标签页不展现\n   * @default false\n   */\n  hideInTab?: boolean;\n  /**\n   * 图标（菜单/tab）\n   */\n  icon?: Component | string;\n  /**\n   * iframe 地址\n   */\n  iframeSrc?: string;\n  /**\n   * 忽略权限，直接可以访问\n   * @default false\n   */\n  ignoreAccess?: boolean;\n  /**\n   * 开启KeepAlive缓存\n   */\n  keepAlive?: boolean;\n  /**\n   * 外链-跳转路径\n   */\n  link?: string;\n  /**\n   * 路由是否已经加载过\n   */\n  loaded?: boolean;\n  /**\n   * 标签页最大打开数量\n   * @default -1\n   */\n  maxNumOfOpenTab?: number;\n  /**\n   * 菜单可以看到，但是访问会被重定向到403\n   */\n  menuVisibleWithForbidden?: boolean;\n  /**\n   * 不使用基础布局（仅在顶级生效）\n   */\n  noBasicLayout?: boolean;\n  /**\n   * 在新窗口打开\n   */\n  openInNewWindow?: boolean;\n  /**\n   * 用于路由->菜单排序\n   */\n  order?: number;\n  /**\n   * 菜单所携带的参数\n   */\n  query?: Recordable;\n  /**\n   * 管理员切换租户 该页面是否需要重定向到首页\n   * 用于区分带路由参数的页面 比如/oss/:id 这种路由是需要回到首页的\n   * 默认false\n   */\n  requireHomeRedirect?: boolean;\n  /**\n   * 标题名称\n   */\n  title: string;\n}\n\n// 定义递归类型以将 RouteRecordRaw 的 component 属性更改为 string\ntype RouteRecordStringComponent<T = string> = Omit<\n  RouteRecordRaw,\n  'children' | 'component'\n> & {\n  children?: RouteRecordStringComponent<T>[];\n  component: T;\n};\n\ntype ComponentRecordType = Record<string, () => Promise<Component>>;\n\ninterface GenerateMenuAndRoutesOptions {\n  fetchMenuListAsync?: () => Promise<RouteRecordStringComponent[]>;\n  forbiddenComponent?: RouteRecordRaw['component'];\n  layoutMap?: ComponentRecordType;\n  pageMap?: ComponentRecordType;\n  roles?: string[];\n  router: Router;\n  routes: RouteRecordRaw[];\n}\n\nexport type {\n  ComponentRecordType,\n  GenerateMenuAndRoutesOptions,\n  RouteMeta,\n  RouteRecordRaw,\n  RouteRecordStringComponent,\n};\n"
  },
  {
    "path": "packages/@core/base/typings/tsconfig.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/tsconfig\",\n  \"extends\": \"@vben/tsconfig/library.json\",\n  \"include\": [\"src\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "packages/@core/base/typings/vue-router.d.ts",
    "content": "/* eslint-disable no-restricted-imports */\nimport type { RouteMeta as IRouteMeta } from '@vben-core/typings';\n\nimport 'vue-router';\n\ndeclare module 'vue-router' {\n  // eslint-disable-next-line @typescript-eslint/no-empty-object-type\n  interface RouteMeta extends IRouteMeta {}\n}\n"
  },
  {
    "path": "packages/@core/composables/build.config.ts",
    "content": "import { defineBuildConfig } from 'unbuild';\n\nexport default defineBuildConfig({\n  clean: true,\n  declaration: true,\n  entries: ['src/index'],\n});\n"
  },
  {
    "path": "packages/@core/composables/package.json",
    "content": "{\n  \"name\": \"@vben-core/composables\",\n  \"version\": \"5.5.9\",\n  \"homepage\": \"https://github.com/vbenjs/vue-vben-admin\",\n  \"bugs\": \"https://github.com/vbenjs/vue-vben-admin/issues\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/vbenjs/vue-vben-admin.git\",\n    \"directory\": \"packages/@core/composables\"\n  },\n  \"license\": \"MIT\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"build\": \"pnpm unbuild\"\n  },\n  \"files\": [\n    \"dist\"\n  ],\n  \"sideEffects\": false,\n  \"main\": \"./dist/index.mjs\",\n  \"module\": \"./dist/index.mjs\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./src/index.ts\",\n      \"development\": \"./src/index.ts\",\n      \"default\": \"./dist/index.mjs\"\n    }\n  },\n  \"publishConfig\": {\n    \"exports\": {\n      \".\": {\n        \"types\": \"./dist/index.d.ts\",\n        \"default\": \"./dist/index.mjs\"\n      }\n    }\n  },\n  \"dependencies\": {\n    \"@vben-core/shared\": \"workspace:*\",\n    \"@vueuse/core\": \"catalog:\",\n    \"radix-vue\": \"catalog:\",\n    \"sortablejs\": \"catalog:\",\n    \"vue\": \"catalog:\"\n  },\n  \"devDependencies\": {\n    \"@types/sortablejs\": \"catalog:\"\n  }\n}\n"
  },
  {
    "path": "packages/@core/composables/src/__tests__/use-sortable.test.ts",
    "content": "import type { SortableOptions } from 'sortablejs';\n\nimport { beforeEach, describe, expect, it, vi } from 'vitest';\n\nimport { useSortable } from '../use-sortable';\n\ndescribe('useSortable', () => {\n  beforeEach(() => {\n    vi.mock('sortablejs/modular/sortable.complete.esm.js', () => ({\n      default: {\n        create: vi.fn(),\n      },\n    }));\n  });\n  it('should call Sortable.create with the correct options', async () => {\n    // Create a mock element\n    const mockElement = document.createElement('div') as HTMLDivElement;\n\n    // Define custom options\n    const customOptions: SortableOptions = {\n      group: 'test-group',\n      sort: false,\n    };\n\n    // Use the useSortable function\n    const { initializeSortable } = useSortable(mockElement, customOptions);\n\n    // Initialize sortable\n    await initializeSortable();\n\n    // Import sortablejs to access the mocked create function\n    const Sortable = await import(\n      'sortablejs/modular/sortable.complete.esm.js'\n    );\n\n    // Verify that Sortable.create was called with the correct parameters\n    expect(Sortable.default.create).toHaveBeenCalledTimes(1);\n    expect(Sortable.default.create).toHaveBeenCalledWith(\n      mockElement,\n      expect.objectContaining({\n        animation: 300,\n        delay: 400,\n        delayOnTouchOnly: true,\n        ...customOptions,\n      }),\n    );\n  });\n});\n"
  },
  {
    "path": "packages/@core/composables/src/index.ts",
    "content": "export * from './use-is-mobile';\nexport * from './use-layout-style';\nexport * from './use-namespace';\nexport * from './use-priority-value';\nexport * from './use-scroll-lock';\nexport * from './use-simple-locale';\nexport * from './use-sortable';\nexport {\n  useEmitAsProps,\n  useForwardExpose,\n  useForwardProps,\n  useForwardPropsEmits,\n} from 'radix-vue';\n"
  },
  {
    "path": "packages/@core/composables/src/use-is-mobile.ts",
    "content": "import { breakpointsTailwind, useBreakpoints } from '@vueuse/core';\n\nexport function useIsMobile() {\n  const breakpoints = useBreakpoints(breakpointsTailwind);\n  const isMobile = breakpoints.smaller('md');\n  return { isMobile };\n}\n"
  },
  {
    "path": "packages/@core/composables/src/use-layout-style.ts",
    "content": "import type { VisibleDomRect } from '@vben-core/shared/utils';\nimport type { CSSProperties } from 'vue';\n\nimport {\n  CSS_VARIABLE_LAYOUT_CONTENT_HEIGHT,\n  CSS_VARIABLE_LAYOUT_CONTENT_WIDTH,\n  CSS_VARIABLE_LAYOUT_FOOTER_HEIGHT,\n  CSS_VARIABLE_LAYOUT_HEADER_HEIGHT,\n} from '@vben-core/shared/constants';\nimport { getElementVisibleRect } from '@vben-core/shared/utils';\nimport { useCssVar, useDebounceFn } from '@vueuse/core';\nimport { computed, onMounted, onUnmounted, ref } from 'vue';\n\n/**\n * @zh_CN content style\n */\nexport function useLayoutContentStyle() {\n  let resizeObserver: null | ResizeObserver = null;\n  const contentElement = ref<HTMLDivElement | null>(null);\n  const visibleDomRect = ref<null | VisibleDomRect>(null);\n  const contentHeight = useCssVar(CSS_VARIABLE_LAYOUT_CONTENT_HEIGHT);\n  const contentWidth = useCssVar(CSS_VARIABLE_LAYOUT_CONTENT_WIDTH);\n\n  const overlayStyle = computed((): CSSProperties => {\n    const { height, left, top, width } = visibleDomRect.value ?? {};\n    return {\n      height: `${height}px`,\n      left: `${left}px`,\n      position: 'fixed',\n      top: `${top}px`,\n      width: `${width}px`,\n      zIndex: 150,\n    };\n  });\n\n  const debouncedCalcHeight = useDebounceFn(\n    (_entries: ResizeObserverEntry[]) => {\n      visibleDomRect.value = getElementVisibleRect(contentElement.value);\n      contentHeight.value = `${visibleDomRect.value.height}px`;\n      contentWidth.value = `${visibleDomRect.value.width}px`;\n    },\n    16,\n  );\n\n  onMounted(() => {\n    if (contentElement.value && !resizeObserver) {\n      resizeObserver = new ResizeObserver(debouncedCalcHeight);\n      resizeObserver.observe(contentElement.value);\n    }\n  });\n\n  onUnmounted(() => {\n    resizeObserver?.disconnect();\n    resizeObserver = null;\n  });\n\n  return { contentElement, overlayStyle, visibleDomRect };\n}\n\nexport function useLayoutHeaderStyle() {\n  const headerHeight = useCssVar(CSS_VARIABLE_LAYOUT_HEADER_HEIGHT);\n\n  return {\n    getLayoutHeaderHeight: () => {\n      return Number.parseInt(`${headerHeight.value}`, 10);\n    },\n    setLayoutHeaderHeight: (height: number) => {\n      headerHeight.value = `${height}px`;\n    },\n  };\n}\n\nexport function useLayoutFooterStyle() {\n  const footerHeight = useCssVar(CSS_VARIABLE_LAYOUT_FOOTER_HEIGHT);\n\n  return {\n    getLayoutFooterHeight: () => {\n      return Number.parseInt(`${footerHeight.value}`, 10);\n    },\n    setLayoutFooterHeight: (height: number) => {\n      footerHeight.value = `${height}px`;\n    },\n  };\n}\n"
  },
  {
    "path": "packages/@core/composables/src/use-namespace.ts",
    "content": "import { DEFAULT_NAMESPACE } from '@vben-core/shared/constants';\n\n/**\n * @see copy https://github.com/element-plus/element-plus/blob/dev/packages/hooks/use-namespace/index.ts\n */\n\nconst statePrefix = 'is-';\n\nconst _bem = (\n  namespace: string,\n  block: string,\n  blockSuffix: string,\n  element: string,\n  modifier: string,\n) => {\n  let cls = `${namespace}-${block}`;\n  if (blockSuffix) {\n    cls += `-${blockSuffix}`;\n  }\n  if (element) {\n    cls += `__${element}`;\n  }\n  if (modifier) {\n    cls += `--${modifier}`;\n  }\n  return cls;\n};\n\nconst is: {\n  (name: string): string;\n  // eslint-disable-next-line @typescript-eslint/unified-signatures\n  (name: string, state: boolean | undefined): string;\n} = (name: string, ...args: [] | [boolean | undefined]) => {\n  const state = args.length > 0 ? args[0] : true;\n  return name && state ? `${statePrefix}${name}` : '';\n};\n\nconst useNamespace = (block: string) => {\n  const namespace = DEFAULT_NAMESPACE;\n  const b = (blockSuffix = '') => _bem(namespace, block, blockSuffix, '', '');\n  const e = (element?: string) =>\n    element ? _bem(namespace, block, '', element, '') : '';\n  const m = (modifier?: string) =>\n    modifier ? _bem(namespace, block, '', '', modifier) : '';\n  const be = (blockSuffix?: string, element?: string) =>\n    blockSuffix && element\n      ? _bem(namespace, block, blockSuffix, element, '')\n      : '';\n  const em = (element?: string, modifier?: string) =>\n    element && modifier ? _bem(namespace, block, '', element, modifier) : '';\n  const bm = (blockSuffix?: string, modifier?: string) =>\n    blockSuffix && modifier\n      ? _bem(namespace, block, blockSuffix, '', modifier)\n      : '';\n  const bem = (blockSuffix?: string, element?: string, modifier?: string) =>\n    blockSuffix && element && modifier\n      ? _bem(namespace, block, blockSuffix, element, modifier)\n      : '';\n\n  // for css var\n  // --el-xxx: value;\n  const cssVar = (object: Record<string, string>) => {\n    const styles: Record<string, string> = {};\n    for (const key in object) {\n      if (object[key]) {\n        styles[`--${namespace}-${key}`] = object[key];\n      }\n    }\n    return styles;\n  };\n  // with block\n  const cssVarBlock = (object: Record<string, string>) => {\n    const styles: Record<string, string> = {};\n    for (const key in object) {\n      if (object[key]) {\n        styles[`--${namespace}-${block}-${key}`] = object[key];\n      }\n    }\n    return styles;\n  };\n\n  const cssVarName = (name: string) => `--${namespace}-${name}`;\n  const cssVarBlockName = (name: string) => `--${namespace}-${block}-${name}`;\n\n  return {\n    b,\n    be,\n    bem,\n    bm,\n    // css\n    cssVar,\n    cssVarBlock,\n    cssVarBlockName,\n    cssVarName,\n    e,\n    em,\n    is,\n    m,\n    namespace,\n  };\n};\n\ntype UseNamespaceReturn = ReturnType<typeof useNamespace>;\n\nexport type { UseNamespaceReturn };\nexport { useNamespace };\n"
  },
  {
    "path": "packages/@core/composables/src/use-priority-value.ts",
    "content": "import type { ComputedRef, Ref } from 'vue';\n\nimport {\n  getFirstNonNullOrUndefined,\n  kebabToCamelCase,\n} from '@vben-core/shared/utils';\nimport { computed, getCurrentInstance, unref, useAttrs, useSlots } from 'vue';\n\n/**\n * 依次从插槽、attrs、props、state 中获取值\n * @param key\n * @param props\n * @param state\n */\nexport function usePriorityValue<\n  T extends Record<string, any>,\n  S extends Record<string, any>,\n  K extends keyof T = keyof T,\n>(key: K, props: T, state: Readonly<Ref<NoInfer<S>>> | undefined) {\n  const instance = getCurrentInstance();\n  const slots = useSlots();\n  const attrs = useAttrs() as T;\n\n  const value = computed((): T[K] => {\n    // props不管有没有传，都会有默认值，会影响这里的顺序，\n    // 通过判断原始props是否有值来判断是否传入\n    const rawProps = (instance?.vnode?.props || {}) as T;\n\n    const standardRawProps = {} as T;\n\n    for (const [key, value] of Object.entries(rawProps)) {\n      standardRawProps[kebabToCamelCase(key) as K] = value;\n    }\n    const propsKey =\n      standardRawProps?.[key] === undefined ? undefined : props[key];\n\n    // slot可以关闭\n    return getFirstNonNullOrUndefined(\n      slots[key as string],\n      attrs[key],\n      propsKey,\n      state?.value?.[key as keyof S],\n    ) as T[K];\n  });\n\n  return value;\n}\n\n/**\n * 批量获取state中的值（每个值都是ref）\n * @param props\n * @param state\n */\nexport function usePriorityValues<\n  T extends Record<string, any>,\n  S extends Ref<Record<string, any>> = Readonly<Ref<NoInfer<T>, NoInfer<T>>>,\n>(props: T, state: S | undefined) {\n  const result: { [K in keyof T]: ComputedRef<T[K]> } = {} as never;\n\n  (Object.keys(props) as (keyof T)[]).forEach((key) => {\n    result[key] = usePriorityValue(key as keyof typeof props, props, state);\n  });\n\n  return result;\n}\n\n/**\n * 批量获取state中的值（集中在一个computed，用于透传）\n * @param props\n * @param state\n */\nexport function useForwardPriorityValues<\n  T extends Record<string, any>,\n  S extends Ref<Record<string, any>> = Readonly<Ref<NoInfer<T>, NoInfer<T>>>,\n>(props: T, state: S | undefined) {\n  const computedResult: { [K in keyof T]: ComputedRef<T[K]> } = {} as never;\n\n  (Object.keys(props) as (keyof T)[]).forEach((key) => {\n    computedResult[key] = usePriorityValue(\n      key as keyof typeof props,\n      props,\n      state,\n    );\n  });\n\n  return computed(() => {\n    const unwrapResult: Record<string, any> = {};\n    Object.keys(props).forEach((key) => {\n      unwrapResult[key] = unref(computedResult[key]);\n    });\n    return unwrapResult as { [K in keyof T]: T[K] };\n  });\n}\n"
  },
  {
    "path": "packages/@core/composables/src/use-scroll-lock.ts",
    "content": "import { getScrollbarWidth, needsScrollbar } from '@vben-core/shared/utils';\n\nimport {\n  useScrollLock as _useScrollLock,\n  tryOnBeforeUnmount,\n  tryOnMounted,\n} from '@vueuse/core';\n\nexport const SCROLL_FIXED_CLASS = `_scroll__fixed_`;\n\nexport function useScrollLock() {\n  const isLocked = _useScrollLock(document.body);\n  const scrollbarWidth = getScrollbarWidth();\n\n  tryOnMounted(() => {\n    if (!needsScrollbar()) {\n      return;\n    }\n    document.body.style.paddingRight = `${scrollbarWidth}px`;\n\n    const layoutFixedNodes = document.querySelectorAll<HTMLElement>(\n      `.${SCROLL_FIXED_CLASS}`,\n    );\n    const nodes = [...layoutFixedNodes];\n    if (nodes.length > 0) {\n      nodes.forEach((node) => {\n        node.dataset.transition = node.style.transition;\n        node.style.transition = 'none';\n        node.style.paddingRight = `${scrollbarWidth}px`;\n      });\n    }\n    isLocked.value = true;\n  });\n\n  tryOnBeforeUnmount(() => {\n    if (!needsScrollbar()) {\n      return;\n    }\n    isLocked.value = false;\n    const layoutFixedNodes = document.querySelectorAll<HTMLElement>(\n      `.${SCROLL_FIXED_CLASS}`,\n    );\n    const nodes = [...layoutFixedNodes];\n    if (nodes.length > 0) {\n      nodes.forEach((node) => {\n        node.style.paddingRight = '';\n        requestAnimationFrame(() => {\n          node.style.transition = node.dataset.transition || '';\n        });\n      });\n    }\n    document.body.style.paddingRight = '';\n  });\n}\n"
  },
  {
    "path": "packages/@core/composables/src/use-simple-locale/README.md",
    "content": "# Simple i18n\n\nSimple i18 implementation\n"
  },
  {
    "path": "packages/@core/composables/src/use-simple-locale/index.ts",
    "content": "import type { Locale } from './messages';\n\nimport { createSharedComposable } from '@vueuse/core';\nimport { computed, ref } from 'vue';\n\nimport { getMessages } from './messages';\n\nexport const useSimpleLocale = createSharedComposable(() => {\n  const currentLocale = ref<Locale>('zh-CN');\n\n  const setSimpleLocale = (locale: Locale) => {\n    currentLocale.value = locale;\n  };\n\n  const $t = computed(() => {\n    const localeMessages = getMessages(currentLocale.value);\n    return (key: string) => {\n      return localeMessages[key] || key;\n    };\n  });\n  return {\n    $t,\n    currentLocale,\n    setSimpleLocale,\n  };\n});\n"
  },
  {
    "path": "packages/@core/composables/src/use-simple-locale/messages.ts",
    "content": "export type Locale = 'en-US' | 'zh-CN';\n\nexport const messages: Record<Locale, Record<string, string>> = {\n  'en-US': {\n    cancel: 'Cancel',\n    collapse: 'Collapse',\n    confirm: 'Confirm',\n    expand: 'Expand',\n    prompt: 'Prompt',\n    reset: 'Reset',\n    submit: 'Submit',\n  },\n  'zh-CN': {\n    cancel: '取消',\n    collapse: '收起',\n    confirm: '确认',\n    expand: '展开',\n    prompt: '提示',\n    reset: '重置',\n    submit: '提交',\n  },\n};\n\nexport const getMessages = (locale: Locale) => messages[locale];\n"
  },
  {
    "path": "packages/@core/composables/src/use-sortable.ts",
    "content": "import type { SortableOptions } from 'sortablejs';\nimport type Sortable from 'sortablejs';\n\nfunction useSortable<T extends HTMLElement>(\n  sortableContainer: T,\n  options: SortableOptions = {},\n) {\n  const initializeSortable = async () => {\n    const Sortable = await import(\n      // @ts-expect-error - This is a dynamic import\n      'sortablejs/modular/sortable.complete.esm.js'\n    );\n    const sortable = Sortable?.default?.create?.(sortableContainer, {\n      animation: 300,\n      delay: 400,\n      delayOnTouchOnly: true,\n      ...options,\n    });\n    return sortable as Sortable;\n  };\n\n  return {\n    initializeSortable,\n  };\n}\n\nexport { useSortable };\n\nexport type { Sortable };\n"
  },
  {
    "path": "packages/@core/composables/tsconfig.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/tsconfig\",\n  \"extends\": \"@vben/tsconfig/library.json\",\n  \"include\": [\"src\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "packages/@core/preferences/__tests__/__snapshots__/config.test.ts.snap",
    "content": "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html\n\nexports[`defaultPreferences immutability test > should not modify the config object 1`] = `\n{\n  \"app\": {\n    \"accessMode\": \"frontend\",\n    \"authPageLayout\": \"panel-right\",\n    \"checkUpdatesInterval\": 1,\n    \"colorGrayMode\": false,\n    \"colorWeakMode\": false,\n    \"compact\": false,\n    \"contentCompact\": \"wide\",\n    \"contentCompactWidth\": 1200,\n    \"contentPadding\": 0,\n    \"contentPaddingBottom\": 0,\n    \"contentPaddingLeft\": 0,\n    \"contentPaddingRight\": 0,\n    \"contentPaddingTop\": 0,\n    \"defaultAvatar\": \"https://unpkg.com/@vbenjs/static-source@0.1.7/source/avatar-v1.webp\",\n    \"defaultHomePath\": \"/analytics\",\n    \"dynamicTitle\": true,\n    \"enableCheckUpdates\": true,\n    \"enablePreferences\": true,\n    \"enableRefreshToken\": false,\n    \"isMobile\": false,\n    \"layout\": \"sidebar-nav\",\n    \"locale\": \"zh-CN\",\n    \"loginExpiredMode\": \"page\",\n    \"name\": \"Vben Admin\",\n    \"preferencesButtonPosition\": \"auto\",\n    \"watermark\": false,\n    \"zIndex\": 200,\n  },\n  \"breadcrumb\": {\n    \"enable\": true,\n    \"hideOnlyOne\": false,\n    \"showHome\": false,\n    \"showIcon\": true,\n    \"styleType\": \"normal\",\n  },\n  \"copyright\": {\n    \"companyName\": \"Vben\",\n    \"companySiteLink\": \"https://www.vben.pro\",\n    \"date\": \"2024\",\n    \"enable\": true,\n    \"icp\": \"\",\n    \"icpLink\": \"\",\n    \"settingShow\": true,\n  },\n  \"footer\": {\n    \"enable\": false,\n    \"fixed\": false,\n    \"height\": 32,\n  },\n  \"header\": {\n    \"enable\": true,\n    \"height\": 50,\n    \"hidden\": false,\n    \"menuAlign\": \"start\",\n    \"mode\": \"fixed\",\n  },\n  \"logo\": {\n    \"enable\": true,\n    \"fit\": \"contain\",\n    \"source\": \"https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp\",\n  },\n  \"navigation\": {\n    \"accordion\": true,\n    \"split\": true,\n    \"styleType\": \"rounded\",\n  },\n  \"shortcutKeys\": {\n    \"enable\": true,\n    \"globalLockScreen\": true,\n    \"globalLogout\": true,\n    \"globalPreferences\": true,\n    \"globalSearch\": true,\n  },\n  \"sidebar\": {\n    \"autoActivateChild\": false,\n    \"collapseWidth\": 60,\n    \"collapsed\": false,\n    \"collapsedButton\": true,\n    \"collapsedShowTitle\": false,\n    \"enable\": true,\n    \"expandOnHover\": true,\n    \"extraCollapse\": false,\n    \"extraCollapsedWidth\": 60,\n    \"fixedButton\": true,\n    \"hidden\": false,\n    \"mixedWidth\": 80,\n    \"width\": 224,\n  },\n  \"tabbar\": {\n    \"draggable\": true,\n    \"enable\": true,\n    \"height\": 38,\n    \"keepAlive\": true,\n    \"maxCount\": 0,\n    \"middleClickToClose\": false,\n    \"persist\": true,\n    \"showIcon\": true,\n    \"showMaximize\": true,\n    \"showMore\": true,\n    \"styleType\": \"chrome\",\n    \"wheelable\": true,\n  },\n  \"theme\": {\n    \"builtinType\": \"default\",\n    \"colorDestructive\": \"hsl(348 100% 61%)\",\n    \"colorPrimary\": \"hsl(215 100% 54%)\",\n    \"colorSuccess\": \"hsl(144 57% 58%)\",\n    \"colorWarning\": \"hsl(42 84% 61%)\",\n    \"mode\": \"dark\",\n    \"radius\": \"0.5\",\n    \"semiDarkHeader\": false,\n    \"semiDarkSidebar\": false,\n  },\n  \"transition\": {\n    \"enable\": true,\n    \"loading\": true,\n    \"name\": \"fade-slide\",\n    \"progress\": true,\n  },\n  \"widget\": {\n    \"fullscreen\": true,\n    \"globalSearch\": true,\n    \"languageToggle\": true,\n    \"lockScreen\": true,\n    \"notification\": true,\n    \"refresh\": true,\n    \"sidebarToggle\": true,\n    \"themeToggle\": true,\n  },\n}\n`;\n"
  },
  {
    "path": "packages/@core/preferences/__tests__/config.test.ts",
    "content": "import { describe, expect, it } from 'vitest';\n\nimport { defaultPreferences } from '../src/config';\n\ndescribe('defaultPreferences immutability test', () => {\n  // 创建快照，确保默认配置对象不被修改\n  it('should not modify the config object', () => {\n    expect(defaultPreferences).toMatchSnapshot();\n  });\n});\n"
  },
  {
    "path": "packages/@core/preferences/__tests__/preferences.test.ts",
    "content": "import { beforeEach, describe, expect, it, vi } from 'vitest';\n\nimport { defaultPreferences } from '../src/config';\nimport { PreferenceManager } from '../src/preferences';\nimport { isDarkTheme } from '../src/update-css-variables';\n\ndescribe('preferences', () => {\n  let preferenceManager: PreferenceManager;\n\n  // 模拟 window.matchMedia 方法\n  vi.stubGlobal(\n    'matchMedia',\n    vi.fn().mockImplementation((query) => ({\n      addEventListener: vi.fn(),\n      addListener: vi.fn(), // Deprecated\n      dispatchEvent: vi.fn(),\n      matches: query === '(prefers-color-scheme: dark)',\n      media: query,\n      onchange: null,\n      removeEventListener: vi.fn(),\n      removeListener: vi.fn(), // Deprecated\n    })),\n  );\n  beforeEach(() => {\n    preferenceManager = new PreferenceManager();\n  });\n\n  it('loads default preferences if no saved preferences found', () => {\n    const preferences = preferenceManager.getPreferences();\n    expect(preferences).toEqual(defaultPreferences);\n  });\n\n  it('initializes preferences with overrides', async () => {\n    const overrides: any = {\n      app: {\n        locale: 'en-US',\n      },\n    };\n    await preferenceManager.initPreferences({\n      namespace: 'testNamespace',\n      overrides,\n    });\n\n    // 等待防抖动操作完成\n    // await new Promise((resolve) => setTimeout(resolve, 300)); // 等待100毫秒\n\n    const expected = {\n      ...defaultPreferences,\n      app: {\n        ...defaultPreferences.app,\n        ...overrides.app,\n      },\n    };\n\n    expect(preferenceManager.getPreferences()).toEqual(expected);\n  });\n\n  it('updates theme mode correctly', () => {\n    preferenceManager.updatePreferences({\n      theme: {\n        mode: 'light',\n      },\n    });\n\n    expect(preferenceManager.getPreferences().theme.mode).toBe('light');\n  });\n\n  it('updates color modes correctly', () => {\n    preferenceManager.updatePreferences({\n      app: { colorGrayMode: true, colorWeakMode: true },\n    });\n\n    expect(preferenceManager.getPreferences().app.colorGrayMode).toBe(true);\n    expect(preferenceManager.getPreferences().app.colorWeakMode).toBe(true);\n  });\n\n  it('resets preferences to default', () => {\n    // 先更新一些偏好设置\n    preferenceManager.updatePreferences({\n      theme: {\n        mode: 'light',\n      },\n    });\n\n    // 然后重置偏好设置\n    preferenceManager.resetPreferences();\n\n    expect(preferenceManager.getPreferences()).toEqual(defaultPreferences);\n  });\n\n  it('updates isMobile correctly', () => {\n    // 模拟移动端状态\n    vi.stubGlobal(\n      'matchMedia',\n      vi.fn().mockImplementation((query) => ({\n        addEventListener: vi.fn(),\n        addListener: vi.fn(),\n        dispatchEvent: vi.fn(),\n        matches: query === '(max-width: 768px)',\n        media: query,\n        onchange: null,\n        removeEventListener: vi.fn(),\n        removeListener: vi.fn(),\n      })),\n    );\n\n    preferenceManager.updatePreferences({\n      app: { isMobile: true },\n    });\n\n    expect(preferenceManager.getPreferences().app.isMobile).toBe(true);\n  });\n\n  it('updates the locale preference correctly', () => {\n    preferenceManager.updatePreferences({\n      app: { locale: 'en-US' },\n    });\n\n    expect(preferenceManager.getPreferences().app.locale).toBe('en-US');\n  });\n\n  it('updates the sidebar width correctly', () => {\n    preferenceManager.updatePreferences({\n      sidebar: { width: 200 },\n    });\n\n    expect(preferenceManager.getPreferences().sidebar.width).toBe(200);\n  });\n  it('updates the sidebar collapse state correctly', () => {\n    preferenceManager.updatePreferences({\n      sidebar: { collapsed: true },\n    });\n\n    expect(preferenceManager.getPreferences().sidebar.collapsed).toBe(true);\n  });\n  it('updates the navigation style type correctly', () => {\n    preferenceManager.updatePreferences({\n      navigation: { styleType: 'flat' },\n    } as any);\n\n    expect(preferenceManager.getPreferences().navigation.styleType).toBe(\n      'flat',\n    );\n  });\n\n  it('resets preferences to default correctly', () => {\n    // 先更新一些偏好设置\n    preferenceManager.updatePreferences({\n      app: { locale: 'en-US' },\n      sidebar: { collapsed: true, width: 200 },\n      theme: {\n        mode: 'light',\n      },\n    });\n\n    // 然后重置偏好设置\n    preferenceManager.resetPreferences();\n\n    expect(preferenceManager.getPreferences()).toEqual(defaultPreferences);\n  });\n\n  it('does not update undefined preferences', () => {\n    const originalPreferences = preferenceManager.getPreferences();\n\n    preferenceManager.updatePreferences({\n      app: { nonexistentField: 'value' },\n    } as any);\n\n    expect(preferenceManager.getPreferences()).toEqual(originalPreferences);\n  });\n\n  it('reverts to default when a preference field is deleted', () => {\n    preferenceManager.updatePreferences({\n      app: { locale: 'en-US' },\n    });\n\n    preferenceManager.updatePreferences({\n      app: { locale: undefined },\n    });\n\n    expect(preferenceManager.getPreferences().app.locale).toBe('en-US');\n  });\n\n  it('ignores updates with invalid preference value types', () => {\n    const originalPreferences = preferenceManager.getPreferences();\n\n    preferenceManager.updatePreferences({\n      app: { isMobile: 'true' as unknown as boolean }, // 错误类型\n    });\n\n    expect(preferenceManager.getPreferences()).toEqual(originalPreferences);\n  });\n\n  it('merges nested preference objects correctly', () => {\n    preferenceManager.updatePreferences({\n      app: { name: 'New App Name' },\n    });\n\n    const expected = {\n      ...defaultPreferences,\n      app: {\n        ...defaultPreferences.app,\n        name: 'New App Name',\n      },\n    };\n\n    expect(preferenceManager.getPreferences()).toEqual(expected);\n  });\n\n  it('applies updates immediately after initialization', async () => {\n    const overrides: any = {\n      app: {\n        locale: 'en-US',\n      },\n    };\n\n    await preferenceManager.initPreferences(overrides);\n\n    preferenceManager.updatePreferences({\n      theme: { mode: 'light' },\n    });\n\n    expect(preferenceManager.getPreferences().theme.mode).toBe('light');\n  });\n});\n\ndescribe('isDarkTheme', () => {\n  it('should return true for dark theme', () => {\n    expect(isDarkTheme('dark')).toBe(true);\n  });\n\n  it('should return false for light theme', () => {\n    expect(isDarkTheme('light')).toBe(false);\n  });\n\n  it('should return system preference for auto theme', () => {\n    vi.spyOn(window, 'matchMedia').mockImplementation((query) => ({\n      addEventListener: vi.fn(),\n      addListener: vi.fn(), // Deprecated\n      dispatchEvent: vi.fn(),\n      matches: query === '(prefers-color-scheme: dark)',\n      media: query,\n      onchange: null,\n      removeEventListener: vi.fn(),\n      removeListener: vi.fn(), // Deprecated\n    }));\n\n    expect(isDarkTheme('auto')).toBe(true);\n    expect(window.matchMedia).toHaveBeenCalledWith(\n      '(prefers-color-scheme: dark)',\n    );\n  });\n});\n"
  },
  {
    "path": "packages/@core/preferences/build.config.ts",
    "content": "import { defineBuildConfig } from 'unbuild';\n\nexport default defineBuildConfig({\n  clean: true,\n  declaration: true,\n  entries: ['src/index'],\n});\n"
  },
  {
    "path": "packages/@core/preferences/package.json",
    "content": "{\n  \"name\": \"@vben-core/preferences\",\n  \"version\": \"5.5.9\",\n  \"homepage\": \"https://github.com/vbenjs/vue-vben-admin\",\n  \"bugs\": \"https://github.com/vbenjs/vue-vben-admin/issues\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/vbenjs/vue-vben-admin.git\",\n    \"directory\": \"packages/@core/preferences\"\n  },\n  \"license\": \"MIT\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"#build\": \"pnpm unbuild\"\n  },\n  \"files\": [\n    \"dist\",\n    \"src\"\n  ],\n  \"sideEffects\": [\n    \"**/*.css\"\n  ],\n  \"exports\": {\n    \".\": {\n      \"types\": \"./src/index.ts\",\n      \"development\": \"./src/index.ts\",\n      \"default\": \"./src/index.ts\",\n      \"#default\": \"./dist/index.mjs\"\n    }\n  },\n  \"dependencies\": {\n    \"@vben-core/shared\": \"workspace:*\",\n    \"@vben-core/typings\": \"workspace:*\",\n    \"@vueuse/core\": \"catalog:\",\n    \"vue\": \"catalog:\"\n  }\n}\n"
  },
  {
    "path": "packages/@core/preferences/src/config.ts",
    "content": "import type { Preferences } from './types';\n\nconst defaultPreferences: Preferences = {\n  app: {\n    accessMode: 'frontend',\n    authPageLayout: 'panel-right',\n    checkUpdatesInterval: 1,\n    colorGrayMode: false,\n    colorWeakMode: false,\n    compact: false,\n    contentCompact: 'wide',\n    contentCompactWidth: 1200,\n    contentPadding: 0,\n    contentPaddingBottom: 0,\n    contentPaddingLeft: 0,\n    contentPaddingRight: 0,\n    contentPaddingTop: 0,\n    defaultAvatar:\n      'https://unpkg.com/@vbenjs/static-source@0.1.7/source/avatar-v1.webp',\n    defaultHomePath: '/analytics',\n    dynamicTitle: true,\n    enableCheckUpdates: true,\n    enablePreferences: true,\n    enableRefreshToken: false,\n    enableStickyPreferencesNavigationBar: true,\n    isMobile: false,\n    layout: 'sidebar-nav',\n    locale: 'zh-CN',\n    loginExpiredMode: 'page',\n    name: 'RuoYI AI Admin',\n    preferencesButtonPosition: 'auto',\n    watermark: false,\n    watermarkContent: '',\n    zIndex: 200,\n  },\n  breadcrumb: {\n    enable: true,\n    hideOnlyOne: false,\n    showHome: false,\n    showIcon: true,\n    styleType: 'normal',\n  },\n  copyright: {\n    companyName: 'ruoyi-ai',\n    companySiteLink: 'https://gitee.com/ageerle/ruoyi-ai',\n    date: '2026',\n    enable: true,\n    icp: '',\n    icpLink: '',\n    settingShow: true,\n  },\n  footer: {\n    enable: false,\n    fixed: false,\n    height: 32,\n  },\n  header: {\n    enable: true,\n    height: 50,\n    hidden: false,\n    menuAlign: 'start',\n    mode: 'fixed',\n  },\n\n  logo: {\n    enable: true,\n    fit: 'contain',\n    source: 'https://ruoyiai-1254149996.cos.ap-guangzhou.myqcloud.com/2026/03/15/4b7e93a72bf04805ae59985cc0845ef1.png',\n  },\n  navigation: {\n    accordion: true,\n    split: true,\n    styleType: 'rounded',\n  },\n  shortcutKeys: {\n    enable: true,\n    globalLockScreen: true,\n    globalLogout: true,\n    globalPreferences: true,\n    globalSearch: true,\n  },\n  sidebar: {\n    autoActivateChild: false,\n    collapsed: false,\n    collapsedButton: true,\n    collapsedShowTitle: false,\n    collapseWidth: 60,\n    enable: true,\n    expandOnHover: true,\n    extraCollapse: false,\n    extraCollapsedWidth: 60,\n    fixedButton: true,\n    hidden: false,\n    mixedWidth: 80,\n    width: 224,\n  },\n  tabbar: {\n    draggable: true,\n    enable: true,\n    height: 38,\n    keepAlive: true,\n    maxCount: 0,\n    middleClickToClose: false,\n    persist: true,\n    showIcon: true,\n    showMaximize: true,\n    showMore: true,\n    styleType: 'chrome',\n    wheelable: true,\n  },\n  theme: {\n    builtinType: 'default',\n    colorDestructive: 'hsl(348 100% 61%)',\n    colorPrimary: 'hsl(215 100% 54%)',\n    colorSuccess: 'hsl(144 57% 58%)',\n    colorWarning: 'hsl(42 84% 61%)',\n    mode: 'auto',\n    radius: '0.5',\n    semiDarkHeader: false,\n    semiDarkSidebar: false,\n  },\n  transition: {\n    enable: true,\n    loading: true,\n    name: 'fade-slide',\n    progress: true,\n  },\n  widget: {\n    fullscreen: true,\n    globalSearch: true,\n    languageToggle: true,\n    lockScreen: false,\n    notification: true,\n    refresh: true,\n    sidebarToggle: true,\n    themeToggle: true,\n  },\n};\n\nexport { defaultPreferences };\n"
  },
  {
    "path": "packages/@core/preferences/src/constants.ts",
    "content": "import type { BuiltinThemeType } from '@vben-core/typings';\n\ninterface BuiltinThemePreset {\n  color: string;\n  darkPrimaryColor?: string;\n  primaryColor?: string;\n  type: BuiltinThemeType;\n}\n\nconst BUILT_IN_THEME_PRESETS: BuiltinThemePreset[] = [\n  {\n    color: 'hsl(215 100% 54%)',\n    type: 'default',\n  },\n  {\n    color: 'hsl(245 82% 67%)',\n    type: 'violet',\n  },\n  {\n    color: 'hsl(347 77% 60%)',\n    type: 'pink',\n  },\n  {\n    color: 'hsl(42 84% 61%)',\n    type: 'yellow',\n  },\n  {\n    color: 'hsl(231 98% 65%)',\n    type: 'sky-blue',\n  },\n  {\n    color: 'hsl(161 90% 43%)',\n    type: 'green',\n  },\n  {\n    color: 'hsl(240 5% 26%)',\n    darkPrimaryColor: 'hsl(0 0% 98%)',\n    primaryColor: 'hsl(240 5.9% 10%)',\n    type: 'zinc',\n  },\n\n  {\n    color: 'hsl(181 84% 32%)',\n    type: 'deep-green',\n  },\n\n  {\n    color: 'hsl(211 91% 39%)',\n    type: 'deep-blue',\n  },\n  {\n    color: 'hsl(18 89% 40%)',\n    type: 'orange',\n  },\n  {\n    color: 'hsl(0 75% 42%)',\n    type: 'rose',\n  },\n\n  {\n    color: 'hsl(0 0% 25%)',\n    darkPrimaryColor: 'hsl(0 0% 98%)',\n    primaryColor: 'hsl(240 5.9% 10%)',\n    type: 'neutral',\n  },\n  {\n    color: 'hsl(215 25% 27%)',\n    darkPrimaryColor: 'hsl(0 0% 98%)',\n    primaryColor: 'hsl(240 5.9% 10%)',\n    type: 'slate',\n  },\n  {\n    color: 'hsl(217 19% 27%)',\n    darkPrimaryColor: 'hsl(0 0% 98%)',\n    primaryColor: 'hsl(240 5.9% 10%)',\n    type: 'gray',\n  },\n  {\n    color: '',\n    type: 'custom',\n  },\n];\n\nexport const COLOR_PRESETS = [...BUILT_IN_THEME_PRESETS].slice(0, 7);\n\nexport { BUILT_IN_THEME_PRESETS };\n\nexport type { BuiltinThemePreset };\n"
  },
  {
    "path": "packages/@core/preferences/src/index.ts",
    "content": "import type { Preferences } from './types';\n\nimport { preferencesManager } from './preferences';\n\n// 偏好设置（带有层级关系）\nconst preferences: Preferences =\n  preferencesManager.getPreferences.apply(preferencesManager);\n\n// 更新偏好设置\nconst updatePreferences =\n  preferencesManager.updatePreferences.bind(preferencesManager);\n\n// 重置偏好设置\nconst resetPreferences =\n  preferencesManager.resetPreferences.bind(preferencesManager);\n\nconst clearPreferencesCache =\n  preferencesManager.clearCache.bind(preferencesManager);\n\n// 初始化偏好设置\nconst initPreferences =\n  preferencesManager.initPreferences.bind(preferencesManager);\n\nexport {\n  clearPreferencesCache,\n  initPreferences,\n  preferences,\n  preferencesManager,\n  resetPreferences,\n  updatePreferences,\n};\n\nexport * from './constants';\nexport type * from './types';\nexport * from './use-preferences';\n"
  },
  {
    "path": "packages/@core/preferences/src/preferences.ts",
    "content": "import type { DeepPartial } from '@vben-core/typings';\n\nimport type { InitialOptions, Preferences } from './types';\n\nimport { markRaw, reactive, readonly, watch } from 'vue';\n\nimport { StorageManager } from '@vben-core/shared/cache';\nimport { isMacOs, merge } from '@vben-core/shared/utils';\n\nimport {\n  breakpointsTailwind,\n  useBreakpoints,\n  useDebounceFn,\n} from '@vueuse/core';\n\nimport { defaultPreferences } from './config';\nimport { updateCSSVariables } from './update-css-variables';\n\nconst STORAGE_KEY = 'preferences';\nconst STORAGE_KEY_LOCALE = `${STORAGE_KEY}-locale`;\nconst STORAGE_KEY_THEME = `${STORAGE_KEY}-theme`;\n\nclass PreferenceManager {\n  private cache: null | StorageManager = null;\n  // private flattenedState: Flatten<Preferences>;\n  private initialPreferences: Preferences = defaultPreferences;\n  private isInitialized: boolean = false;\n  private savePreferences: (preference: Preferences) => void;\n  private state: Preferences = reactive<Preferences>({\n    ...this.loadPreferences(),\n  });\n  constructor() {\n    this.cache = new StorageManager();\n\n    // 避免频繁的操作缓存\n    this.savePreferences = useDebounceFn(\n      (preference: Preferences) => this._savePreferences(preference),\n      150,\n    );\n  }\n\n  clearCache() {\n    [STORAGE_KEY, STORAGE_KEY_LOCALE, STORAGE_KEY_THEME].forEach((key) => {\n      this.cache?.removeItem(key);\n    });\n  }\n\n  public getInitialPreferences() {\n    return this.initialPreferences;\n  }\n\n  public getPreferences() {\n    return readonly(this.state);\n  }\n\n  /**\n   * 覆盖偏好设置\n   * overrides  要覆盖的偏好设置\n   * namespace  命名空间\n   */\n  public async initPreferences({ namespace, overrides }: InitialOptions) {\n    // 是否初始化过\n    if (this.isInitialized) {\n      return;\n    }\n    // 初始化存储管理器\n    this.cache = new StorageManager({ prefix: namespace });\n    // 合并初始偏好设置\n    this.initialPreferences = merge({}, overrides, defaultPreferences);\n\n    // 加载并合并当前存储的偏好设置\n    const mergedPreference = merge(\n      {},\n      // overrides,\n      this.loadCachedPreferences() || {},\n      this.initialPreferences,\n    );\n\n    // 更新偏好设置\n    this.updatePreferences(mergedPreference);\n\n    this.setupWatcher();\n\n    this.initPlatform();\n    // 标记为已初始化\n    this.isInitialized = true;\n  }\n\n  /**\n   * 重置偏好设置\n   * 偏好设置将被重置为初始值，并从 localStorage 中移除。\n   *\n   * @example\n   * 假设 initialPreferences 为 { theme: 'light', language: 'en' }\n   * 当前 state 为 { theme: 'dark', language: 'fr' }\n   * this.resetPreferences();\n   * 调用后，state 将被重置为 { theme: 'light', language: 'en' }\n   * 并且 localStorage 中的对应项将被移除\n   */\n  resetPreferences() {\n    // 将状态重置为初始偏好设置\n    Object.assign(this.state, this.initialPreferences);\n    // 保存重置后的偏好设置\n    this.savePreferences(this.state);\n    // 从存储中移除偏好设置项\n    [STORAGE_KEY, STORAGE_KEY_THEME, STORAGE_KEY_LOCALE].forEach((key) => {\n      this.cache?.removeItem(key);\n    });\n    this.updatePreferences(this.state);\n  }\n\n  /**\n   * 更新偏好设置\n   * @param updates - 要更新的偏好设置\n   */\n  public updatePreferences(updates: DeepPartial<Preferences>) {\n    const mergedState = merge({}, updates, markRaw(this.state));\n\n    Object.assign(this.state, mergedState);\n\n    // 根据更新的键值执行相应的操作\n    this.handleUpdates(updates);\n    this.savePreferences(this.state);\n  }\n\n  /**\n   * 保存偏好设置\n   * @param {Preferences} preference - 需要保存的偏好设置\n   */\n  private _savePreferences(preference: Preferences) {\n    this.cache?.setItem(STORAGE_KEY, preference);\n    this.cache?.setItem(STORAGE_KEY_LOCALE, preference.app.locale);\n    this.cache?.setItem(STORAGE_KEY_THEME, preference.theme.mode);\n  }\n\n  /**\n   * 处理更新的键值\n   * 根据更新的键值执行相应的操作。\n   * @param {DeepPartial<Preferences>} updates - 部分更新的偏好设置\n   */\n  private handleUpdates(updates: DeepPartial<Preferences>) {\n    const themeUpdates = updates.theme || {};\n    const appUpdates = updates.app || {};\n    if (themeUpdates && Object.keys(themeUpdates).length > 0) {\n      updateCSSVariables(this.state);\n    }\n\n    if (\n      Reflect.has(appUpdates, 'colorGrayMode') ||\n      Reflect.has(appUpdates, 'colorWeakMode')\n    ) {\n      this.updateColorMode(this.state);\n    }\n  }\n\n  private initPlatform() {\n    const dom = document.documentElement;\n    dom.dataset.platform = isMacOs() ? 'macOs' : 'window';\n  }\n\n  /**\n   *  从缓存中加载偏好设置。如果缓存中没有找到对应的偏好设置，则返回默认偏好设置。\n   */\n  private loadCachedPreferences() {\n    return this.cache?.getItem<Preferences>(STORAGE_KEY);\n  }\n\n  /**\n   * 加载偏好设置\n   * @returns {Preferences} 加载的偏好设置\n   */\n  private loadPreferences(): Preferences {\n    return this.loadCachedPreferences() || { ...defaultPreferences };\n  }\n\n  /**\n   * 监听状态和系统偏好设置的变化。\n   */\n  private setupWatcher() {\n    if (this.isInitialized) {\n      return;\n    }\n\n    // 监听断点，判断是否移动端\n    const breakpoints = useBreakpoints(breakpointsTailwind);\n    const isMobile = breakpoints.smaller('md');\n    watch(\n      () => isMobile.value,\n      (val) => {\n        this.updatePreferences({\n          app: { isMobile: val },\n        });\n      },\n      { immediate: true },\n    );\n\n    // 监听系统主题偏好设置变化\n    window\n      .matchMedia('(prefers-color-scheme: dark)')\n      .addEventListener('change', ({ matches: isDark }) => {\n        // 如果偏好设置中主题模式为auto，则跟随系统更新\n        if (this.state.theme.mode === 'auto') {\n          this.updatePreferences({\n            theme: { mode: isDark ? 'dark' : 'light' },\n          });\n          // 恢复为auto模式\n          this.updatePreferences({\n            theme: { mode: 'auto' },\n          });\n        }\n      });\n  }\n\n  /**\n   * 更新页面颜色模式（灰色、色弱）\n   * @param preference\n   */\n  private updateColorMode(preference: Preferences) {\n    if (preference.app) {\n      const { colorGrayMode, colorWeakMode } = preference.app;\n      const dom = document.documentElement;\n      const COLOR_WEAK = 'invert-mode';\n      const COLOR_GRAY = 'grayscale-mode';\n      colorWeakMode\n        ? dom.classList.add(COLOR_WEAK)\n        : dom.classList.remove(COLOR_WEAK);\n      colorGrayMode\n        ? dom.classList.add(COLOR_GRAY)\n        : dom.classList.remove(COLOR_GRAY);\n    }\n  }\n}\n\nconst preferencesManager = new PreferenceManager();\nexport { PreferenceManager, preferencesManager };\n"
  },
  {
    "path": "packages/@core/preferences/src/types.ts",
    "content": "import type {\n  AccessModeType,\n  AuthPageLayoutType,\n  BreadcrumbStyleType,\n  BuiltinThemeType,\n  ContentCompactType,\n  DeepPartial,\n  LayoutHeaderMenuAlignType,\n  LayoutHeaderModeType,\n  LayoutType,\n  LoginExpiredModeType,\n  NavigationStyleType,\n  PageTransitionType,\n  PreferencesButtonPositionType,\n  TabsStyleType,\n  ThemeModeType,\n} from '@vben-core/typings';\n\ntype SupportedLanguagesType = 'en-US' | 'zh-CN';\n\ninterface AppPreferences {\n  /** 权限模式 */\n  accessMode: AccessModeType;\n  /** 登录注册页面布局 */\n  authPageLayout: AuthPageLayoutType;\n  /** 检查更新轮询时间 */\n  checkUpdatesInterval: number;\n  /** 是否开启灰色模式 */\n  colorGrayMode: boolean;\n  /** 是否开启色弱模式 */\n  colorWeakMode: boolean;\n  /** 是否开启紧凑模式 */\n  compact: boolean;\n  /** 是否开启内容紧凑模式 */\n  contentCompact: ContentCompactType;\n  /** 内容紧凑宽度 */\n  contentCompactWidth: number;\n  /** 内容内边距 */\n  contentPadding: number;\n  /** 内容底部内边距 */\n  contentPaddingBottom: number;\n  /** 内容左侧内边距 */\n  contentPaddingLeft: number;\n  /** 内容右侧内边距 */\n  contentPaddingRight: number;\n  /** 内容顶部内边距 */\n  contentPaddingTop: number;\n  // /** 应用默认头像 */\n  defaultAvatar: string;\n  /** 默认首页地址 */\n  defaultHomePath: string;\n  // /** 开启动态标题 */\n  dynamicTitle: boolean;\n  /** 是否开启检查更新 */\n  enableCheckUpdates: boolean;\n  /** 是否显示偏好设置 */\n  enablePreferences: boolean;\n  /**\n   * @zh_CN 是否开启refreshToken\n   */\n  enableRefreshToken: boolean;\n  /**\n   * @zh_CN 是否开启首选项导航栏吸顶效果\n   */\n  enableStickyPreferencesNavigationBar: boolean;\n  /** 是否移动端 */\n  isMobile: boolean;\n  /** 布局方式 */\n  layout: LayoutType;\n  /** 支持的语言 */\n  locale: SupportedLanguagesType;\n  /** 登录过期模式 */\n  loginExpiredMode: LoginExpiredModeType;\n  /** 应用名 */\n  name: string;\n  /** 偏好设置按钮位置 */\n  preferencesButtonPosition: PreferencesButtonPositionType;\n  /**\n   * @zh_CN 是否开启水印\n   */\n  watermark: boolean;\n  /**\n   * @zh_CN 水印文案\n   */\n  watermarkContent: string;\n  /** z-index */\n  zIndex: number;\n}\n\ninterface BreadcrumbPreferences {\n  /** 面包屑是否启用 */\n  enable: boolean;\n  /** 面包屑是否只有一个时隐藏 */\n  hideOnlyOne: boolean;\n  /** 面包屑首页图标是否可见 */\n  showHome: boolean;\n  /** 面包屑图标是否可见 */\n  showIcon: boolean;\n  /** 面包屑风格 */\n  styleType: BreadcrumbStyleType;\n}\n\ninterface CopyrightPreferences {\n  /** 版权公司名 */\n  companyName: string;\n  /** 版权公司名链接 */\n  companySiteLink: string;\n  /** 版权日期 */\n  date: string;\n  /** 版权是否可见 */\n  enable: boolean;\n  /** 备案号 */\n  icp: string;\n  /** 备案号链接 */\n  icpLink: string;\n  /** 设置面板是否显示*/\n  settingShow?: boolean;\n}\n\ninterface FooterPreferences {\n  /** 底栏是否可见 */\n  enable: boolean;\n  /** 底栏是否固定 */\n  fixed: boolean;\n  /** 底栏高度 */\n  height: number;\n}\n\ninterface HeaderPreferences {\n  /** 顶栏是否启用 */\n  enable: boolean;\n  /** 顶栏高度 */\n  height: number;\n  /** 顶栏是否隐藏,css-隐藏 */\n  hidden: boolean;\n  /** 顶栏菜单位置 */\n  menuAlign: LayoutHeaderMenuAlignType;\n  /** header显示模式 */\n  mode: LayoutHeaderModeType;\n}\n\ninterface LogoPreferences {\n  /** logo是否可见 */\n  enable: boolean;\n  /** logo图片适应方式 */\n  fit: 'contain' | 'cover' | 'fill' | 'none' | 'scale-down';\n  /** logo地址 */\n  source: string;\n}\n\ninterface NavigationPreferences {\n  /** 导航菜单手风琴模式 */\n  accordion: boolean;\n  /** 导航菜单是否切割，只在 layout=mixed-nav 生效 */\n  split: boolean;\n  /** 导航菜单风格 */\n  styleType: NavigationStyleType;\n}\n\ninterface SidebarPreferences {\n  /** 点击目录时自动激活子菜单   */\n  autoActivateChild: boolean;\n  /** 侧边栏是否折叠 */\n  collapsed: boolean;\n  /** 侧边栏折叠按钮是否可见 */\n  collapsedButton: boolean;\n  /** 侧边栏折叠时，是否显示title */\n  collapsedShowTitle: boolean;\n  /** 侧边栏折叠宽度 */\n  collapseWidth: number;\n  /** 侧边栏是否可见 */\n  enable: boolean;\n  /** 菜单自动展开状态 */\n  expandOnHover: boolean;\n  /** 侧边栏扩展区域是否折叠 */\n  extraCollapse: boolean;\n  /** 侧边栏扩展区域折叠宽度 */\n  extraCollapsedWidth: number;\n  /** 侧边栏固定按钮是否可见 */\n  fixedButton: boolean;\n  /** 侧边栏是否隐藏 - css */\n  hidden: boolean;\n  /** 混合侧边栏宽度 */\n  mixedWidth: number;\n  /** 侧边栏宽度 */\n  width: number;\n}\n\ninterface ShortcutKeyPreferences {\n  /** 是否启用快捷键-全局 */\n  enable: boolean;\n  /** 是否启用全局锁屏快捷键 */\n  globalLockScreen: boolean;\n  /** 是否启用全局注销快捷键 */\n  globalLogout: boolean;\n  /** 是否启用全局偏好设置快捷键 */\n  globalPreferences: boolean;\n  /** 是否启用全局搜索快捷键 */\n  globalSearch: boolean;\n}\n\ninterface TabbarPreferences {\n  /** 是否开启多标签页拖拽 */\n  draggable: boolean;\n  /** 是否开启多标签页 */\n  enable: boolean;\n  /** 标签页高度 */\n  height: number;\n  /** 开启标签页缓存功能 */\n  keepAlive: boolean;\n  /** 限制最大数量 */\n  maxCount: number;\n  /** 是否点击中键时关闭标签 */\n  middleClickToClose: boolean;\n  /** 是否持久化标签 */\n  persist: boolean;\n  /** 是否开启多标签页图标 */\n  showIcon: boolean;\n  /** 显示最大化按钮 */\n  showMaximize: boolean;\n  /** 显示更多按钮 */\n  showMore: boolean;\n  /** 标签页风格 */\n  styleType: TabsStyleType;\n  /** 是否开启鼠标滚轮响应 */\n  wheelable: boolean;\n}\n\ninterface ThemePreferences {\n  /** 内置主题名 */\n  builtinType: BuiltinThemeType;\n  /** 错误色 */\n  colorDestructive: string;\n  /** 主题色 */\n  colorPrimary: string;\n  /** 成功色 */\n  colorSuccess: string;\n  /** 警告色 */\n  colorWarning: string;\n  /** 当前主题 */\n  mode: ThemeModeType;\n  /** 圆角 */\n  radius: string;\n  /** 是否开启半深色header（只在theme='light'时生效） */\n  semiDarkHeader: boolean;\n  /** 是否开启半深色菜单（只在theme='light'时生效） */\n  semiDarkSidebar: boolean;\n}\n\ninterface TransitionPreferences {\n  /** 页面切换动画是否启用 */\n  enable: boolean;\n  // /** 是否开启页面加载loading */\n  loading: boolean;\n  /** 页面切换动画 */\n  name: PageTransitionType | string;\n  /** 是否开启页面加载进度动画 */\n  progress: boolean;\n}\n\ninterface WidgetPreferences {\n  /** 是否启用全屏部件 */\n  fullscreen: boolean;\n  /** 是否启用全局搜索部件 */\n  globalSearch: boolean;\n  /** 是否启用语言切换部件 */\n  languageToggle: boolean;\n  /** 是否开启锁屏功能 */\n  lockScreen: boolean;\n  /** 是否显示通知部件 */\n  notification: boolean;\n  /** 显示刷新按钮 */\n  refresh: boolean;\n  /** 是否显示侧边栏显示/隐藏部件 */\n  sidebarToggle: boolean;\n  /** 是否显示主题切换部件 */\n  themeToggle: boolean;\n}\n\ninterface Preferences {\n  /** 全局配置 */\n  app: AppPreferences;\n  /** 顶栏配置 */\n  breadcrumb: BreadcrumbPreferences;\n  /** 版权配置 */\n  copyright: CopyrightPreferences;\n  /** 底栏配置 */\n  footer: FooterPreferences;\n  /** 面包屑配置 */\n  header: HeaderPreferences;\n  /** logo配置 */\n  logo: LogoPreferences;\n  /** 导航配置 */\n  navigation: NavigationPreferences;\n  /** 快捷键配置 */\n  shortcutKeys: ShortcutKeyPreferences;\n  /** 侧边栏配置 */\n  sidebar: SidebarPreferences;\n  /** 标签页配置 */\n  tabbar: TabbarPreferences;\n  /** 主题配置 */\n  theme: ThemePreferences;\n  /** 动画配置 */\n  transition: TransitionPreferences;\n  /** 功能配置 */\n  widget: WidgetPreferences;\n}\n\ntype PreferencesKeys = keyof Preferences;\n\ninterface InitialOptions {\n  namespace: string;\n  overrides?: DeepPartial<Preferences>;\n}\nexport type {\n  AppPreferences,\n  BreadcrumbPreferences,\n  FooterPreferences,\n  HeaderPreferences,\n  InitialOptions,\n  LogoPreferences,\n  NavigationPreferences,\n  Preferences,\n  PreferencesKeys,\n  ShortcutKeyPreferences,\n  SidebarPreferences,\n  SupportedLanguagesType,\n  TabbarPreferences,\n  ThemePreferences,\n  TransitionPreferences,\n  WidgetPreferences,\n};\n"
  },
  {
    "path": "packages/@core/preferences/src/update-css-variables.ts",
    "content": "import type { Preferences } from './types';\n\nimport { generatorColorVariables } from '@vben-core/shared/color';\nimport { updateCSSVariables as executeUpdateCSSVariables } from '@vben-core/shared/utils';\n\nimport { BUILT_IN_THEME_PRESETS } from './constants';\n\n/**\n * 更新主题的 CSS 变量以及其他 CSS 变量\n * @param preferences - 当前偏好设置对象，它的主题值将被用来设置文档的主题。\n */\nfunction updateCSSVariables(preferences: Preferences) {\n  // 当修改到颜色变量时，更新 css 变量\n  const root = document.documentElement;\n  if (!root) {\n    return;\n  }\n\n  const theme = preferences?.theme ?? {};\n\n  const { builtinType, mode, radius } = theme;\n\n  // html 设置 dark 类\n  if (Reflect.has(theme, 'mode')) {\n    const dark = isDarkTheme(mode);\n    root.classList.toggle('dark', dark);\n  }\n\n  // html 设置 data-theme=[builtinType]\n  if (Reflect.has(theme, 'builtinType')) {\n    const rootTheme = root.dataset.theme;\n    if (rootTheme !== builtinType) {\n      root.dataset.theme = builtinType;\n    }\n  }\n\n  // 获取当前的内置主题\n  const currentBuiltType = [...BUILT_IN_THEME_PRESETS].find(\n    (item) => item.type === builtinType,\n  );\n\n  let builtinTypeColorPrimary: string | undefined = '';\n\n  if (currentBuiltType) {\n    const isDark = isDarkTheme(preferences.theme.mode);\n    // 设置不同主题的主要颜色\n    const color = isDark\n      ? currentBuiltType.darkPrimaryColor || currentBuiltType.primaryColor\n      : currentBuiltType.primaryColor;\n    builtinTypeColorPrimary = color || currentBuiltType.color;\n  }\n\n  // 如果内置主题颜色和自定义颜色都不存在，则不更新主题颜色\n  if (\n    builtinTypeColorPrimary ||\n    Reflect.has(theme, 'colorPrimary') ||\n    Reflect.has(theme, 'colorDestructive') ||\n    Reflect.has(theme, 'colorSuccess') ||\n    Reflect.has(theme, 'colorWarning')\n  ) {\n    // preferences.theme.colorPrimary = builtinTypeColorPrimary || colorPrimary;\n    updateMainColorVariables(preferences);\n  }\n\n  // 更新圆角\n  if (Reflect.has(theme, 'radius')) {\n    document.documentElement.style.setProperty('--radius', `${radius}rem`);\n  }\n}\n\n/**\n * 更新主要的 CSS 变量\n * @param  preference - 当前偏好设置对象，它的颜色值将被转换成 HSL 格式并设置为 CSS 变量。\n */\nfunction updateMainColorVariables(preference: Preferences) {\n  if (!preference.theme) {\n    return;\n  }\n  const { colorDestructive, colorPrimary, colorSuccess, colorWarning } =\n    preference.theme;\n\n  const colorVariables = generatorColorVariables([\n    { color: colorPrimary, name: 'primary' },\n    { alias: 'warning', color: colorWarning, name: 'yellow' },\n    { alias: 'success', color: colorSuccess, name: 'green' },\n    { alias: 'destructive', color: colorDestructive, name: 'red' },\n  ]);\n\n  // 要设置的 CSS 变量映射\n  const colorMappings = {\n    '--green-500': '--success',\n    '--primary-500': '--primary',\n    '--red-500': '--destructive',\n    '--yellow-500': '--warning',\n  };\n\n  // 统一处理颜色变量的更新\n  Object.entries(colorMappings).forEach(([sourceVar, targetVar]) => {\n    const colorValue = colorVariables[sourceVar];\n    if (colorValue) {\n      document.documentElement.style.setProperty(targetVar, colorValue);\n    }\n  });\n\n  executeUpdateCSSVariables(colorVariables);\n}\n\nfunction isDarkTheme(theme: string) {\n  let dark = theme === 'dark';\n  if (theme === 'auto') {\n    dark = window.matchMedia('(prefers-color-scheme: dark)').matches;\n  }\n  return dark;\n}\n\nexport { isDarkTheme, updateCSSVariables };\n"
  },
  {
    "path": "packages/@core/preferences/src/use-preferences.ts",
    "content": "import { diff } from '@vben-core/shared/utils';\nimport { computed } from 'vue';\n\nimport { preferencesManager } from './preferences';\nimport { isDarkTheme } from './update-css-variables';\n\nfunction usePreferences() {\n  const preferences = preferencesManager.getPreferences();\n  const initialPreferences = preferencesManager.getInitialPreferences();\n  /**\n   * @zh_CN 计算偏好设置的变化\n   */\n  const diffPreference = computed(() => {\n    return diff(initialPreferences, preferences);\n  });\n\n  const appPreferences = computed(() => preferences.app);\n\n  const shortcutKeysPreferences = computed(() => preferences.shortcutKeys);\n\n  /**\n   * @zh_CN 判断是否为暗黑模式\n   * @param  preferences - 当前偏好设置对象，它的主题值将被用来判断是否为暗黑模式。\n   * @returns 如果主题为暗黑模式，返回 true，否则返回 false。\n   */\n  const isDark = computed(() => {\n    return isDarkTheme(preferences.theme.mode);\n  });\n\n  const locale = computed(() => {\n    return preferences.app.locale;\n  });\n\n  const isMobile = computed(() => {\n    return appPreferences.value.isMobile;\n  });\n\n  const theme = computed(() => {\n    return isDark.value ? 'dark' : 'light';\n  });\n\n  /**\n   * @zh_CN 布局方式\n   */\n  const layout = computed(() =>\n    isMobile.value ? 'sidebar-nav' : appPreferences.value.layout,\n  );\n\n  /**\n   * @zh_CN 是否显示顶栏\n   */\n  const isShowHeaderNav = computed(() => {\n    return preferences.header.enable;\n  });\n\n  /**\n   * @zh_CN 是否全屏显示content，不需要侧边、底部、顶部、tab区域\n   */\n  const isFullContent = computed(\n    () => appPreferences.value.layout === 'full-content',\n  );\n\n  /**\n   * @zh_CN 是否侧边导航模式\n   */\n  const isSideNav = computed(\n    () => appPreferences.value.layout === 'sidebar-nav',\n  );\n\n  /**\n   * @zh_CN 是否侧边混合模式\n   */\n  const isSideMixedNav = computed(\n    () => appPreferences.value.layout === 'sidebar-mixed-nav',\n  );\n\n  /**\n   * @zh_CN 是否为头部导航模式\n   */\n  const isHeaderNav = computed(\n    () => appPreferences.value.layout === 'header-nav',\n  );\n\n  /**\n   * @zh_CN 是否为头部混合导航模式\n   */\n  const isHeaderMixedNav = computed(\n    () => appPreferences.value.layout === 'header-mixed-nav',\n  );\n\n  /**\n   * @zh_CN 是否为顶部通栏+侧边导航模式\n   */\n  const isHeaderSidebarNav = computed(\n    () => appPreferences.value.layout === 'header-sidebar-nav',\n  );\n\n  /**\n   * @zh_CN 是否为混合导航模式\n   */\n  const isMixedNav = computed(\n    () => appPreferences.value.layout === 'mixed-nav',\n  );\n\n  /**\n   * @zh_CN 是否包含侧边导航模式\n   */\n  const isSideMode = computed(() => {\n    return (\n      isMixedNav.value ||\n      isSideMixedNav.value ||\n      isSideNav.value ||\n      isHeaderMixedNav.value ||\n      isHeaderSidebarNav.value\n    );\n  });\n\n  const sidebarCollapsed = computed(() => {\n    return preferences.sidebar.collapsed;\n  });\n\n  /**\n   * @zh_CN 是否开启keep-alive\n   * 在tabs可见以及开启keep-alive的情况下才开启\n   */\n  const keepAlive = computed(\n    () => preferences.tabbar.enable && preferences.tabbar.keepAlive,\n  );\n\n  /**\n   * @zh_CN 登录注册页面布局是否为左侧\n   */\n  const authPanelLeft = computed(() => {\n    return appPreferences.value.authPageLayout === 'panel-left';\n  });\n\n  /**\n   * @zh_CN 登录注册页面布局是否为左侧\n   */\n  const authPanelRight = computed(() => {\n    return appPreferences.value.authPageLayout === 'panel-right';\n  });\n\n  /**\n   * @zh_CN 登录注册页面布局是否为中间\n   */\n  const authPanelCenter = computed(() => {\n    return appPreferences.value.authPageLayout === 'panel-center';\n  });\n\n  /**\n   * @zh_CN 内容是否已经最大化\n   * 排除 full-content模式\n   */\n  const contentIsMaximize = computed(() => {\n    const headerIsHidden = preferences.header.hidden;\n    const sidebarIsHidden = preferences.sidebar.hidden;\n    return headerIsHidden && sidebarIsHidden && !isFullContent.value;\n  });\n\n  /**\n   * @zh_CN 是否启用全局搜索快捷键\n   */\n  const globalSearchShortcutKey = computed(() => {\n    const { enable, globalSearch } = shortcutKeysPreferences.value;\n    return enable && globalSearch;\n  });\n\n  /**\n   * @zh_CN 是否启用全局注销快捷键\n   */\n  const globalLogoutShortcutKey = computed(() => {\n    const { enable, globalLogout } = shortcutKeysPreferences.value;\n    return enable && globalLogout;\n  });\n\n  const globalLockScreenShortcutKey = computed(() => {\n    const { enable, globalLockScreen } = shortcutKeysPreferences.value;\n    return enable && globalLockScreen;\n  });\n\n  /**\n   * @zh_CN 偏好设置按钮位置\n   */\n  const preferencesButtonPosition = computed(() => {\n    const { enablePreferences, preferencesButtonPosition } = preferences.app;\n\n    // 如果没有启用偏好设置按钮\n    if (!enablePreferences) {\n      return {\n        fixed: false,\n        header: false,\n      };\n    }\n\n    const { header, sidebar } = preferences;\n    const headerHidden = header.hidden;\n    const sidebarHidden = sidebar.hidden;\n\n    const contentIsMaximize = headerHidden && sidebarHidden;\n\n    const isHeaderPosition = preferencesButtonPosition === 'header';\n\n    // 如果设置了固定位置\n    if (preferencesButtonPosition !== 'auto') {\n      return {\n        fixed: preferencesButtonPosition === 'fixed',\n        header: isHeaderPosition,\n      };\n    }\n\n    // 如果是全屏模式或者没有固定在顶部，\n    const fixed =\n      contentIsMaximize ||\n      isFullContent.value ||\n      isMobile.value ||\n      !isShowHeaderNav.value;\n\n    return {\n      fixed,\n      header: !fixed,\n    };\n  });\n\n  return {\n    authPanelCenter,\n    authPanelLeft,\n    authPanelRight,\n    contentIsMaximize,\n    diffPreference,\n    globalLockScreenShortcutKey,\n    globalLogoutShortcutKey,\n    globalSearchShortcutKey,\n    isDark,\n    isFullContent,\n    isHeaderMixedNav,\n    isHeaderNav,\n    isHeaderSidebarNav,\n    isMixedNav,\n    isMobile,\n    isSideMixedNav,\n    isSideMode,\n    isSideNav,\n    keepAlive,\n    layout,\n    locale,\n    preferencesButtonPosition,\n    sidebarCollapsed,\n    theme,\n  };\n}\n\nexport { usePreferences };\n"
  },
  {
    "path": "packages/@core/preferences/tsconfig.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/tsconfig\",\n  \"extends\": \"@vben/tsconfig/web.json\",\n  \"include\": [\"src\", \"__tests__\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "packages/@core/ui-kit/README.md",
    "content": "# ui-kit\n\n用于管理公共组件、不同UI组件库封装的组件\n"
  },
  {
    "path": "packages/@core/ui-kit/form-ui/__tests__/form-api.test.ts",
    "content": "import { beforeEach, describe, expect, it, vi } from 'vitest';\n\nimport { FormApi } from '../src/form-api';\n\ndescribe('formApi', () => {\n  let formApi: FormApi;\n\n  beforeEach(() => {\n    formApi = new FormApi();\n  });\n\n  it('should initialize with default state', () => {\n    expect(formApi.state).toEqual(\n      expect.objectContaining({\n        actionWrapperClass: '',\n        collapsed: false,\n        collapsedRows: 1,\n        commonConfig: {},\n        handleReset: undefined,\n        handleSubmit: undefined,\n        layout: 'horizontal',\n        resetButtonOptions: {},\n        schema: [],\n        showCollapseButton: false,\n        showDefaultActions: true,\n        submitButtonOptions: {},\n        wrapperClass: 'grid-cols-1',\n      }),\n    );\n    expect(formApi.isMounted).toBe(false);\n  });\n\n  it('should mount form actions', async () => {\n    const formActions: any = {\n      meta: {},\n      resetForm: vi.fn(),\n      setFieldValue: vi.fn(),\n      setValues: vi.fn(),\n      submitForm: vi.fn(),\n      validate: vi.fn(),\n      values: { name: 'test' },\n    };\n\n    await formApi.mount(formActions);\n    expect(formApi.isMounted).toBe(true);\n    expect(formApi.form).toEqual(formActions);\n  });\n\n  it('should get values from form', async () => {\n    const formActions: any = {\n      meta: {},\n      values: { name: 'test' },\n    };\n\n    await formApi.mount(formActions);\n    const values = await formApi.getValues();\n    expect(values).toEqual({ name: 'test' });\n  });\n\n  it('should set field value', async () => {\n    const setFieldValueMock = vi.fn();\n    const formActions: any = {\n      meta: {},\n      setFieldValue: setFieldValueMock,\n      values: { name: 'test' },\n    };\n\n    await formApi.mount(formActions);\n    await formApi.setFieldValue('name', 'new value');\n    expect(setFieldValueMock).toHaveBeenCalledWith(\n      'name',\n      'new value',\n      undefined,\n    );\n  });\n\n  it('should reset form', async () => {\n    const resetFormMock = vi.fn();\n    const formActions: any = {\n      meta: {},\n      resetForm: resetFormMock,\n      values: { name: 'test' },\n    };\n\n    await formApi.mount(formActions);\n    await formApi.resetForm();\n    expect(resetFormMock).toHaveBeenCalled();\n  });\n\n  it('should call handleSubmit on submit', async () => {\n    const handleSubmitMock = vi.fn();\n    const formActions: any = {\n      meta: {},\n      submitForm: vi.fn().mockResolvedValue(true),\n      values: { name: 'test' },\n    };\n\n    const state = {\n      handleSubmit: handleSubmitMock,\n    };\n\n    formApi.setState(state);\n    await formApi.mount(formActions);\n\n    const result = await formApi.submitForm();\n    expect(formActions.submitForm).toHaveBeenCalled();\n    expect(handleSubmitMock).toHaveBeenCalledWith({ name: 'test' });\n    expect(result).toEqual({ name: 'test' });\n  });\n\n  it('should unmount form and reset state', () => {\n    formApi.unmount();\n    expect(formApi.isMounted).toBe(false);\n  });\n\n  it('should validate form', async () => {\n    const validateMock = vi.fn().mockResolvedValue(true);\n    const formActions: any = {\n      meta: {},\n      validate: validateMock,\n    };\n\n    await formApi.mount(formActions);\n    const isValid = await formApi.validate();\n    expect(validateMock).toHaveBeenCalled();\n    expect(isValid).toBe(true);\n  });\n});\n\ndescribe('updateSchema', () => {\n  let instance: FormApi;\n\n  beforeEach(() => {\n    instance = new FormApi();\n    instance.state = {\n      schema: [\n        { component: 'text', fieldName: 'name' },\n        { component: 'number', fieldName: 'age', label: 'Age' },\n      ],\n    };\n  });\n\n  it('should update the schema correctly when fieldName matches', () => {\n    const newSchema = [\n      { component: 'text', fieldName: 'name' },\n      { component: 'number', fieldName: 'age', label: 'Age' },\n    ];\n\n    instance.updateSchema(newSchema);\n\n    expect(instance.state?.schema?.[0]?.component).toBe('text');\n    expect(instance.state?.schema?.[1]?.label).toBe('Age');\n  });\n\n  it('should log an error if fieldName is missing in some items', () => {\n    const newSchema: any[] = [\n      { component: 'textarea', fieldName: 'name' },\n      { component: 'number' },\n    ];\n\n    const consoleErrorSpy = vi\n      .spyOn(console, 'error')\n      .mockImplementation(() => {});\n\n    instance.updateSchema(newSchema);\n\n    expect(consoleErrorSpy).toHaveBeenCalledWith(\n      'All items in the schema array must have a valid `fieldName` property to be updated',\n    );\n  });\n\n  it('should not update schema if fieldName does not match', () => {\n    const newSchema = [{ component: 'textarea', fieldName: 'unknown' }];\n\n    instance.updateSchema(newSchema);\n\n    expect(instance.state?.schema?.[0]?.component).toBe('text');\n    expect(instance.state?.schema?.[1]?.component).toBe('number');\n  });\n\n  it('should not update schema if updatedMap is empty', () => {\n    const newSchema: any[] = [{ component: 'textarea' }];\n\n    instance.updateSchema(newSchema);\n\n    expect(instance.state?.schema?.[0]?.component).toBe('text');\n    expect(instance.state?.schema?.[1]?.component).toBe('number');\n  });\n});\n"
  },
  {
    "path": "packages/@core/ui-kit/form-ui/build.config.ts",
    "content": "import { defineBuildConfig } from 'unbuild';\n\nexport default defineBuildConfig({\n  clean: true,\n  declaration: true,\n  entries: [\n    {\n      builder: 'mkdist',\n      input: './src',\n      loaders: ['vue'],\n      pattern: ['**/*.vue'],\n    },\n    {\n      builder: 'mkdist',\n      format: 'esm',\n      input: './src',\n      loaders: ['js'],\n      pattern: ['**/*.ts'],\n    },\n  ],\n});\n"
  },
  {
    "path": "packages/@core/ui-kit/form-ui/package.json",
    "content": "{\n  \"name\": \"@vben-core/form-ui\",\n  \"version\": \"5.5.9\",\n  \"homepage\": \"https://github.com/vbenjs/vue-vben-admin\",\n  \"bugs\": \"https://github.com/vbenjs/vue-vben-admin/issues\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/vbenjs/vue-vben-admin.git\",\n    \"directory\": \"packages/@vben-core/uikit/form-ui\"\n  },\n  \"license\": \"MIT\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"build\": \"pnpm unbuild\",\n    \"prepublishOnly\": \"npm run build\"\n  },\n  \"files\": [\n    \"dist\"\n  ],\n  \"sideEffects\": [\n    \"**/*.css\"\n  ],\n  \"main\": \"./dist/index.mjs\",\n  \"module\": \"./dist/index.mjs\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./src/index.ts\",\n      \"development\": \"./src/index.ts\",\n      \"default\": \"./dist/index.mjs\"\n    }\n  },\n  \"publishConfig\": {\n    \"exports\": {\n      \".\": {\n        \"default\": \"./dist/index.mjs\"\n      }\n    }\n  },\n  \"dependencies\": {\n    \"@vben-core/composables\": \"workspace:*\",\n    \"@vben-core/icons\": \"workspace:*\",\n    \"@vben-core/shadcn-ui\": \"workspace:*\",\n    \"@vben-core/shared\": \"workspace:*\",\n    \"@vben-core/typings\": \"workspace:*\",\n    \"@vee-validate/zod\": \"catalog:\",\n    \"@vueuse/core\": \"catalog:\",\n    \"vee-validate\": \"catalog:\",\n    \"vue\": \"catalog:\",\n    \"zod\": \"catalog:\",\n    \"zod-defaults\": \"catalog:\"\n  }\n}\n"
  },
  {
    "path": "packages/@core/ui-kit/form-ui/postcss.config.mjs",
    "content": "export { default } from '@vben/tailwind-config/postcss';\n"
  },
  {
    "path": "packages/@core/ui-kit/form-ui/src/components/form-actions.vue",
    "content": "<script setup lang=\"ts\">\nimport { computed, toRaw, unref, watch } from 'vue';\n\nimport { useSimpleLocale } from '@vben-core/composables';\nimport { VbenExpandableArrow } from '@vben-core/shadcn-ui';\nimport { cn, isFunction, triggerWindowResize } from '@vben-core/shared/utils';\n\nimport { COMPONENT_MAP } from '../config';\nimport { injectFormProps } from '../use-form-context';\n\nconst { $t } = useSimpleLocale();\n\nconst [rootProps, form] = injectFormProps();\n\nconst collapsed = defineModel({ default: false });\n\nconst resetButtonOptions = computed(() => {\n  return {\n    content: `${$t.value('reset')}`,\n    show: true,\n    ...unref(rootProps).resetButtonOptions,\n  };\n});\n\nconst submitButtonOptions = computed(() => {\n  return {\n    content: `${$t.value('submit')}`,\n    show: true,\n    ...unref(rootProps).submitButtonOptions,\n  };\n});\n\n// const isQueryForm = computed(() => {\n//   return !!unref(rootProps).showCollapseButton;\n// });\n\nasync function handleSubmit(e: Event) {\n  e?.preventDefault();\n  e?.stopPropagation();\n  const props = unref(rootProps);\n  if (!props.formApi) {\n    return;\n  }\n\n  const { valid } = await props.formApi.validate();\n  if (!valid) {\n    return;\n  }\n\n  const values = toRaw(await props.formApi.getValues());\n  await props.handleSubmit?.(values);\n}\n\nasync function handleReset(e: Event) {\n  e?.preventDefault();\n  e?.stopPropagation();\n  const props = unref(rootProps);\n\n  const values = toRaw(await props.formApi?.getValues());\n\n  if (isFunction(props.handleReset)) {\n    await props.handleReset?.(values);\n  } else {\n    form.resetForm();\n  }\n}\n\nwatch(\n  () => collapsed.value,\n  () => {\n    const props = unref(rootProps);\n    if (props.collapseTriggerResize) {\n      triggerWindowResize();\n    }\n  },\n);\n\nconst actionWrapperClass = computed(() => {\n  const props = unref(rootProps);\n  const actionLayout = props.actionLayout || 'rowEnd';\n  const actionPosition = props.actionPosition || 'right';\n\n  const cls = [\n    'flex',\n    'items-center',\n    'gap-3',\n    props.compact ? 'pb-2' : 'pb-4',\n    props.layout === 'vertical' ? 'self-end' : 'self-center',\n    props.layout === 'inline' ? '' : 'w-full',\n    props.actionWrapperClass,\n  ];\n\n  switch (actionLayout) {\n    case 'newLine': {\n      cls.push('col-span-full');\n      break;\n    }\n    case 'rowEnd': {\n      cls.push('col-[-2/-1]');\n      break;\n    }\n    // 'inline' 不需要额外类名，保持默认\n  }\n\n  switch (actionPosition) {\n    case 'center': {\n      cls.push('justify-center');\n      break;\n    }\n    case 'left': {\n      cls.push('justify-start');\n      break;\n    }\n    default: {\n      // case 'right': 默认右对齐\n      cls.push('justify-end');\n      break;\n    }\n  }\n\n  return cls.join(' ');\n});\n\ndefineExpose({\n  handleReset,\n  handleSubmit,\n});\n</script>\n<template>\n  <div :class=\"cn(actionWrapperClass)\">\n    <template v-if=\"rootProps.actionButtonsReverse\">\n      <!-- 提交按钮前 -->\n      <slot name=\"submit-before\"></slot>\n\n      <component\n        :is=\"COMPONENT_MAP.PrimaryButton\"\n        v-if=\"submitButtonOptions.show\"\n        type=\"button\"\n        @click=\"handleSubmit\"\n        v-bind=\"submitButtonOptions\"\n      >\n        {{ submitButtonOptions.content }}\n      </component>\n    </template>\n\n    <!-- 重置按钮前 -->\n    <slot name=\"reset-before\"></slot>\n\n    <component\n      :is=\"COMPONENT_MAP.DefaultButton\"\n      v-if=\"resetButtonOptions.show\"\n      type=\"button\"\n      @click=\"handleReset\"\n      v-bind=\"resetButtonOptions\"\n    >\n      {{ resetButtonOptions.content }}\n    </component>\n\n    <template v-if=\"!rootProps.actionButtonsReverse\">\n      <!-- 提交按钮前 -->\n      <slot name=\"submit-before\"></slot>\n\n      <component\n        :is=\"COMPONENT_MAP.PrimaryButton\"\n        v-if=\"submitButtonOptions.show\"\n        type=\"button\"\n        @click=\"handleSubmit\"\n        v-bind=\"submitButtonOptions\"\n      >\n        {{ submitButtonOptions.content }}\n      </component>\n    </template>\n\n    <!-- 展开按钮前 -->\n    <slot name=\"expand-before\"></slot>\n\n    <VbenExpandableArrow\n      class=\"ml-[-0.3em]\"\n      v-if=\"rootProps.showCollapseButton\"\n      v-model:model-value=\"collapsed\"\n    >\n      <span>{{ collapsed ? $t('expand') : $t('collapse') }}</span>\n    </VbenExpandableArrow>\n\n    <!-- 展开按钮后 -->\n    <slot name=\"expand-after\"></slot>\n  </div>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/form-ui/src/config.ts",
    "content": "import type { Component } from 'vue';\n\nimport type {\n  BaseFormComponentType,\n  FormCommonConfig,\n  VbenFormAdapterOptions,\n} from './types';\n\nimport {\n  VbenButton,\n  VbenCheckbox,\n  Input as VbenInput,\n  VbenInputCaptcha,\n  VbenInputPassword,\n  VbenPinInput,\n  VbenSelect,\n} from '@vben-core/shadcn-ui';\nimport { globalShareState } from '@vben-core/shared/global-state';\nimport { defineRule } from 'vee-validate';\nimport { h } from 'vue';\n\nconst DEFAULT_MODEL_PROP_NAME = 'modelValue';\n\nexport const DEFAULT_FORM_COMMON_CONFIG: FormCommonConfig = {};\n\nexport const COMPONENT_MAP: Record<BaseFormComponentType, Component> = {\n  DefaultButton: h(VbenButton, { size: 'sm', variant: 'outline' }),\n  PrimaryButton: h(VbenButton, { size: 'sm', variant: 'default' }),\n  VbenCheckbox,\n  VbenInput,\n  VbenInputCaptcha,\n  VbenInputPassword,\n  VbenPinInput,\n  VbenSelect,\n};\n\nexport const COMPONENT_BIND_EVENT_MAP: Partial<\n  Record<BaseFormComponentType, string>\n> = {\n  VbenCheckbox: 'checked',\n};\n\nexport function setupVbenForm<\n  T extends BaseFormComponentType = BaseFormComponentType,\n>(options: VbenFormAdapterOptions<T>) {\n  const { config, defineRules } = options;\n\n  const {\n    disabledOnChangeListener = true,\n    disabledOnInputListener = true,\n    emptyStateValue = undefined,\n  } = (config || {}) as FormCommonConfig;\n\n  Object.assign(DEFAULT_FORM_COMMON_CONFIG, {\n    disabledOnChangeListener,\n    disabledOnInputListener,\n    emptyStateValue,\n  });\n\n  if (defineRules) {\n    for (const key of Object.keys(defineRules)) {\n      defineRule(key, defineRules[key as never]);\n    }\n  }\n\n  const baseModelPropName =\n    config?.baseModelPropName ?? DEFAULT_MODEL_PROP_NAME;\n  const modelPropNameMap = config?.modelPropNameMap as\n    | Record<BaseFormComponentType, string>\n    | undefined;\n\n  const components = globalShareState.getComponents();\n\n  for (const component of Object.keys(components)) {\n    const key = component as BaseFormComponentType;\n    COMPONENT_MAP[key] = components[component as never];\n\n    if (baseModelPropName !== DEFAULT_MODEL_PROP_NAME) {\n      COMPONENT_BIND_EVENT_MAP[key] = baseModelPropName;\n    }\n\n    // 覆盖特殊组件的modelPropName\n    if (modelPropNameMap && modelPropNameMap[key]) {\n      COMPONENT_BIND_EVENT_MAP[key] = modelPropNameMap[key];\n    }\n  }\n}\n"
  },
  {
    "path": "packages/@core/ui-kit/form-ui/src/form-api.ts",
    "content": "import type {\n  FormState,\n  GenericObject,\n  ResetFormOpts,\n  ValidationOptions,\n} from 'vee-validate';\n\nimport type { ComponentPublicInstance } from 'vue';\n\nimport type { Recordable } from '@vben-core/typings';\n\nimport type { FormActions, FormSchema, VbenFormProps } from './types';\n\nimport { isRef, toRaw } from 'vue';\n\nimport { Store } from '@vben-core/shared/store';\nimport {\n  bindMethods,\n  createMerge,\n  formatDate,\n  isDate,\n  isDayjsObject,\n  isFunction,\n  isObject,\n  mergeWithArrayOverride,\n  StateHandler,\n} from '@vben-core/shared/utils';\n\nfunction getDefaultState(): VbenFormProps {\n  return {\n    actionWrapperClass: '',\n    collapsed: false,\n    collapsedRows: 1,\n    collapseTriggerResize: false,\n    commonConfig: {},\n    handleReset: undefined,\n    handleSubmit: undefined,\n    handleValuesChange: undefined,\n    layout: 'horizontal',\n    resetButtonOptions: {},\n    schema: [],\n    scrollToFirstError: false,\n    showCollapseButton: false,\n    showDefaultActions: true,\n    submitButtonOptions: {},\n    submitOnChange: false,\n    submitOnEnter: false,\n    wrapperClass: 'grid-cols-1',\n  };\n}\n\nexport class FormApi {\n  // private api: Pick<VbenFormProps, 'handleReset' | 'handleSubmit'>;\n  public form = {} as FormActions;\n  isMounted = false;\n\n  public state: null | VbenFormProps = null;\n  stateHandler: StateHandler;\n\n  public store: Store<VbenFormProps>;\n\n  /**\n   * 组件实例映射\n   */\n  private componentRefMap: Map<string, unknown> = new Map();\n\n  // 最后一次点击提交时的表单值\n  private latestSubmissionValues: null | Recordable<any> = null;\n\n  private prevState: null | VbenFormProps = null;\n\n  constructor(options: VbenFormProps = {}) {\n    const { ...storeState } = options;\n\n    const defaultState = getDefaultState();\n\n    this.store = new Store<VbenFormProps>(\n      {\n        ...defaultState,\n        ...storeState,\n      },\n      {\n        onUpdate: () => {\n          this.prevState = this.state;\n          this.state = this.store.state;\n          this.updateState();\n        },\n      },\n    );\n\n    this.state = this.store.state;\n    this.stateHandler = new StateHandler();\n    bindMethods(this);\n  }\n\n  /**\n   * 获取字段组件实例\n   * @param fieldName 字段名\n   * @returns 组件实例\n   */\n  getFieldComponentRef<T = ComponentPublicInstance>(\n    fieldName: string,\n  ): T | undefined {\n    let target = this.componentRefMap.has(fieldName)\n      ? (this.componentRefMap.get(fieldName) as ComponentPublicInstance)\n      : undefined;\n    if (\n      target &&\n      target.$.type.name === 'AsyncComponentWrapper' &&\n      target.$.subTree.ref\n    ) {\n      if (Array.isArray(target.$.subTree.ref)) {\n        if (\n          target.$.subTree.ref.length > 0 &&\n          isRef(target.$.subTree.ref[0]?.r)\n        ) {\n          target = target.$.subTree.ref[0]?.r.value as ComponentPublicInstance;\n        }\n      } else if (isRef(target.$.subTree.ref.r)) {\n        target = target.$.subTree.ref.r.value as ComponentPublicInstance;\n      }\n    }\n    return target as T;\n  }\n\n  /**\n   * 获取当前聚焦的字段，如果没有聚焦的字段则返回undefined\n   */\n  getFocusedField() {\n    for (const fieldName of this.componentRefMap.keys()) {\n      const ref = this.getFieldComponentRef(fieldName);\n      if (ref) {\n        let el: HTMLElement | null = null;\n        if (ref instanceof HTMLElement) {\n          el = ref;\n        } else if (ref.$el instanceof HTMLElement) {\n          el = ref.$el;\n        }\n        if (!el) {\n          continue;\n        }\n        if (\n          el === document.activeElement ||\n          el.contains(document.activeElement)\n        ) {\n          return fieldName;\n        }\n      }\n    }\n    return undefined;\n  }\n\n  getLatestSubmissionValues() {\n    return this.latestSubmissionValues || {};\n  }\n\n  getState() {\n    return this.state;\n  }\n\n  async getValues<T = Recordable<any>>() {\n    const form = await this.getForm();\n    return (form.values ? this.handleRangeTimeValue(form.values) : {}) as T;\n  }\n\n  async isFieldValid(fieldName: string) {\n    const form = await this.getForm();\n    return form.isFieldValid(fieldName);\n  }\n\n  merge(formApi: FormApi) {\n    const chain = [this, formApi];\n    const proxy = new Proxy(formApi, {\n      get(target: any, prop: any) {\n        if (prop === 'merge') {\n          return (nextFormApi: FormApi) => {\n            chain.push(nextFormApi);\n            return proxy;\n          };\n        }\n        if (prop === 'submitAllForm') {\n          return async (needMerge: boolean = true) => {\n            try {\n              const results = await Promise.all(\n                chain.map(async (api) => {\n                  const validateResult = await api.validate();\n                  if (!validateResult.valid) {\n                    return;\n                  }\n                  const rawValues = toRaw((await api.getValues()) || {});\n                  return rawValues;\n                }),\n              );\n              if (needMerge) {\n                const mergedResults = Object.assign({}, ...results);\n                return mergedResults;\n              }\n              return results;\n            } catch (error) {\n              console.error('Validation error:', error);\n            }\n          };\n        }\n        return target[prop];\n      },\n    });\n\n    return proxy;\n  }\n\n  mount(formActions: FormActions, componentRefMap: Map<string, unknown>) {\n    if (!this.isMounted) {\n      Object.assign(this.form, formActions);\n      this.stateHandler.setConditionTrue();\n      this.setLatestSubmissionValues({\n        ...toRaw(this.handleRangeTimeValue(this.form.values)),\n      });\n      this.componentRefMap = componentRefMap;\n      this.isMounted = true;\n    }\n  }\n\n  /**\n   * 根据字段名移除表单项\n   * @param fields\n   */\n  async removeSchemaByFields(fields: string[]) {\n    const fieldSet = new Set(fields);\n    const schema = this.state?.schema ?? [];\n\n    const filterSchema = schema.filter((item) => !fieldSet.has(item.fieldName));\n\n    this.setState({\n      schema: filterSchema,\n    });\n  }\n\n  /**\n   * 重置表单\n   */\n  async resetForm(\n    state?: Partial<FormState<GenericObject>> | undefined,\n    opts?: Partial<ResetFormOpts>,\n  ) {\n    const form = await this.getForm();\n    return form.resetForm(state, opts);\n  }\n\n  async resetValidate() {\n    const form = await this.getForm();\n    const fields = Object.keys(form.errors.value);\n    fields.forEach((field) => {\n      form.setFieldError(field, undefined);\n    });\n  }\n\n  /**\n   * 滚动到第一个错误字段\n   * @param errors 验证错误对象\n   */\n  scrollToFirstError(errors: Record<string, any> | string) {\n    // https://github.com/logaretm/vee-validate/discussions/3835\n    const firstErrorFieldName =\n      typeof errors === 'string' ? errors : Object.keys(errors)[0];\n\n    if (!firstErrorFieldName) {\n      return;\n    }\n\n    let el = document.querySelector(\n      `[name=\"${firstErrorFieldName}\"]`,\n    ) as HTMLElement;\n\n    // 如果通过 name 属性找不到，尝试通过组件引用查找, 正常情况下不会走到这，怕哪天 vee-validate 改了 name 属性有个兜底的\n    if (!el) {\n      const componentRef = this.getFieldComponentRef(firstErrorFieldName);\n      if (componentRef && componentRef.$el instanceof HTMLElement) {\n        el = componentRef.$el;\n      }\n    }\n\n    if (el) {\n      // 滚动到错误字段，添加一些偏移量以确保字段完全可见\n      el.scrollIntoView({\n        behavior: 'smooth',\n        block: 'center',\n        inline: 'nearest',\n      });\n    }\n  }\n\n  async setFieldValue(field: string, value: any, shouldValidate?: boolean) {\n    const form = await this.getForm();\n    form.setFieldValue(field, value, shouldValidate);\n  }\n\n  setLatestSubmissionValues(values: null | Recordable<any>) {\n    this.latestSubmissionValues = { ...toRaw(values) };\n  }\n\n  setState(\n    stateOrFn:\n      | ((prev: VbenFormProps) => Partial<VbenFormProps>)\n      | Partial<VbenFormProps>,\n  ) {\n    if (isFunction(stateOrFn)) {\n      this.store.setState((prev) => {\n        return mergeWithArrayOverride(stateOrFn(prev), prev);\n      });\n    } else {\n      this.store.setState((prev) => mergeWithArrayOverride(stateOrFn, prev));\n    }\n  }\n\n  /**\n   * 设置表单值\n   * @param fields record\n   * @param filterFields 过滤不在schema中定义的字段 默认为true\n   * @param shouldValidate\n   */\n  async setValues(\n    fields: Record<string, any>,\n    filterFields: boolean = true,\n    shouldValidate: boolean = false,\n  ) {\n    const form = await this.getForm();\n    if (!filterFields) {\n      form.setValues(fields, shouldValidate);\n      return;\n    }\n\n    /**\n     * 合并算法有待改进，目前的算法不支持object类型的值。\n     * antd的日期时间相关组件的值类型为dayjs对象\n     * element-plus的日期时间相关组件的值类型可能为Date对象\n     * 以上两种类型需要排除深度合并\n     */\n    const fieldMergeFn = createMerge((obj, key, value) => {\n      if (key in obj) {\n        obj[key] =\n          !Array.isArray(obj[key]) &&\n          isObject(obj[key]) &&\n          !isDayjsObject(obj[key]) &&\n          !isDate(obj[key])\n            ? fieldMergeFn(value, obj[key])\n            : value;\n      }\n      return true;\n    });\n    const filteredFields = fieldMergeFn(fields, form.values);\n    form.setValues(filteredFields, shouldValidate);\n  }\n\n  async submitForm(e?: Event) {\n    e?.preventDefault();\n    e?.stopPropagation();\n    const form = await this.getForm();\n    await form.submitForm();\n    const rawValues = toRaw(await this.getValues());\n    await this.state?.handleSubmit?.(rawValues);\n\n    return rawValues;\n  }\n\n  unmount() {\n    this.form?.resetForm?.();\n    // this.state = null;\n    this.latestSubmissionValues = null;\n    this.isMounted = false;\n    this.stateHandler.reset();\n  }\n\n  updateSchema(schema: Partial<FormSchema>[]) {\n    const updated: Partial<FormSchema>[] = [...schema];\n    const hasField = updated.every(\n      (item) => Reflect.has(item, 'fieldName') && item.fieldName,\n    );\n\n    if (!hasField) {\n      console.error(\n        'All items in the schema array must have a valid `fieldName` property to be updated',\n      );\n      return;\n    }\n    const currentSchema = [...(this.state?.schema ?? [])];\n\n    const updatedMap: Record<string, any> = {};\n\n    updated.forEach((item) => {\n      if (item.fieldName) {\n        updatedMap[item.fieldName] = item;\n      }\n    });\n\n    currentSchema.forEach((schema, index) => {\n      const updatedData = updatedMap[schema.fieldName];\n      if (updatedData) {\n        currentSchema[index] = mergeWithArrayOverride(\n          updatedData,\n          schema,\n        ) as FormSchema;\n      }\n    });\n    this.setState({ schema: currentSchema });\n  }\n\n  async validate(opts?: Partial<ValidationOptions>) {\n    const form = await this.getForm();\n\n    const validateResult = await form.validate(opts);\n\n    if (Object.keys(validateResult?.errors ?? {}).length > 0) {\n      console.error('validate error', validateResult?.errors);\n\n      if (this.state?.scrollToFirstError) {\n        this.scrollToFirstError(validateResult.errors);\n      }\n    }\n    return validateResult;\n  }\n\n  async validateAndSubmitForm() {\n    const form = await this.getForm();\n    const { valid, errors } = await form.validate();\n    if (!valid) {\n      if (this.state?.scrollToFirstError) {\n        this.scrollToFirstError(errors);\n      }\n      return;\n    }\n    return await this.submitForm();\n  }\n\n  async validateField(fieldName: string, opts?: Partial<ValidationOptions>) {\n    const form = await this.getForm();\n    const validateResult = await form.validateField(fieldName, opts);\n\n    if (Object.keys(validateResult?.errors ?? {}).length > 0) {\n      console.error('validate error', validateResult?.errors);\n\n      if (this.state?.scrollToFirstError) {\n        this.scrollToFirstError(fieldName);\n      }\n    }\n    return validateResult;\n  }\n\n  private async getForm() {\n    if (!this.isMounted) {\n      // 等待form挂载\n      await this.stateHandler.waitForCondition();\n    }\n    if (!this.form?.meta) {\n      throw new Error('<VbenForm /> is not mounted');\n    }\n    return this.form;\n  }\n\n  private handleMultiFields = (originValues: Record<string, any>) => {\n    const arrayToStringFields = this.state?.arrayToStringFields;\n    if (!arrayToStringFields || !Array.isArray(arrayToStringFields)) {\n      return;\n    }\n\n    const processFields = (fields: string[], separator: string = ',') => {\n      this.processFields(fields, separator, originValues, (value, sep) => {\n        if (Array.isArray(value)) {\n          return value.join(sep);\n        } else if (typeof value === 'string') {\n          // 处理空字符串的情况\n          if (value === '') {\n            return [];\n          }\n          // 处理复杂分隔符的情况\n          const escapedSeparator = sep.replaceAll(\n            /[.*+?^${}()|[\\]\\\\]/g,\n            String.raw`\\$&`,\n          );\n          return value.split(new RegExp(escapedSeparator));\n        } else {\n          return value;\n        }\n      });\n    };\n\n    // 处理简单数组格式 ['field1', 'field2', ';'] 或 ['field1', 'field2']\n    if (arrayToStringFields.every((item) => typeof item === 'string')) {\n      const lastItem =\n        arrayToStringFields[arrayToStringFields.length - 1] || '';\n      const fields =\n        lastItem.length === 1\n          ? arrayToStringFields.slice(0, -1)\n          : arrayToStringFields;\n      const separator = lastItem.length === 1 ? lastItem : ',';\n      processFields(fields, separator);\n      return;\n    }\n\n    // 处理嵌套数组格式 [['field1'], ';']\n    arrayToStringFields.forEach((fieldConfig) => {\n      if (Array.isArray(fieldConfig)) {\n        const [fields, separator = ','] = fieldConfig;\n        // 根据类型定义，fields 应该始终是字符串数组\n        if (!Array.isArray(fields)) {\n          console.warn(\n            `Invalid field configuration: fields should be an array of strings, got ${typeof fields}`,\n          );\n          return;\n        }\n        processFields(fields, separator);\n      }\n    });\n  };\n\n  private handleRangeTimeValue = (originValues: Record<string, any>) => {\n    const values = { ...originValues };\n    const fieldMappingTime = this.state?.fieldMappingTime;\n\n    this.handleMultiFields(values);\n    if (!fieldMappingTime || !Array.isArray(fieldMappingTime)) {\n      return values;\n    }\n\n    fieldMappingTime.forEach(\n      ([field, [startTimeKey, endTimeKey], format = 'YYYY-MM-DD']) => {\n        if (startTimeKey && endTimeKey && values[field] === null) {\n          Reflect.deleteProperty(values, startTimeKey);\n          Reflect.deleteProperty(values, endTimeKey);\n          // delete values[startTimeKey];\n          // delete values[endTimeKey];\n        }\n\n        if (!values[field]) {\n          Reflect.deleteProperty(values, field);\n          // delete values[field];\n          return;\n        }\n\n        const [startTime, endTime] = values[field];\n        if (format === null) {\n          values[startTimeKey] = startTime;\n          values[endTimeKey] = endTime;\n        } else if (isFunction(format)) {\n          values[startTimeKey] = format(startTime, startTimeKey);\n          values[endTimeKey] = format(endTime, endTimeKey);\n        } else {\n          const [startTimeFormat, endTimeFormat] = Array.isArray(format)\n            ? format\n            : [format, format];\n\n          values[startTimeKey] = startTime\n            ? formatDate(startTime, startTimeFormat)\n            : undefined;\n          values[endTimeKey] = endTime\n            ? formatDate(endTime, endTimeFormat)\n            : undefined;\n        }\n        // delete values[field];\n        Reflect.deleteProperty(values, field);\n      },\n    );\n    return values;\n  };\n\n  private processFields = (\n    fields: string[],\n    separator: string,\n    originValues: Record<string, any>,\n    transformFn: (value: any, separator: string) => any,\n  ) => {\n    fields.forEach((field) => {\n      const value = originValues[field];\n      if (value === undefined || value === null) {\n        return;\n      }\n      originValues[field] = transformFn(value, separator);\n    });\n  };\n\n  private updateState() {\n    const currentSchema = this.state?.schema ?? [];\n    const prevSchema = this.prevState?.schema ?? [];\n    // 进行了删除schema操作\n    if (currentSchema.length < prevSchema.length) {\n      const currentFields = new Set(\n        currentSchema.map((item) => item.fieldName),\n      );\n      const deletedSchema = prevSchema.filter(\n        (item) => !currentFields.has(item.fieldName),\n      );\n      for (const schema of deletedSchema) {\n        this.form?.setFieldValue?.(schema.fieldName, undefined);\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "packages/@core/ui-kit/form-ui/src/form-render/context.ts",
    "content": "import type { FormRenderProps } from '../types';\n\nimport { computed } from 'vue';\n\nimport { createContext } from '@vben-core/shadcn-ui';\n\nexport const [injectRenderFormProps, provideFormRenderProps] =\n  createContext<FormRenderProps>('FormRenderProps');\n\nexport const useFormContext = () => {\n  const formRenderProps = injectRenderFormProps();\n\n  const isVertical = computed(() => formRenderProps.layout === 'vertical');\n\n  const componentMap = computed(() => formRenderProps.componentMap);\n  const componentBindEventMap = computed(\n    () => formRenderProps.componentBindEventMap,\n  );\n  return {\n    componentBindEventMap,\n    componentMap,\n    isVertical,\n  };\n};\n"
  },
  {
    "path": "packages/@core/ui-kit/form-ui/src/form-render/dependencies.ts",
    "content": "import type {\n  FormItemDependencies,\n  FormSchemaRuleType,\n  MaybeComponentProps,\n} from '../types';\n\nimport { computed, ref, watch } from 'vue';\n\nimport { isBoolean, isFunction } from '@vben-core/shared/utils';\n\nimport { useFormValues } from 'vee-validate';\n\nimport { injectRenderFormProps } from './context';\n\nexport default function useDependencies(\n  getDependencies: () => FormItemDependencies | undefined,\n) {\n  const values = useFormValues();\n\n  const formRenderProps = injectRenderFormProps();\n\n  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion\n  const formApi = formRenderProps.form!;\n\n  if (!values) {\n    throw new Error('useDependencies should be used within <VbenForm>');\n  }\n\n  const isIf = ref(true);\n  const isDisabled = ref(false);\n  const isShow = ref(true);\n  const isRequired = ref(false);\n  const dynamicComponentProps = ref<MaybeComponentProps>({});\n  const dynamicRules = ref<FormSchemaRuleType>();\n\n  const triggerFieldValues = computed(() => {\n    // 该字段可能会被多个字段触发\n    const triggerFields = getDependencies()?.triggerFields ?? [];\n    return triggerFields.map((dep) => {\n      return values.value[dep];\n    });\n  });\n\n  const resetConditionState = () => {\n    isDisabled.value = false;\n    isIf.value = true;\n    isShow.value = true;\n    isRequired.value = false;\n    dynamicRules.value = undefined;\n    dynamicComponentProps.value = {};\n  };\n\n  watch(\n    [triggerFieldValues, getDependencies],\n    async ([_values, dependencies]) => {\n      if (!dependencies || !dependencies?.triggerFields?.length) {\n        return;\n      }\n      resetConditionState();\n      const {\n        componentProps,\n        disabled,\n        if: whenIf,\n        required,\n        rules,\n        show,\n        trigger,\n      } = dependencies;\n\n      // 1. 优先判断if，如果if为false，则不渲染dom，后续判断也不再执行\n      const formValues = values.value;\n\n      if (isFunction(whenIf)) {\n        isIf.value = !!(await whenIf(formValues, formApi));\n        // 不渲染\n        if (!isIf.value) return;\n      } else if (isBoolean(whenIf)) {\n        isIf.value = whenIf;\n        if (!isIf.value) return;\n      }\n\n      // 2. 判断show，如果show为false，则隐藏\n      if (isFunction(show)) {\n        isShow.value = !!(await show(formValues, formApi));\n        if (!isShow.value) return;\n      } else if (isBoolean(show)) {\n        isShow.value = show;\n        if (!isShow.value) return;\n      }\n\n      if (isFunction(componentProps)) {\n        dynamicComponentProps.value = await componentProps(formValues, formApi);\n      }\n\n      if (isFunction(rules)) {\n        dynamicRules.value = await rules(formValues, formApi);\n      }\n\n      if (isFunction(disabled)) {\n        isDisabled.value = !!(await disabled(formValues, formApi));\n      } else if (isBoolean(disabled)) {\n        isDisabled.value = disabled;\n      }\n\n      if (isFunction(required)) {\n        isRequired.value = !!(await required(formValues, formApi));\n      }\n\n      if (isFunction(trigger)) {\n        await trigger(formValues, formApi);\n      }\n    },\n    { deep: true, immediate: true },\n  );\n\n  return {\n    dynamicComponentProps,\n    dynamicRules,\n    isDisabled,\n    isIf,\n    isRequired,\n    isShow,\n  };\n}\n"
  },
  {
    "path": "packages/@core/ui-kit/form-ui/src/form-render/expandable.ts",
    "content": "import type { FormRenderProps } from '../types';\n\nimport { computed, nextTick, onMounted, ref, useTemplateRef, watch } from 'vue';\n\nimport {\n  breakpointsTailwind,\n  useBreakpoints,\n  useElementVisibility,\n} from '@vueuse/core';\n\n/**\n * 动态计算行数\n */\nexport function useExpandable(props: FormRenderProps) {\n  const wrapperRef = useTemplateRef<HTMLElement>('wrapperRef');\n  const isVisible = useElementVisibility(wrapperRef);\n  const rowMapping = ref<Record<number, number>>({});\n  // 是否已经计算过一次\n  const isCalculated = ref(false);\n\n  const breakpoints = useBreakpoints(breakpointsTailwind);\n\n  const keepFormItemIndex = computed(() => {\n    const rows = props.collapsedRows ?? 1;\n    const mapping = rowMapping.value;\n    let maxItem = 0;\n    for (let index = 1; index <= rows; index++) {\n      maxItem += mapping?.[index] ?? 0;\n    }\n    // 保持一行\n    return maxItem - 1 || 1;\n  });\n\n  watch(\n    [\n      () => props.showCollapseButton,\n      () => breakpoints.active().value,\n      () => props.schema?.length,\n      () => isVisible.value,\n    ],\n    async ([val]) => {\n      if (val) {\n        await nextTick();\n        rowMapping.value = {};\n        isCalculated.value = false;\n        await calculateRowMapping();\n      }\n    },\n  );\n\n  async function calculateRowMapping() {\n    if (!props.showCollapseButton) {\n      return;\n    }\n\n    await nextTick();\n    if (!wrapperRef.value) {\n      return;\n    }\n    // 小屏幕不计算\n    // if (breakpoints.smaller('sm').value) {\n    //   // 保持一行\n    //   rowMapping.value = { 1: 2 };\n    //   return;\n    // }\n\n    const formItems = [...wrapperRef.value.children];\n\n    const container = wrapperRef.value;\n    const containerStyles = window.getComputedStyle(container);\n    const rowHeights = containerStyles\n      .getPropertyValue('grid-template-rows')\n      .split(' ');\n\n    const containerRect = container?.getBoundingClientRect();\n\n    formItems.forEach((el) => {\n      const itemRect = el.getBoundingClientRect();\n\n      // 计算元素在第几行\n      const itemTop = itemRect.top - containerRect.top;\n      let rowStart = 0;\n      let cumulativeHeight = 0;\n\n      for (const [i, rowHeight] of rowHeights.entries()) {\n        cumulativeHeight += Number.parseFloat(rowHeight);\n        if (itemTop < cumulativeHeight) {\n          rowStart = i + 1;\n          break;\n        }\n      }\n      if (rowStart > (props?.collapsedRows ?? 1)) {\n        return;\n      }\n      rowMapping.value[rowStart] = (rowMapping.value[rowStart] ?? 0) + 1;\n      isCalculated.value = true;\n    });\n  }\n\n  onMounted(() => {\n    calculateRowMapping();\n  });\n\n  return { isCalculated, keepFormItemIndex, wrapperRef };\n}\n"
  },
  {
    "path": "packages/@core/ui-kit/form-ui/src/form-render/form-field.vue",
    "content": "<script setup lang=\"ts\">\nimport type { ZodType } from 'zod';\n\nimport type { FormSchema, MaybeComponentProps } from '../types';\n\nimport { computed, nextTick, onUnmounted, useTemplateRef, watch } from 'vue';\n\nimport { CircleAlert } from '@vben-core/icons';\nimport {\n  FormControl,\n  FormDescription,\n  FormField,\n  FormItem,\n  FormMessage,\n  VbenRenderContent,\n  VbenTooltip,\n} from '@vben-core/shadcn-ui';\nimport { cn, isFunction, isObject, isString } from '@vben-core/shared/utils';\n\nimport { toTypedSchema } from '@vee-validate/zod';\nimport { useFieldError, useFormValues } from 'vee-validate';\n\nimport { injectComponentRefMap } from '../use-form-context';\nimport { injectRenderFormProps, useFormContext } from './context';\nimport useDependencies from './dependencies';\nimport FormLabel from './form-label.vue';\nimport { isEventObjectLike } from './helper';\n\ninterface Props extends FormSchema {}\n\nconst {\n  colon,\n  commonComponentProps,\n  component,\n  componentProps,\n  dependencies,\n  description,\n  disabled,\n  disabledOnChangeListener,\n  disabledOnInputListener,\n  emptyStateValue,\n  fieldName,\n  formFieldProps,\n  hide,\n  label,\n  labelClass,\n  labelWidth,\n  modelPropName,\n  renderComponentContent,\n  rules,\n} = defineProps<\n  Props & {\n    commonComponentProps: MaybeComponentProps;\n  }\n>();\n\nconst { componentBindEventMap, componentMap, isVertical } = useFormContext();\nconst formRenderProps = injectRenderFormProps();\nconst values = useFormValues();\nconst errors = useFieldError(fieldName);\nconst fieldComponentRef = useTemplateRef<HTMLInputElement>('fieldComponentRef');\nconst formApi = formRenderProps.form;\nconst compact = computed(() => formRenderProps.compact);\nconst isInValid = computed(() => errors.value?.length > 0);\n\nconst FieldComponent = computed(() => {\n  const finalComponent = isString(component)\n    ? componentMap.value[component]\n    : component;\n  if (!finalComponent) {\n    // 组件未注册\n    console.warn(`Component ${component} is not registered`);\n  }\n  return finalComponent;\n});\n\nconst {\n  dynamicComponentProps,\n  dynamicRules,\n  isDisabled,\n  isIf,\n  isRequired,\n  isShow,\n} = useDependencies(() => dependencies);\n\nconst labelStyle = computed(() => {\n  return labelClass?.includes('w-') || isVertical.value\n    ? {}\n    : {\n        width: `${labelWidth}px`,\n      };\n});\n\nconst currentRules = computed(() => {\n  return dynamicRules.value || rules;\n});\n\nconst visible = computed(() => {\n  return !hide && isIf.value && isShow.value;\n});\n\nconst shouldRequired = computed(() => {\n  if (!visible.value) {\n    return false;\n  }\n\n  if (!currentRules.value) {\n    return isRequired.value;\n  }\n\n  if (isRequired.value) {\n    return true;\n  }\n\n  if (isString(currentRules.value)) {\n    return ['required', 'selectRequired'].includes(currentRules.value);\n  }\n\n  let isOptional = currentRules?.value?.isOptional?.();\n\n  // 如果有设置默认值，则不是必填，需要特殊处理\n  const typeName = currentRules?.value?._def?.typeName;\n  if (typeName === 'ZodDefault') {\n    const innerType = currentRules?.value?._def.innerType;\n    if (innerType) {\n      isOptional = innerType.isOptional?.();\n    }\n  }\n\n  return !isOptional;\n});\n\nconst fieldRules = computed(() => {\n  if (!visible.value) {\n    return null;\n  }\n\n  let rules = currentRules.value;\n  if (!rules) {\n    return isRequired.value ? 'required' : null;\n  }\n\n  if (isString(rules)) {\n    return rules;\n  }\n\n  const isOptional = !shouldRequired.value;\n  if (!isOptional) {\n    const unwrappedRules = (rules as any)?.unwrap?.();\n    if (unwrappedRules) {\n      rules = unwrappedRules;\n    }\n  }\n  return toTypedSchema(rules as ZodType);\n});\n\nconst computedProps = computed(() => {\n  const finalComponentProps = isFunction(componentProps)\n    ? componentProps(values.value, formApi!)\n    : componentProps;\n\n  return {\n    ...commonComponentProps,\n    ...finalComponentProps,\n    ...dynamicComponentProps.value,\n  };\n});\n\nwatch(\n  () => computedProps.value?.autofocus,\n  (value) => {\n    if (value === true) {\n      nextTick(() => {\n        autofocus();\n      });\n    }\n  },\n  { immediate: true },\n);\n\nconst shouldDisabled = computed(() => {\n  return isDisabled.value || disabled || computedProps.value?.disabled;\n});\n\nconst customContentRender = computed(() => {\n  if (!isFunction(renderComponentContent)) {\n    return {};\n  }\n  return renderComponentContent(values.value, formApi!);\n});\n\nconst renderContentKey = computed(() => {\n  return Object.keys(customContentRender.value);\n});\n\nconst fieldProps = computed(() => {\n  const rules = fieldRules.value;\n  return {\n    keepValue: true,\n    label: isString(label) ? label : '',\n    ...(rules ? { rules } : {}),\n    ...(formFieldProps as Record<string, any>),\n  };\n});\n\nfunction fieldBindEvent(slotProps: Record<string, any>) {\n  const modelValue = slotProps.componentField.modelValue;\n  const handler = slotProps.componentField['onUpdate:modelValue'];\n\n  const bindEventField =\n    modelPropName ||\n    (isString(component) ? componentBindEventMap.value?.[component] : null);\n\n  let value = modelValue;\n  // antd design 的一些组件会传递一个 event 对象\n  if (modelValue && isObject(modelValue) && bindEventField) {\n    value = isEventObjectLike(modelValue)\n      ? modelValue?.target?.[bindEventField]\n      : (modelValue?.[bindEventField] ?? modelValue);\n  }\n\n  if (bindEventField) {\n    return {\n      [`onUpdate:${bindEventField}`]: handler,\n      [bindEventField]: value === undefined ? emptyStateValue : value,\n      onChange: disabledOnChangeListener\n        ? undefined\n        : (e: Record<string, any>) => {\n            const shouldUnwrap = isEventObjectLike(e);\n            const onChange = slotProps?.componentField?.onChange;\n            if (!shouldUnwrap) {\n              return onChange?.(e);\n            }\n\n            return onChange?.(e?.target?.[bindEventField] ?? e);\n          },\n      ...(disabledOnInputListener ? { onInput: undefined } : {}),\n    };\n  }\n  return {\n    ...(disabledOnInputListener ? { onInput: undefined } : {}),\n    ...(disabledOnChangeListener ? { onChange: undefined } : {}),\n  };\n}\n\nfunction createComponentProps(slotProps: Record<string, any>) {\n  const bindEvents = fieldBindEvent(slotProps);\n\n  const binds = {\n    ...slotProps.componentField,\n    ...computedProps.value,\n    ...bindEvents,\n    ...(Reflect.has(computedProps.value, 'onChange')\n      ? { onChange: computedProps.value.onChange }\n      : {}),\n    ...(Reflect.has(computedProps.value, 'onInput')\n      ? { onInput: computedProps.value.onInput }\n      : {}),\n  };\n\n  return binds;\n}\n\nfunction autofocus() {\n  if (\n    fieldComponentRef.value &&\n    isFunction(fieldComponentRef.value.focus) &&\n    // 检查当前是否有元素被聚焦\n    document.activeElement !== fieldComponentRef.value\n  ) {\n    fieldComponentRef.value?.focus?.();\n  }\n}\nconst componentRefMap = injectComponentRefMap();\nwatch(fieldComponentRef, (componentRef) => {\n  componentRefMap?.set(fieldName, componentRef);\n});\nonUnmounted(() => {\n  if (componentRefMap?.has(fieldName)) {\n    componentRefMap.delete(fieldName);\n  }\n});\n</script>\n\n<template>\n  <FormField\n    v-if=\"!hide && isIf\"\n    v-bind=\"fieldProps\"\n    v-slot=\"slotProps\"\n    :name=\"fieldName\"\n  >\n    <FormItem\n      v-show=\"isShow\"\n      :class=\"{\n        'form-valid-error': isInValid,\n        'form-is-required': shouldRequired,\n        'flex-col': isVertical,\n        'flex-row items-center': !isVertical,\n        'pb-4': !compact,\n        'pb-2': compact,\n      }\"\n      class=\"relative flex\"\n      v-bind=\"$attrs\"\n    >\n      <FormLabel\n        v-if=\"!hideLabel\"\n        :class=\"\n          cn(\n            'flex leading-6',\n            {\n              'mr-2 flex-shrink-0 justify-end': !isVertical,\n              'mb-1 flex-row': isVertical,\n            },\n            labelClass,\n          )\n        \"\n        :help=\"help\"\n        :colon=\"colon\"\n        :label=\"label\"\n        :required=\"shouldRequired && !hideRequiredMark\"\n        :style=\"labelStyle\"\n      >\n        <template v-if=\"label\">\n          <VbenRenderContent :content=\"label\" />\n        </template>\n      </FormLabel>\n      <div class=\"flex-auto overflow-hidden p-[1px]\">\n        <div :class=\"cn('relative flex w-full items-center', wrapperClass)\">\n          <FormControl :class=\"cn(controlClass)\">\n            <slot\n              v-bind=\"{\n                ...slotProps,\n                ...createComponentProps(slotProps),\n                disabled: shouldDisabled,\n                isInValid,\n              }\"\n            >\n              <component\n                :is=\"FieldComponent\"\n                ref=\"fieldComponentRef\"\n                :class=\"{\n                  'border-destructive focus:border-destructive hover:border-destructive/80 focus:shadow-[0_0_0_2px_rgba(255,38,5,0.06)]':\n                    isInValid,\n                }\"\n                v-bind=\"createComponentProps(slotProps)\"\n                :disabled=\"shouldDisabled\"\n              >\n                <template\n                  v-for=\"name in renderContentKey\"\n                  :key=\"name\"\n                  #[name]=\"renderSlotProps\"\n                >\n                  <VbenRenderContent\n                    :content=\"customContentRender[name]\"\n                    v-bind=\"{ ...renderSlotProps, formContext: slotProps }\"\n                  />\n                </template>\n                <!-- <slot></slot> -->\n              </component>\n              <VbenTooltip\n                v-if=\"compact && isInValid\"\n                :delay-duration=\"300\"\n                side=\"left\"\n              >\n                <template #trigger>\n                  <slot name=\"trigger\">\n                    <CircleAlert\n                      :class=\"\n                        cn(\n                          'text-foreground/80 hover:text-foreground inline-flex size-5 cursor-pointer',\n                        )\n                      \"\n                    />\n                  </slot>\n                </template>\n                <FormMessage />\n              </VbenTooltip>\n            </slot>\n          </FormControl>\n          <!-- 自定义后缀 -->\n          <div v-if=\"suffix\" class=\"ml-1\">\n            <VbenRenderContent :content=\"suffix\" />\n          </div>\n          <FormDescription v-if=\"description\" class=\"ml-1\">\n            <VbenRenderContent :content=\"description\" />\n          </FormDescription>\n        </div>\n\n        <Transition name=\"slide-up\" v-if=\"!compact\">\n          <FormMessage class=\"absolute bottom-[-4px]\" />\n        </Transition>\n      </div>\n    </FormItem>\n  </FormField>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/form-ui/src/form-render/form-label.vue",
    "content": "<script setup lang=\"ts\">\nimport type { CustomRenderType } from '../types';\n\nimport { FormLabel, VbenHelpTooltip } from '@vben-core/shadcn-ui';\nimport { cn } from '@vben-core/shared/utils';\n\ninterface Props {\n  class?: string;\n  colon?: boolean;\n  help?: CustomRenderType;\n  label?: CustomRenderType;\n  required?: boolean;\n}\n\nconst props = defineProps<Props>();\n</script>\n\n<template>\n  <FormLabel :class=\"cn('flex items-center', props.class)\">\n    <span v-if=\"required\" class=\"text-destructive mr-[2px]\">*</span>\n    <slot></slot>\n    <VbenHelpTooltip v-if=\"help\" trigger-class=\"size-3.5 ml-1\">\n      <!-- 可通过\\n换行 -->\n      <span class=\"whitespace-pre-line\">\n        {{ help }}\n      </span>\n      <!-- <VbenRenderContent :content=\"help\" /> -->\n    </VbenHelpTooltip>\n    <span v-if=\"colon && label\" class=\"ml-[2px]\">:</span>\n  </FormLabel>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/form-ui/src/form-render/form.vue",
    "content": "<script setup lang=\"ts\">\nimport type { GenericObject } from 'vee-validate';\nimport type { ZodTypeAny } from 'zod';\n\nimport type {\n  FormCommonConfig,\n  FormRenderProps,\n  FormSchema,\n  FormShape,\n} from '../types';\n\nimport { computed } from 'vue';\n\nimport { Form } from '@vben-core/shadcn-ui';\nimport {\n  cn,\n  isFunction,\n  isString,\n  mergeWithArrayOverride,\n} from '@vben-core/shared/utils';\n\nimport { provideFormRenderProps } from './context';\nimport { useExpandable } from './expandable';\nimport FormField from './form-field.vue';\nimport { getBaseRules, getDefaultValueInZodStack } from './helper';\n\ninterface Props extends FormRenderProps {}\n\nconst props = withDefaults(\n  defineProps<Props & { globalCommonConfig?: FormCommonConfig }>(),\n  {\n    collapsedRows: 1,\n    commonConfig: () => ({}),\n    globalCommonConfig: () => ({}),\n    showCollapseButton: false,\n    wrapperClass: 'grid-cols-1 sm:grid-cols-2 md:grid-cols-3',\n  },\n);\n\nconst emits = defineEmits<{\n  submit: [event: any];\n}>();\n\nconst wrapperClass = computed(() => {\n  const cls = ['flex'];\n  if (props.layout === 'inline') {\n    cls.push('flex-wrap gap-x-2');\n  } else {\n    cls.push(props.compact ? 'gap-x-2' : 'gap-x-4', 'flex-col grid gap-y-1.5');\n  }\n  return cn(...cls, props.wrapperClass);\n});\n\nprovideFormRenderProps(props);\n\nconst { isCalculated, keepFormItemIndex, wrapperRef } = useExpandable(props);\n\nconst shapes = computed(() => {\n  const resultShapes: FormShape[] = [];\n  props.schema?.forEach((schema) => {\n    const { fieldName } = schema;\n    const rules = schema.rules as ZodTypeAny;\n\n    let typeName = '';\n    if (rules && !isString(rules)) {\n      typeName = rules._def.typeName;\n    }\n\n    const baseRules = getBaseRules(rules) as ZodTypeAny;\n\n    resultShapes.push({\n      default: getDefaultValueInZodStack(rules),\n      fieldName,\n      required: !['ZodNullable', 'ZodOptional'].includes(typeName),\n      rules: baseRules,\n    });\n  });\n  return resultShapes;\n});\n\nconst formComponent = computed(() => (props.form ? 'form' : Form));\n\nconst formComponentProps = computed(() => {\n  return props.form\n    ? {\n        onSubmit: props.form.handleSubmit((val) => emits('submit', val)),\n      }\n    : {\n        onSubmit: (val: GenericObject) => emits('submit', val),\n      };\n});\n\nconst formCollapsed = computed(() => {\n  return props.collapsed && isCalculated.value;\n});\n\nconst computedSchema = computed(\n  (): (Omit<FormSchema, 'formFieldProps'> & {\n    commonComponentProps: Record<string, any>;\n    formFieldProps: Record<string, any>;\n  })[] => {\n    const {\n      colon = false,\n      componentProps = {},\n      controlClass = '',\n      disabled,\n      disabledOnChangeListener = true,\n      disabledOnInputListener = true,\n      emptyStateValue = undefined,\n      formFieldProps = {},\n      formItemClass = '',\n      hideLabel = false,\n      hideRequiredMark = false,\n      labelClass = '',\n      labelWidth = 100,\n      modelPropName = '',\n      wrapperClass = '',\n    } = mergeWithArrayOverride(props.commonConfig, props.globalCommonConfig);\n    return (props.schema || []).map((schema, index) => {\n      const keepIndex = keepFormItemIndex.value;\n\n      const hidden =\n        // 折叠状态 & 显示折叠按钮 & 当前索引大于保留索引\n        props.showCollapseButton && !!formCollapsed.value && keepIndex\n          ? keepIndex <= index\n          : false;\n\n      // 处理函数形式的formItemClass\n      let resolvedSchemaFormItemClass = schema.formItemClass;\n      if (isFunction(schema.formItemClass)) {\n        try {\n          resolvedSchemaFormItemClass = schema.formItemClass();\n        } catch (error) {\n          console.error('Error calling formItemClass function:', error);\n          resolvedSchemaFormItemClass = '';\n        }\n      }\n\n      return {\n        colon,\n        disabled,\n        disabledOnChangeListener,\n        disabledOnInputListener,\n        emptyStateValue,\n        hideLabel,\n        hideRequiredMark,\n        labelWidth,\n        modelPropName,\n        wrapperClass,\n        ...schema,\n        commonComponentProps: componentProps,\n        componentProps: schema.componentProps,\n        controlClass: cn(controlClass, schema.controlClass),\n        formFieldProps: {\n          ...formFieldProps,\n          ...schema.formFieldProps,\n        },\n        formItemClass: cn(\n          'flex-shrink-0',\n          { hidden },\n          formItemClass,\n          resolvedSchemaFormItemClass,\n        ),\n        labelClass: cn(labelClass, schema.labelClass),\n      };\n    });\n  },\n);\n</script>\n\n<template>\n  <component :is=\"formComponent\" v-bind=\"formComponentProps\">\n    <div ref=\"wrapperRef\" :class=\"wrapperClass\">\n      <template v-for=\"cSchema in computedSchema\" :key=\"cSchema.fieldName\">\n        <!-- <div v-if=\"$slots[cSchema.fieldName]\" :class=\"cSchema.formItemClass\">\n          <slot :definition=\"cSchema\" :name=\"cSchema.fieldName\"> </slot>\n        </div> -->\n        <FormField\n          v-bind=\"cSchema\"\n          :class=\"cSchema.formItemClass\"\n          :rules=\"cSchema.rules\"\n        >\n          <template #default=\"slotProps\">\n            <slot v-bind=\"slotProps\" :name=\"cSchema.fieldName\"> </slot>\n          </template>\n        </FormField>\n      </template>\n      <slot :shapes=\"shapes\"></slot>\n    </div>\n  </component>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/form-ui/src/form-render/helper.ts",
    "content": "import type {\n  AnyZodObject,\n  ZodDefault,\n  ZodEffects,\n  ZodNumber,\n  ZodString,\n  ZodTypeAny,\n} from 'zod';\n\nimport { isObject, isString } from '@vben-core/shared/utils';\n\n/**\n * Get the lowest level Zod type.\n * This will unpack optionals, refinements, etc.\n */\nexport function getBaseRules<\n  ChildType extends AnyZodObject | ZodTypeAny = ZodTypeAny,\n>(schema: ChildType | ZodEffects<ChildType>): ChildType | null {\n  if (!schema || isString(schema)) return null;\n  if ('innerType' in schema._def)\n    return getBaseRules(schema._def.innerType as ChildType);\n\n  if ('schema' in schema._def)\n    return getBaseRules(schema._def.schema as ChildType);\n\n  return schema as ChildType;\n}\n\n/**\n * Search for a \"ZodDefault\" in the Zod stack and return its value.\n */\nexport function getDefaultValueInZodStack(schema: ZodTypeAny): any {\n  if (!schema || isString(schema)) {\n    return;\n  }\n  const typedSchema = schema as unknown as ZodDefault<ZodNumber | ZodString>;\n\n  if (typedSchema._def.typeName === 'ZodDefault')\n    return typedSchema._def.defaultValue();\n\n  if ('innerType' in typedSchema._def) {\n    return getDefaultValueInZodStack(\n      typedSchema._def.innerType as unknown as ZodTypeAny,\n    );\n  }\n  if ('schema' in typedSchema._def) {\n    return getDefaultValueInZodStack(\n      (typedSchema._def as any).schema as ZodTypeAny,\n    );\n  }\n\n  return undefined;\n}\n\nexport function isEventObjectLike(obj: any) {\n  if (!obj || !isObject(obj)) {\n    return false;\n  }\n  return Reflect.has(obj, 'target') && Reflect.has(obj, 'stopPropagation');\n}\n"
  },
  {
    "path": "packages/@core/ui-kit/form-ui/src/form-render/index.ts",
    "content": "export { default as Form } from './form.vue';\nexport { default as FormField } from './form-field.vue';\nexport { default as FormLabel } from './form-label.vue';\n"
  },
  {
    "path": "packages/@core/ui-kit/form-ui/src/index.ts",
    "content": "export { setupVbenForm } from './config';\n\nexport type {\n  BaseFormComponentType,\n  ExtendedFormApi,\n  FormSchema as VbenFormSchema,\n  VbenFormProps,\n} from './types';\n\nexport * from './use-vben-form';\n// export { default as VbenForm } from './vben-form.vue';\nexport * as z from 'zod';\n"
  },
  {
    "path": "packages/@core/ui-kit/form-ui/src/types.ts",
    "content": "import type { FieldOptions, FormContext, GenericObject } from 'vee-validate';\nimport type { ZodTypeAny } from 'zod';\n\nimport type { Component, HtmlHTMLAttributes, Ref } from 'vue';\n\nimport type { VbenButtonProps } from '@vben-core/shadcn-ui';\nimport type { ClassType, MaybeComputedRef } from '@vben-core/typings';\n\nimport type { FormApi } from './form-api';\n\nexport type FormLayout = 'horizontal' | 'inline' | 'vertical';\n\nexport type BaseFormComponentType =\n  | 'DefaultButton'\n  | 'PrimaryButton'\n  | 'VbenCheckbox'\n  | 'VbenInput'\n  | 'VbenInputPassword'\n  | 'VbenPinInput'\n  | 'VbenSelect'\n  | (Record<never, never> & string);\n\ntype Breakpoints = '2xl:' | '3xl:' | '' | 'lg:' | 'md:' | 'sm:' | 'xl:';\n\ntype GridCols = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13;\n\nexport type WrapperClassType =\n  | `${Breakpoints}grid-cols-${GridCols}`\n  | (Record<never, never> & string);\n\nexport type FormItemClassType =\n  | `${Breakpoints}cols-end-${'auto' | GridCols}`\n  | `${Breakpoints}cols-span-${'auto' | 'full' | GridCols}`\n  | `${Breakpoints}cols-start-${'auto' | GridCols}`\n  | (Record<never, never> & string)\n  | WrapperClassType;\n\nexport type FormFieldOptions = Partial<\n  FieldOptions & {\n    validateOnBlur?: boolean;\n    validateOnChange?: boolean;\n    validateOnInput?: boolean;\n    validateOnModelUpdate?: boolean;\n  }\n>;\n\nexport interface FormShape {\n  /** 默认值 */\n  default?: any;\n  /** 字段名 */\n  fieldName: string;\n  /** 是否必填 */\n  required?: boolean;\n  rules?: ZodTypeAny;\n}\n\nexport type MaybeComponentPropKey =\n  | 'options'\n  | 'placeholder'\n  | 'title'\n  | keyof HtmlHTMLAttributes\n  | (Record<never, never> & string);\n\nexport type MaybeComponentProps = { [K in MaybeComponentPropKey]?: any };\n\nexport type FormActions = FormContext<GenericObject>;\n\nexport type CustomRenderType = (() => Component | string) | string;\n\nexport type FormSchemaRuleType =\n  | 'required'\n  | 'selectRequired'\n  | null\n  | (Record<never, never> & string)\n  | ZodTypeAny;\n\ntype FormItemDependenciesCondition<T = boolean | PromiseLike<boolean>> = (\n  value: Partial<Record<string, any>>,\n  actions: FormActions,\n) => T;\n\ntype FormItemDependenciesConditionWithRules = (\n  value: Partial<Record<string, any>>,\n  actions: FormActions,\n) => FormSchemaRuleType | PromiseLike<FormSchemaRuleType>;\n\ntype FormItemDependenciesConditionWithProps = (\n  value: Partial<Record<string, any>>,\n  actions: FormActions,\n) => MaybeComponentProps | PromiseLike<MaybeComponentProps>;\n\nexport interface FormItemDependencies {\n  /**\n   * 组件参数\n   * @returns 组件参数\n   */\n  componentProps?: FormItemDependenciesConditionWithProps;\n  /**\n   * 是否禁用\n   * @returns 是否禁用\n   */\n  disabled?: boolean | FormItemDependenciesCondition;\n  /**\n   * 是否渲染（删除dom）\n   * @returns 是否渲染\n   */\n  if?: boolean | FormItemDependenciesCondition;\n  /**\n   * 是否必填\n   * @returns 是否必填\n   */\n  required?: FormItemDependenciesCondition;\n  /**\n   * 字段规则\n   */\n  rules?: FormItemDependenciesConditionWithRules;\n  /**\n   * 是否隐藏(Css)\n   * @returns 是否隐藏\n   */\n  show?: boolean | FormItemDependenciesCondition;\n  /**\n   * 任意触发都会执行\n   */\n  trigger?: FormItemDependenciesCondition<void>;\n  /**\n   * 触发字段\n   */\n  triggerFields: string[];\n}\n\ntype ComponentProps =\n  | ((\n      value: Partial<Record<string, any>>,\n      actions: FormActions,\n    ) => MaybeComponentProps)\n  | MaybeComponentProps;\n\nexport interface FormCommonConfig {\n  /**\n   * 在Label后显示一个冒号\n   */\n  colon?: boolean;\n  /**\n   * 所有表单项的props\n   */\n  componentProps?: ComponentProps;\n  /**\n   * 所有表单项的控件样式\n   */\n  controlClass?: string;\n  /**\n   * 所有表单项的禁用状态\n   * @default false\n   */\n  disabled?: boolean;\n  /**\n   * 是否禁用所有表单项的change事件监听\n   * @default true\n   */\n  disabledOnChangeListener?: boolean;\n  /**\n   * 是否禁用所有表单项的input事件监听\n   * @default true\n   */\n  disabledOnInputListener?: boolean;\n  /**\n   * 所有表单项的空状态值,默认都是undefined，naive-ui的空状态值是null\n   */\n  emptyStateValue?: null | undefined;\n  /**\n   * 所有表单项的控件样式\n   * @default {}\n   */\n  formFieldProps?: FormFieldOptions;\n  /**\n   * 所有表单项的栅格布局，支持函数形式\n   * @default \"\"\n   */\n  formItemClass?: (() => string) | string;\n  /**\n   * 隐藏所有表单项label\n   * @default false\n   */\n  hideLabel?: boolean;\n  /**\n   * 是否隐藏必填标记\n   * @default false\n   */\n  hideRequiredMark?: boolean;\n  /**\n   * 所有表单项的label样式\n   * @default \"\"\n   */\n  labelClass?: string;\n  /**\n   * 所有表单项的label宽度\n   */\n  labelWidth?: number;\n  /**\n   * 所有表单项的model属性名\n   * @default \"modelValue\"\n   */\n  modelPropName?: string;\n  /**\n   * 所有表单项的wrapper样式\n   */\n  wrapperClass?: string;\n}\n\ntype RenderComponentContentType = (\n  value: Partial<Record<string, any>>,\n  api: FormActions,\n) => Record<string, any>;\n\nexport type HandleSubmitFn = (\n  values: Record<string, any>,\n) => Promise<void> | void;\n\nexport type HandleResetFn = (\n  values: Record<string, any>,\n) => Promise<void> | void;\n\nexport type FieldMappingTime = [\n  string,\n  [string, string],\n  (\n    | ((value: any, fieldName: string) => any)\n    | [string, string]\n    | null\n    | string\n  )?,\n][];\n\nexport type ArrayToStringFields = Array<\n  | [string[], string?] // 嵌套数组格式，可选分隔符\n  | string // 单个字段，使用默认分隔符\n  | string[] // 简单数组格式，最后一个元素可以是分隔符\n>;\n\nexport interface FormSchema<\n  T extends BaseFormComponentType = BaseFormComponentType,\n> extends FormCommonConfig {\n  /** 组件 */\n  component: Component | T;\n  /** 组件参数 */\n  componentProps?: ComponentProps;\n  /** 默认值 */\n  defaultValue?: any;\n  /** 依赖 */\n  dependencies?: FormItemDependencies;\n  /** 描述 */\n  description?: CustomRenderType;\n  /** 字段名 */\n  fieldName: string;\n  /** 帮助信息 */\n  help?: CustomRenderType;\n  /** 是否隐藏表单项 */\n  hide?: boolean;\n  /** 表单项 */\n  label?: CustomRenderType;\n  // 自定义组件内部渲染\n  renderComponentContent?: RenderComponentContentType;\n  /** 字段规则 */\n  rules?: FormSchemaRuleType;\n  /** 后缀 */\n  suffix?: CustomRenderType;\n}\n\nexport interface FormFieldProps extends FormSchema {\n  required?: boolean;\n}\n\nexport interface FormRenderProps<\n  T extends BaseFormComponentType = BaseFormComponentType,\n> {\n  /**\n   * 表单字段数组映射字符串配置 默认使用\",\"\n   */\n  arrayToStringFields?: ArrayToStringFields;\n  /**\n   * 是否折叠，在showCollapseButton=true下生效\n   * true:折叠 false:展开\n   */\n  collapsed?: boolean;\n  /**\n   * 折叠时保持行数\n   * @default 1\n   */\n  collapsedRows?: number;\n  /**\n   * 是否触发resize事件\n   * @default false\n   */\n  collapseTriggerResize?: boolean;\n  /**\n   * 表单项通用后备配置，当子项目没配置时使用这里的配置，子项目配置优先级高于此配置\n   */\n  commonConfig?: FormCommonConfig;\n  /**\n   * 紧凑模式（移除表单每一项底部为校验信息预留的空间）\n   */\n  compact?: boolean;\n  /**\n   * 组件v-model事件绑定\n   */\n  componentBindEventMap?: Partial<Record<BaseFormComponentType, string>>;\n  /**\n   * 组件集合\n   */\n  componentMap: Record<BaseFormComponentType, Component>;\n  /**\n   * 表单字段映射到时间格式\n   */\n  fieldMappingTime?: FieldMappingTime;\n  /**\n   * 表单实例\n   */\n  form?: FormContext<GenericObject>;\n  /**\n   * 表单项布局\n   */\n  layout?: FormLayout;\n  /**\n   * 表单定义\n   */\n  schema?: FormSchema<T>[];\n\n  /**\n   * 是否显示展开/折叠\n   */\n  showCollapseButton?: boolean;\n  /**\n   * 格式化日期\n   */\n\n  /**\n   * 表单栅格布局\n   * @default \"grid-cols-1\"\n   */\n  wrapperClass?: WrapperClassType;\n}\n\nexport interface ActionButtonOptions extends VbenButtonProps {\n  [key: string]: any;\n  content?: MaybeComputedRef<string>;\n  show?: boolean;\n}\n\nexport interface VbenFormProps<\n  T extends BaseFormComponentType = BaseFormComponentType,\n> extends Omit<\n    FormRenderProps<T>,\n    'componentBindEventMap' | 'componentMap' | 'form'\n  > {\n  /**\n   * 操作按钮是否反转（提交按钮前置）\n   */\n  actionButtonsReverse?: boolean;\n  /**\n   * 操作按钮组的样式\n   * newLine: 在新行显示。rowEnd: 在行内显示，靠右对齐（默认）。inline: 使用grid默认样式\n   */\n  actionLayout?: 'inline' | 'newLine' | 'rowEnd';\n  /**\n   * 操作按钮组显示位置，默认靠右显示\n   */\n  actionPosition?: 'center' | 'left' | 'right';\n  /**\n   * 表单操作区域class\n   */\n  actionWrapperClass?: ClassType;\n  /**\n   * 表单字段数组映射字符串配置 默认使用\",\"\n   */\n  arrayToStringFields?: ArrayToStringFields;\n\n  /**\n   * 表单字段映射\n   */\n  fieldMappingTime?: FieldMappingTime;\n  /**\n   * 表单重置回调\n   */\n  handleReset?: HandleResetFn;\n  /**\n   * 表单提交回调\n   */\n  handleSubmit?: HandleSubmitFn;\n  /**\n   * 表单值变化回调\n   */\n  handleValuesChange?: (\n    values: Record<string, any>,\n    fieldsChanged: string[],\n  ) => void;\n  /**\n   * 重置按钮参数\n   */\n  resetButtonOptions?: ActionButtonOptions;\n\n  /**\n   * 验证失败时是否自动滚动到第一个错误字段\n   * @default false\n   */\n  scrollToFirstError?: boolean;\n\n  /**\n   * 是否显示默认操作按钮\n   * @default true\n   */\n  showDefaultActions?: boolean;\n\n  /**\n   * 提交按钮参数\n   */\n  submitButtonOptions?: ActionButtonOptions;\n\n  /**\n   * 是否在字段值改变时提交表单\n   * @default false\n   */\n  submitOnChange?: boolean;\n\n  /**\n   * 是否在回车时提交表单\n   * @default false\n   */\n  submitOnEnter?: boolean;\n}\n\nexport type ExtendedFormApi = FormApi & {\n  useStore: <T = NoInfer<VbenFormProps>>(\n    selector?: (state: NoInfer<VbenFormProps>) => T,\n  ) => Readonly<Ref<T>>;\n};\n\nexport interface VbenFormAdapterOptions<\n  T extends BaseFormComponentType = BaseFormComponentType,\n> {\n  config?: {\n    baseModelPropName?: string;\n    disabledOnChangeListener?: boolean;\n    disabledOnInputListener?: boolean;\n    emptyStateValue?: null | undefined;\n    modelPropNameMap?: Partial<Record<T, string>>;\n  };\n  defineRules?: {\n    required?: (\n      value: any,\n      params: any,\n      ctx: Record<string, any>,\n    ) => boolean | string;\n    selectRequired?: (\n      value: any,\n      params: any,\n      ctx: Record<string, any>,\n    ) => boolean | string;\n  };\n}\n"
  },
  {
    "path": "packages/@core/ui-kit/form-ui/src/use-form-context.ts",
    "content": "import type { ZodRawShape } from 'zod';\n\nimport type { ComputedRef } from 'vue';\n\nimport type { ExtendedFormApi, FormActions, VbenFormProps } from './types';\n\nimport { computed, unref, useSlots } from 'vue';\n\nimport { createContext } from '@vben-core/shadcn-ui';\nimport { isString, mergeWithArrayOverride, set } from '@vben-core/shared/utils';\n\nimport { useForm } from 'vee-validate';\nimport { object, ZodIntersection, ZodNumber, ZodObject, ZodString } from 'zod';\nimport { getDefaultsForSchema } from 'zod-defaults';\n\ntype ExtendFormProps = VbenFormProps & { formApi: ExtendedFormApi };\n\nexport const [injectFormProps, provideFormProps] =\n  createContext<[ComputedRef<ExtendFormProps> | ExtendFormProps, FormActions]>(\n    'VbenFormProps',\n  );\n\nexport const [injectComponentRefMap, provideComponentRefMap] =\n  createContext<Map<string, unknown>>('ComponentRefMap');\n\nexport function useFormInitial(\n  props: ComputedRef<VbenFormProps> | VbenFormProps,\n) {\n  const slots = useSlots();\n  const initialValues = generateInitialValues();\n\n  const form = useForm({\n    ...(Object.keys(initialValues)?.length ? { initialValues } : {}),\n  });\n\n  const delegatedSlots = computed(() => {\n    const resultSlots: string[] = [];\n\n    for (const key of Object.keys(slots)) {\n      if (key !== 'default') {\n        resultSlots.push(key);\n      }\n    }\n    return resultSlots;\n  });\n\n  function generateInitialValues() {\n    const initialValues: Record<string, any> = {};\n\n    const zodObject: ZodRawShape = {};\n    (unref(props).schema || []).forEach((item) => {\n      if (Reflect.has(item, 'defaultValue')) {\n        set(initialValues, item.fieldName, item.defaultValue);\n      } else if (item.rules && !isString(item.rules)) {\n        // 检查规则是否适合提取默认值\n        const customDefaultValue = getCustomDefaultValue(item.rules);\n        zodObject[item.fieldName] = item.rules;\n        if (customDefaultValue !== undefined) {\n          initialValues[item.fieldName] = customDefaultValue;\n        }\n      }\n    });\n\n    const schemaInitialValues = getDefaultsForSchema(object(zodObject));\n\n    const zodDefaults: Record<string, any> = {};\n    for (const key in schemaInitialValues) {\n      set(zodDefaults, key, schemaInitialValues[key]);\n    }\n    return mergeWithArrayOverride(initialValues, zodDefaults);\n  }\n  // 自定义默认值提取逻辑\n  function getCustomDefaultValue(rule: any): any {\n    if (rule instanceof ZodString) {\n      return ''; // 默认为空字符串\n    } else if (rule instanceof ZodNumber) {\n      return null; // 默认为 null（避免显示 0）\n    } else if (rule instanceof ZodObject) {\n      // 递归提取嵌套对象的默认值\n      const defaultValues: Record<string, any> = {};\n      for (const [key, valueSchema] of Object.entries(rule.shape)) {\n        defaultValues[key] = getCustomDefaultValue(valueSchema);\n      }\n      return defaultValues;\n    } else if (rule instanceof ZodIntersection) {\n      // 对于交集类型，从schema 提取默认值\n      const leftDefaultValue = getCustomDefaultValue(rule._def.left);\n      const rightDefaultValue = getCustomDefaultValue(rule._def.right);\n\n      // 如果左右两边都能提取默认值，合并它们\n      if (\n        typeof leftDefaultValue === 'object' &&\n        typeof rightDefaultValue === 'object'\n      ) {\n        return { ...leftDefaultValue, ...rightDefaultValue };\n      }\n\n      // 否则优先使用左边的默认值\n      return leftDefaultValue ?? rightDefaultValue;\n    } else {\n      return undefined; // 其他类型不提供默认值\n    }\n  }\n\n  return {\n    delegatedSlots,\n    form,\n  };\n}\n"
  },
  {
    "path": "packages/@core/ui-kit/form-ui/src/use-vben-form.ts",
    "content": "import type {\n  BaseFormComponentType,\n  ExtendedFormApi,\n  VbenFormProps,\n} from './types';\n\nimport { defineComponent, h, isReactive, onBeforeUnmount, watch } from 'vue';\n\nimport { useStore } from '@vben-core/shared/store';\n\nimport { FormApi } from './form-api';\nimport VbenUseForm from './vben-use-form.vue';\n\nexport function useVbenForm<\n  T extends BaseFormComponentType = BaseFormComponentType,\n>(options: VbenFormProps<T>) {\n  const IS_REACTIVE = isReactive(options);\n  const api = new FormApi(options);\n  const extendedApi: ExtendedFormApi = api as never;\n  extendedApi.useStore = (selector) => {\n    return useStore(api.store, selector);\n  };\n\n  const Form = defineComponent(\n    (props: VbenFormProps, { attrs, slots }) => {\n      onBeforeUnmount(() => {\n        api.unmount();\n      });\n      api.setState({ ...props, ...attrs });\n      return () =>\n        h(VbenUseForm, { ...props, ...attrs, formApi: extendedApi }, slots);\n    },\n    {\n      name: 'VbenUseForm',\n      inheritAttrs: false,\n    },\n  );\n  // Add reactivity support\n  if (IS_REACTIVE) {\n    watch(\n      () => options.schema,\n      () => {\n        api.setState({ schema: options.schema });\n      },\n      { immediate: true },\n    );\n  }\n\n  return [Form, extendedApi] as const;\n}\n"
  },
  {
    "path": "packages/@core/ui-kit/form-ui/src/vben-form.vue",
    "content": "<script setup lang=\"ts\">\nimport type { VbenFormProps } from './types';\n\nimport { ref, watchEffect } from 'vue';\n\nimport { useForwardPropsEmits } from '@vben-core/composables';\n\nimport FormActions from './components/form-actions.vue';\nimport {\n  COMPONENT_BIND_EVENT_MAP,\n  COMPONENT_MAP,\n  DEFAULT_FORM_COMMON_CONFIG,\n} from './config';\nimport { Form } from './form-render';\nimport { provideFormProps, useFormInitial } from './use-form-context';\n\n// 通过 extends 会导致热更新卡死\ninterface Props extends VbenFormProps {}\nconst props = withDefaults(defineProps<Props>(), {\n  actionWrapperClass: '',\n  collapsed: false,\n  collapsedRows: 1,\n  commonConfig: () => ({}),\n  handleReset: undefined,\n  handleSubmit: undefined,\n  layout: 'horizontal',\n  resetButtonOptions: () => ({}),\n  showCollapseButton: false,\n  showDefaultActions: true,\n  submitButtonOptions: () => ({}),\n  wrapperClass: 'grid-cols-1',\n});\n\nconst forward = useForwardPropsEmits(props);\n\nconst currentCollapsed = ref(false);\n\nconst { delegatedSlots, form } = useFormInitial(props);\n\nprovideFormProps([props, form]);\n\nconst handleUpdateCollapsed = (value: boolean) => {\n  currentCollapsed.value = !!value;\n};\n\nwatchEffect(() => {\n  currentCollapsed.value = props.collapsed;\n});\n</script>\n\n<template>\n  <Form\n    v-bind=\"forward\"\n    :collapsed=\"currentCollapsed\"\n    :component-bind-event-map=\"COMPONENT_BIND_EVENT_MAP\"\n    :component-map=\"COMPONENT_MAP\"\n    :form=\"form\"\n    :global-common-config=\"DEFAULT_FORM_COMMON_CONFIG\"\n  >\n    <template\n      v-for=\"slotName in delegatedSlots\"\n      :key=\"slotName\"\n      #[slotName]=\"slotProps\"\n    >\n      <slot :name=\"slotName\" v-bind=\"slotProps\"></slot>\n    </template>\n    <template #default=\"slotProps\">\n      <slot v-bind=\"slotProps\">\n        <FormActions\n          v-if=\"showDefaultActions\"\n          :model-value=\"currentCollapsed\"\n          @update:model-value=\"handleUpdateCollapsed\"\n        />\n      </slot>\n    </template>\n  </Form>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/form-ui/src/vben-use-form.vue",
    "content": "<script setup lang=\"ts\">\nimport type { Recordable } from '@vben-core/typings';\n\nimport type { ExtendedFormApi, VbenFormProps } from './types';\n\n// import { toRaw, watch } from 'vue';\nimport { nextTick, onMounted, watch } from 'vue';\n\nimport { useForwardPriorityValues } from '@vben-core/composables';\nimport { cloneDeep, get, isEqual, set } from '@vben-core/shared/utils';\n\nimport { useDebounceFn } from '@vueuse/core';\n\nimport FormActions from './components/form-actions.vue';\nimport {\n  COMPONENT_BIND_EVENT_MAP,\n  COMPONENT_MAP,\n  DEFAULT_FORM_COMMON_CONFIG,\n} from './config';\nimport { Form } from './form-render';\nimport {\n  provideComponentRefMap,\n  provideFormProps,\n  useFormInitial,\n} from './use-form-context';\n// 通过 extends 会导致热更新卡死，所以重复写了一遍\ninterface Props extends VbenFormProps {\n  formApi: ExtendedFormApi;\n}\n\nconst props = defineProps<Props>();\n\nconst state = props.formApi?.useStore?.();\n\nconst forward = useForwardPriorityValues(props, state);\n\nconst componentRefMap = new Map<string, unknown>();\n\nconst { delegatedSlots, form } = useFormInitial(forward);\n\nprovideFormProps([forward, form]);\nprovideComponentRefMap(componentRefMap);\n\nprops.formApi?.mount?.(form, componentRefMap);\n\nconst handleUpdateCollapsed = (value: boolean) => {\n  props.formApi?.setState({ collapsed: !!value });\n};\n\nfunction handleKeyDownEnter(event: KeyboardEvent) {\n  if (!state.value.submitOnEnter || !forward.value.formApi?.isMounted) {\n    return;\n  }\n  // 如果是 textarea 不阻止默认行为，否则会导致无法换行。\n  // 跳过 textarea 的回车提交处理\n  if (event.target instanceof HTMLTextAreaElement) {\n    return;\n  }\n  event.preventDefault();\n\n  forward.value.formApi.validateAndSubmitForm();\n}\n\nconst handleValuesChangeDebounced = useDebounceFn(async () => {\n  state.value.submitOnChange && forward.value.formApi?.validateAndSubmitForm();\n}, 300);\n\nconst valuesCache: Recordable<any> = {};\n\nonMounted(async () => {\n  // 只在挂载后开始监听，form.values会有一个初始化的过程\n  await nextTick();\n  watch(\n    () => form.values,\n    async (newVal) => {\n      if (forward.value.handleValuesChange) {\n        const fields = state.value.schema?.map((item) => {\n          return item.fieldName;\n        });\n\n        if (fields && fields.length > 0) {\n          const changedFields: string[] = [];\n          fields.forEach((field) => {\n            const newFieldValue = get(newVal, field);\n            const oldFieldValue = get(valuesCache, field);\n            if (!isEqual(newFieldValue, oldFieldValue)) {\n              changedFields.push(field);\n              set(valuesCache, field, newFieldValue);\n            }\n          });\n\n          if (changedFields.length > 0) {\n            // 调用handleValuesChange回调，传入所有表单值的深拷贝和变更的字段列表\n            forward.value.handleValuesChange(\n              cloneDeep(await forward.value.formApi.getValues()),\n              changedFields,\n            );\n          }\n        }\n      }\n      handleValuesChangeDebounced();\n    },\n    { deep: true },\n  );\n});\n</script>\n\n<template>\n  <Form\n    @keydown.enter=\"handleKeyDownEnter\"\n    v-bind=\"forward\"\n    :collapsed=\"state.collapsed\"\n    :component-bind-event-map=\"COMPONENT_BIND_EVENT_MAP\"\n    :component-map=\"COMPONENT_MAP\"\n    :form=\"form\"\n    :global-common-config=\"DEFAULT_FORM_COMMON_CONFIG\"\n  >\n    <template\n      v-for=\"slotName in delegatedSlots\"\n      :key=\"slotName\"\n      #[slotName]=\"slotProps\"\n    >\n      <slot :name=\"slotName\" v-bind=\"slotProps\"></slot>\n    </template>\n    <template #default=\"slotProps\">\n      <slot v-bind=\"slotProps\">\n        <FormActions\n          v-if=\"forward.showDefaultActions\"\n          :model-value=\"state.collapsed\"\n          @update:model-value=\"handleUpdateCollapsed\"\n        >\n          <template #reset-before=\"resetSlotProps\">\n            <slot name=\"reset-before\" v-bind=\"resetSlotProps\"></slot>\n          </template>\n          <template #submit-before=\"submitSlotProps\">\n            <slot name=\"submit-before\" v-bind=\"submitSlotProps\"></slot>\n          </template>\n          <template #expand-before=\"expandBeforeSlotProps\">\n            <slot name=\"expand-before\" v-bind=\"expandBeforeSlotProps\"></slot>\n          </template>\n          <template #expand-after=\"expandAfterSlotProps\">\n            <slot name=\"expand-after\" v-bind=\"expandAfterSlotProps\"></slot>\n          </template>\n        </FormActions>\n      </slot>\n    </template>\n  </Form>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/form-ui/tailwind.config.mjs",
    "content": "export { default } from '@vben/tailwind-config';\n"
  },
  {
    "path": "packages/@core/ui-kit/form-ui/tsconfig.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/tsconfig\",\n  \"extends\": \"@vben/tsconfig/web.json\",\n  \"include\": [\"src\", \"__tests__\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "packages/@core/ui-kit/layout-ui/build.config.ts",
    "content": "import { defineBuildConfig } from 'unbuild';\n\nexport default defineBuildConfig({\n  clean: true,\n  declaration: true,\n  entries: [\n    {\n      builder: 'mkdist',\n      input: './src',\n      loaders: ['vue'],\n      pattern: ['**/*.vue'],\n    },\n    {\n      builder: 'mkdist',\n      format: 'esm',\n      input: './src',\n      loaders: ['js'],\n      pattern: ['**/*.ts'],\n    },\n  ],\n});\n"
  },
  {
    "path": "packages/@core/ui-kit/layout-ui/package.json",
    "content": "{\n  \"name\": \"@vben-core/layout-ui\",\n  \"version\": \"5.5.9\",\n  \"homepage\": \"https://github.com/vbenjs/vue-vben-admin\",\n  \"bugs\": \"https://github.com/vbenjs/vue-vben-admin/issues\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/vbenjs/vue-vben-admin.git\",\n    \"directory\": \"packages/@vben-core/uikit/layout-ui\"\n  },\n  \"license\": \"MIT\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"build\": \"pnpm unbuild\",\n    \"prepublishOnly\": \"npm run build\"\n  },\n  \"files\": [\n    \"dist\"\n  ],\n  \"sideEffects\": [\n    \"**/*.css\"\n  ],\n  \"main\": \"./dist/index.mjs\",\n  \"module\": \"./dist/index.mjs\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./src/index.ts\",\n      \"development\": \"./src/index.ts\",\n      \"default\": \"./dist/index.mjs\"\n    }\n  },\n  \"publishConfig\": {\n    \"exports\": {\n      \".\": {\n        \"default\": \"./dist/index.mjs\"\n      }\n    }\n  },\n  \"dependencies\": {\n    \"@vben-core/composables\": \"workspace:*\",\n    \"@vben-core/icons\": \"workspace:*\",\n    \"@vben-core/shadcn-ui\": \"workspace:*\",\n    \"@vben-core/shared\": \"workspace:*\",\n    \"@vben-core/typings\": \"workspace:*\",\n    \"@vueuse/core\": \"catalog:\",\n    \"vue\": \"catalog:\"\n  }\n}\n"
  },
  {
    "path": "packages/@core/ui-kit/layout-ui/postcss.config.mjs",
    "content": "export { default } from '@vben/tailwind-config/postcss';\n"
  },
  {
    "path": "packages/@core/ui-kit/layout-ui/src/components/index.ts",
    "content": "export { default as LayoutContent } from './layout-content.vue';\nexport { default as LayoutFooter } from './layout-footer.vue';\nexport { default as LayoutHeader } from './layout-header.vue';\nexport { default as LayoutSidebar } from './layout-sidebar.vue';\nexport { default as LayoutTabbar } from './layout-tabbar.vue';\n"
  },
  {
    "path": "packages/@core/ui-kit/layout-ui/src/components/layout-content.vue",
    "content": "<script setup lang=\"ts\">\nimport type { ContentCompactType } from '@vben-core/typings';\nimport type { CSSProperties } from 'vue';\n\nimport { useLayoutContentStyle } from '@vben-core/composables';\nimport { Slot } from '@vben-core/shadcn-ui';\nimport { computed } from 'vue';\n\ninterface Props {\n  /**\n   * 内容区域定宽\n   */\n  contentCompact: ContentCompactType;\n  /**\n   * 定宽布局宽度\n   */\n  contentCompactWidth: number;\n  padding: number;\n  paddingBottom: number;\n  paddingLeft: number;\n  paddingRight: number;\n  paddingTop: number;\n}\n\nconst props = withDefaults(defineProps<Props>(), {});\n\nconst { contentElement, overlayStyle } = useLayoutContentStyle();\n\nconst style = computed((): CSSProperties => {\n  const {\n    contentCompact,\n    padding,\n    paddingBottom,\n    paddingLeft,\n    paddingRight,\n    paddingTop,\n  } = props;\n\n  const compactStyle: CSSProperties =\n    contentCompact === 'compact'\n      ? { margin: '0 auto', width: `${props.contentCompactWidth}px` }\n      : {};\n  return {\n    ...compactStyle,\n    flex: 1,\n    padding: `${padding}px`,\n    paddingBottom: `${paddingBottom}px`,\n    paddingLeft: `${paddingLeft}px`,\n    paddingRight: `${paddingRight}px`,\n    paddingTop: `${paddingTop}px`,\n  };\n});\n</script>\n\n<template>\n  <main ref=\"contentElement\" :style=\"style\" class=\"bg-background-deep relative\">\n    <Slot :style=\"overlayStyle\">\n      <slot name=\"overlay\"></slot>\n    </Slot>\n    <slot></slot>\n  </main>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/layout-ui/src/components/layout-footer.vue",
    "content": "<script setup lang=\"ts\">\nimport type { CSSProperties } from 'vue';\n\nimport { computed } from 'vue';\n\ninterface Props {\n  /**\n   * 是否固定在底部\n   */\n  fixed?: boolean;\n  height: number;\n  /**\n   * 是否显示\n   * @default true\n   */\n  show?: boolean;\n  width: string;\n  zIndex: number;\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n  show: true,\n});\n\nconst style = computed((): CSSProperties => {\n  const { fixed, height, show, width, zIndex } = props;\n  return {\n    height: `${height}px`,\n    marginBottom: show ? '0' : `-${height}px`,\n    position: fixed ? 'fixed' : 'static',\n    width,\n    zIndex,\n  };\n});\n</script>\n\n<template>\n  <footer\n    :style=\"style\"\n    class=\"bg-background-deep bottom-0 w-full transition-all duration-200\"\n  >\n    <slot></slot>\n  </footer>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/layout-ui/src/components/layout-header.vue",
    "content": "<script setup lang=\"ts\">\nimport type { CSSProperties } from 'vue';\n\nimport { computed, useSlots } from 'vue';\n\ninterface Props {\n  /**\n   * 横屏\n   */\n  fullWidth: boolean;\n  /**\n   * 高度\n   */\n  height: number;\n  /**\n   * 是否移动端\n   */\n  isMobile: boolean;\n  /**\n   * 是否显示\n   */\n  show: boolean;\n  /**\n   * 侧边菜单宽度\n   */\n  sidebarWidth: number;\n  /**\n   * 主题\n   */\n  theme: string | undefined;\n  /**\n   * 宽度\n   */\n  width: string;\n  /**\n   * zIndex\n   */\n  zIndex: number;\n}\n\nconst props = withDefaults(defineProps<Props>(), {});\n\nconst slots = useSlots();\n\nconst style = computed((): CSSProperties => {\n  const { fullWidth, height, show } = props;\n  const right = !show || !fullWidth ? undefined : 0;\n\n  return {\n    height: `${height}px`,\n    marginTop: show ? 0 : `-${height}px`,\n    right,\n  };\n});\n\nconst logoStyle = computed((): CSSProperties => {\n  return {\n    minWidth: `${props.isMobile ? 40 : props.sidebarWidth}px`,\n  };\n});\n</script>\n\n<template>\n  <header\n    :class=\"theme\"\n    :style=\"style\"\n    class=\"border-border bg-header top-0 flex w-full flex-[0_0_auto] items-center border-b pl-2 transition-[margin-top] duration-200\"\n  >\n    <div v-if=\"slots.logo\" :style=\"logoStyle\">\n      <slot name=\"logo\"></slot>\n    </div>\n\n    <slot name=\"toggle-button\"> </slot>\n\n    <slot></slot>\n  </header>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/layout-ui/src/components/layout-sidebar.vue",
    "content": "<script setup lang=\"ts\">\nimport type { CSSProperties } from 'vue';\n\nimport { computed, shallowRef, useSlots, watchEffect } from 'vue';\n\nimport { VbenScrollbar } from '@vben-core/shadcn-ui';\n\nimport { useScrollLock } from '@vueuse/core';\n\nimport { SidebarCollapseButton, SidebarFixedButton } from './widgets';\n\ninterface Props {\n  /**\n   * 折叠区域高度\n   * @default 42\n   */\n  collapseHeight?: number;\n  /**\n   * 折叠宽度\n   * @default 48\n   */\n  collapseWidth?: number;\n  /**\n   * 隐藏的dom是否可见\n   * @default true\n   */\n  domVisible?: boolean;\n  /**\n   * 扩展区域宽度\n   */\n  extraWidth: number;\n  /**\n   * 固定扩展区域\n   * @default false\n   */\n  fixedExtra?: boolean;\n  /**\n   * 头部高度\n   */\n  headerHeight: number;\n  /**\n   * 是否侧边混合模式\n   * @default false\n   */\n  isSidebarMixed?: boolean;\n  /**\n   * 顶部margin\n   * @default 60\n   */\n  marginTop?: number;\n  /**\n   * 混合菜单宽度\n   * @default 80\n   */\n  mixedWidth?: number;\n  /**\n   * 顶部padding\n   * @default 60\n   */\n  paddingTop?: number;\n  /**\n   * 是否显示\n   * @default true\n   */\n  show?: boolean;\n  /**\n   * 显示折叠按钮\n   * @default true\n   */\n  showCollapseButton?: boolean;\n  /**\n   * 显示固定按钮\n   * @default true\n   */\n  showFixedButton?: boolean;\n  /**\n   * 主题\n   */\n  theme: string;\n\n  /**\n   * 宽度\n   */\n  width: number;\n  /**\n   * zIndex\n   * @default 0\n   */\n  zIndex?: number;\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n  collapseHeight: 42,\n  collapseWidth: 48,\n  domVisible: true,\n  fixedExtra: false,\n  isSidebarMixed: false,\n  marginTop: 0,\n  mixedWidth: 70,\n  paddingTop: 0,\n  show: true,\n  showCollapseButton: true,\n  showFixedButton: true,\n  zIndex: 0,\n});\n\nconst emit = defineEmits<{ leave: [] }>();\nconst collapse = defineModel<boolean>('collapse');\nconst extraCollapse = defineModel<boolean>('extraCollapse');\nconst expandOnHovering = defineModel<boolean>('expandOnHovering');\nconst expandOnHover = defineModel<boolean>('expandOnHover');\nconst extraVisible = defineModel<boolean>('extraVisible');\n\nconst isLocked = useScrollLock(document.body);\nconst slots = useSlots();\n\nconst asideRef = shallowRef<HTMLDivElement | null>();\n\nconst hiddenSideStyle = computed((): CSSProperties => calcMenuWidthStyle(true));\n\nconst style = computed((): CSSProperties => {\n  const { isSidebarMixed, marginTop, paddingTop, zIndex } = props;\n\n  return {\n    '--scroll-shadow': 'var(--sidebar)',\n    ...calcMenuWidthStyle(false),\n    height: `calc(100% - ${marginTop}px)`,\n    marginTop: `${marginTop}px`,\n    paddingTop: `${paddingTop}px`,\n    zIndex,\n    ...(isSidebarMixed && extraVisible.value ? { transition: 'none' } : {}),\n  };\n});\n\nconst extraStyle = computed((): CSSProperties => {\n  const { extraWidth, show, width, zIndex } = props;\n\n  return {\n    left: `${width}px`,\n    width: extraVisible.value && show ? `${extraWidth}px` : 0,\n    zIndex,\n  };\n});\n\nconst extraTitleStyle = computed((): CSSProperties => {\n  const { headerHeight } = props;\n\n  return {\n    height: `${headerHeight - 1}px`,\n  };\n});\n\nconst contentWidthStyle = computed((): CSSProperties => {\n  const { collapseWidth, fixedExtra, isSidebarMixed, mixedWidth } = props;\n  if (isSidebarMixed && fixedExtra) {\n    return { width: `${collapse.value ? collapseWidth : mixedWidth}px` };\n  }\n  return {};\n});\n\nconst contentStyle = computed((): CSSProperties => {\n  const { collapseHeight, headerHeight } = props;\n\n  return {\n    height: `calc(100% - ${headerHeight + collapseHeight}px)`,\n    paddingTop: '8px',\n    ...contentWidthStyle.value,\n  };\n});\n\nconst headerStyle = computed((): CSSProperties => {\n  const { headerHeight, isSidebarMixed } = props;\n\n  return {\n    ...(isSidebarMixed ? { display: 'flex', justifyContent: 'center' } : {}),\n    height: `${headerHeight - 1}px`,\n    ...contentWidthStyle.value,\n  };\n});\n\nconst extraContentStyle = computed((): CSSProperties => {\n  const { collapseHeight, headerHeight } = props;\n  return {\n    height: `calc(100% - ${headerHeight + collapseHeight}px)`,\n  };\n});\n\nconst collapseStyle = computed((): CSSProperties => {\n  return {\n    height: `${props.collapseHeight}px`,\n  };\n});\n\nwatchEffect(() => {\n  extraVisible.value = props.fixedExtra ? true : extraVisible.value;\n});\n\nfunction calcMenuWidthStyle(isHiddenDom: boolean): CSSProperties {\n  const { extraWidth, fixedExtra, isSidebarMixed, show, width } = props;\n\n  let widthValue =\n    width === 0\n      ? '0px'\n      : `${width + (isSidebarMixed && fixedExtra && extraVisible.value ? extraWidth : 0)}px`;\n\n  const { collapseWidth } = props;\n\n  if (isHiddenDom && expandOnHovering.value && !expandOnHover.value) {\n    widthValue = `${collapseWidth}px`;\n  }\n\n  return {\n    ...(widthValue === '0px' ? { overflow: 'hidden' } : {}),\n    flex: `0 0 ${widthValue}`,\n    marginLeft: show ? 0 : `-${widthValue}`,\n    maxWidth: widthValue,\n    minWidth: widthValue,\n    width: widthValue,\n  };\n}\n\nfunction handleMouseenter(e: MouseEvent) {\n  if (e?.offsetX < 10) {\n    return;\n  }\n\n  // 未开启和未折叠状态不生效\n  if (expandOnHover.value) {\n    return;\n  }\n  if (!expandOnHovering.value) {\n    collapse.value = false;\n  }\n  if (props.isSidebarMixed) {\n    isLocked.value = true;\n  }\n  expandOnHovering.value = true;\n}\n\nfunction handleMouseleave() {\n  emit('leave');\n  if (props.isSidebarMixed) {\n    isLocked.value = false;\n  }\n  if (expandOnHover.value) {\n    return;\n  }\n\n  expandOnHovering.value = false;\n  collapse.value = true;\n  extraVisible.value = false;\n}\n</script>\n\n<template>\n  <div\n    v-if=\"domVisible\"\n    :class=\"theme\"\n    :style=\"hiddenSideStyle\"\n    class=\"h-full transition-all duration-150\"\n  ></div>\n  <aside\n    :class=\"[\n      theme,\n      {\n        'bg-sidebar-deep': isSidebarMixed,\n        'bg-sidebar border-border border-r': !isSidebarMixed,\n      },\n    ]\"\n    :style=\"style\"\n    class=\"fixed left-0 top-0 h-full transition-all duration-150\"\n    @mouseenter=\"handleMouseenter\"\n    @mouseleave=\"handleMouseleave\"\n  >\n    <SidebarFixedButton\n      v-if=\"!collapse && !isSidebarMixed && showFixedButton\"\n      v-model:expand-on-hover=\"expandOnHover\"\n    />\n    <div v-if=\"slots.logo\" :style=\"headerStyle\">\n      <slot name=\"logo\"></slot>\n    </div>\n    <VbenScrollbar :style=\"contentStyle\" shadow shadow-border>\n      <slot></slot>\n    </VbenScrollbar>\n\n    <div :style=\"collapseStyle\"></div>\n    <SidebarCollapseButton\n      v-if=\"showCollapseButton && !isSidebarMixed\"\n      v-model:collapsed=\"collapse\"\n    />\n    <div\n      v-if=\"isSidebarMixed\"\n      ref=\"asideRef\"\n      :class=\"{\n        'border-l': extraVisible,\n      }\"\n      :style=\"extraStyle\"\n      class=\"border-border bg-sidebar fixed top-0 h-full overflow-hidden border-r transition-all duration-200\"\n    >\n      <SidebarCollapseButton\n        v-if=\"isSidebarMixed && expandOnHover\"\n        v-model:collapsed=\"extraCollapse\"\n      />\n\n      <SidebarFixedButton\n        v-if=\"!extraCollapse\"\n        v-model:expand-on-hover=\"expandOnHover\"\n      />\n      <div v-if=\"!extraCollapse\" :style=\"extraTitleStyle\" class=\"pl-2\">\n        <slot name=\"extra-title\"></slot>\n      </div>\n      <VbenScrollbar\n        :style=\"extraContentStyle\"\n        class=\"border-border py-2\"\n        shadow\n        shadow-border\n      >\n        <slot name=\"extra\"></slot>\n      </VbenScrollbar>\n    </div>\n  </aside>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/layout-ui/src/components/layout-tabbar.vue",
    "content": "<script setup lang=\"ts\">\nimport type { CSSProperties } from 'vue';\n\nimport { computed } from 'vue';\n\ninterface Props {\n  /**\n   * 高度\n   */\n  height: number;\n}\n\nconst props = withDefaults(defineProps<Props>(), {});\n\nconst style = computed((): CSSProperties => {\n  const { height } = props;\n  return {\n    height: `${height}px`,\n  };\n});\n</script>\n\n<template>\n  <section\n    :style=\"style\"\n    class=\"border-border bg-background flex w-full border-b transition-all\"\n  >\n    <slot></slot>\n  </section>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/layout-ui/src/components/widgets/index.ts",
    "content": "export { default as SidebarCollapseButton } from './sidebar-collapse-button.vue';\nexport { default as SidebarFixedButton } from './sidebar-fixed-button.vue';\n"
  },
  {
    "path": "packages/@core/ui-kit/layout-ui/src/components/widgets/sidebar-collapse-button.vue",
    "content": "<script setup lang=\"ts\">\nimport { ChevronsLeft, ChevronsRight } from '@vben-core/icons';\n\nconst collapsed = defineModel<boolean>('collapsed');\n\nfunction handleCollapsed() {\n  collapsed.value = !collapsed.value;\n}\n</script>\n\n<template>\n  <div\n    class=\"flex-center hover:text-foreground text-foreground/60 hover:bg-accent-hover bg-accent absolute bottom-2 left-3 z-10 cursor-pointer rounded-sm p-1\"\n    @click.stop=\"handleCollapsed\"\n  >\n    <ChevronsRight v-if=\"collapsed\" class=\"size-4\" />\n    <ChevronsLeft v-else class=\"size-4\" />\n  </div>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/layout-ui/src/components/widgets/sidebar-fixed-button.vue",
    "content": "<script setup lang=\"ts\">\nimport { Pin, PinOff } from '@vben-core/icons';\n\nconst expandOnHover = defineModel<boolean>('expandOnHover');\n\nfunction toggleFixed() {\n  expandOnHover.value = !expandOnHover.value;\n}\n</script>\n\n<template>\n  <div\n    class=\"flex-center hover:text-foreground text-foreground/60 hover:bg-accent-hover bg-accent absolute bottom-2 right-3 z-10 cursor-pointer rounded-sm p-[5px] transition-all duration-300\"\n    @click=\"toggleFixed\"\n  >\n    <PinOff v-if=\"!expandOnHover\" class=\"size-3.5\" />\n    <Pin v-else class=\"size-3.5\" />\n  </div>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/layout-ui/src/hooks/use-layout.ts",
    "content": "import type { LayoutType } from '@vben-core/typings';\n\nimport type { VbenLayoutProps } from '../vben-layout';\n\nimport { computed } from 'vue';\n\nexport function useLayout(props: VbenLayoutProps) {\n  const currentLayout = computed(() =>\n    props.isMobile ? 'sidebar-nav' : (props.layout as LayoutType),\n  );\n\n  /**\n   * 是否全屏显示content，不需要侧边、底部、顶部、tab区域\n   */\n  const isFullContent = computed(() => currentLayout.value === 'full-content');\n\n  /**\n   * 是否侧边混合模式\n   */\n  const isSidebarMixedNav = computed(\n    () => currentLayout.value === 'sidebar-mixed-nav',\n  );\n\n  /**\n   * 是否为头部导航模式\n   */\n  const isHeaderNav = computed(() => currentLayout.value === 'header-nav');\n\n  /**\n   * 是否为混合导航模式\n   */\n  const isMixedNav = computed(\n    () =>\n      currentLayout.value === 'mixed-nav' ||\n      currentLayout.value === 'header-sidebar-nav',\n  );\n\n  /**\n   * 是否为头部混合模式\n   */\n  const isHeaderMixedNav = computed(\n    () => currentLayout.value === 'header-mixed-nav',\n  );\n\n  return {\n    currentLayout,\n    isFullContent,\n    isHeaderMixedNav,\n    isHeaderNav,\n    isMixedNav,\n    isSidebarMixedNav,\n  };\n}\n"
  },
  {
    "path": "packages/@core/ui-kit/layout-ui/src/index.ts",
    "content": "export type * from './vben-layout';\nexport { default as VbenAdminLayout } from './vben-layout.vue';\n"
  },
  {
    "path": "packages/@core/ui-kit/layout-ui/src/vben-layout.ts",
    "content": "import type {\n  ContentCompactType,\n  LayoutHeaderModeType,\n  LayoutType,\n  ThemeModeType,\n} from '@vben-core/typings';\n\ninterface VbenLayoutProps {\n  /**\n   * 内容区域定宽\n   * @default 'wide'\n   */\n  contentCompact?: ContentCompactType;\n  /**\n   * 定宽布局宽度\n   * @default 1200\n   */\n  contentCompactWidth?: number;\n  /**\n   * padding\n   * @default 16\n   */\n  contentPadding?: number;\n  /**\n   * paddingBottom\n   * @default 16\n   */\n  contentPaddingBottom?: number;\n  /**\n   * paddingLeft\n   * @default 16\n   */\n  contentPaddingLeft?: number;\n  /**\n   * paddingRight\n   * @default 16\n   */\n  contentPaddingRight?: number;\n  /**\n   * paddingTop\n   * @default 16\n   */\n  contentPaddingTop?: number;\n  /**\n   * footer 是否可见\n   * @default false\n   */\n  footerEnable?: boolean;\n  /**\n   * footer 是否固定\n   * @default true\n   */\n  footerFixed?: boolean;\n  /**\n   * footer 高度\n   * @default 32\n   */\n  footerHeight?: number;\n\n  /**\n   * header高度\n   * @default 48\n   */\n  headerHeight?: number;\n  /**\n   * 顶栏是否隐藏\n   * @default false\n   */\n  headerHidden?: boolean;\n  /**\n   * header 显示模式\n   * @default 'fixed'\n   */\n  headerMode?: LayoutHeaderModeType;\n  /**\n   * header 顶栏主题\n   */\n  headerTheme?: ThemeModeType;\n  /**\n   * 是否显示header切换侧边栏按钮\n   * @default\n   */\n  headerToggleSidebarButton?: boolean;\n  /**\n   * header是否显示\n   * @default true\n   */\n  headerVisible?: boolean;\n  /**\n   * 是否移动端显示\n   * @default false\n   */\n  isMobile?: boolean;\n  /**\n   * 布局方式\n   * sidebar-nav 侧边菜单布局\n   * header-nav 顶部菜单布局\n   * mixed-nav 侧边&顶部菜单布局\n   * sidebar-mixed-nav 侧边混合菜单布局\n   * full-content 全屏内容布局\n   * @default sidebar-nav\n   */\n  layout?: LayoutType;\n  /**\n   * 侧边菜单折叠状态\n   * @default false\n   */\n  sidebarCollapse?: boolean;\n  /**\n   * 侧边菜单折叠按钮\n   * @default true\n   */\n  sidebarCollapsedButton?: boolean;\n  /**\n   * 侧边菜单是否折叠时，是否显示title\n   * @default true\n   */\n  sidebarCollapseShowTitle?: boolean;\n  /**\n   * 侧边栏是否可见\n   * @default true\n   */\n  sidebarEnable?: boolean;\n  /**\n   * 侧边菜单折叠额外宽度\n   * @default 48\n   */\n  sidebarExtraCollapsedWidth?: number;\n  /**\n   * 侧边菜单折叠按钮是否固定\n   * @default true\n   */\n  sidebarFixedButton?: boolean;\n  /**\n   * 侧边栏是否隐藏\n   * @default false\n   */\n  sidebarHidden?: boolean;\n  /**\n   * 混合侧边栏宽度\n   * @default 80\n   */\n  sidebarMixedWidth?: number;\n  /**\n   * 侧边栏\n   * @default dark\n   */\n  sidebarTheme?: ThemeModeType;\n  /**\n   * 侧边栏宽度\n   * @default 210\n   */\n  sidebarWidth?: number;\n  /**\n   *  侧边菜单折叠宽度\n   * @default 48\n   */\n  sideCollapseWidth?: number;\n  /**\n   * tab是否可见\n   * @default true\n   */\n  tabbarEnable?: boolean;\n  /**\n   * tab高度\n   * @default 30\n   */\n  tabbarHeight?: number;\n  /**\n   * zIndex\n   * @default 100\n   */\n  zIndex?: number;\n}\nexport type { VbenLayoutProps };\n"
  },
  {
    "path": "packages/@core/ui-kit/layout-ui/src/vben-layout.vue",
    "content": "<script setup lang=\"ts\">\nimport type { CSSProperties } from 'vue';\n\nimport type { VbenLayoutProps } from './vben-layout';\n\nimport { computed, ref, watch } from 'vue';\n\nimport {\n  SCROLL_FIXED_CLASS,\n  useLayoutFooterStyle,\n  useLayoutHeaderStyle,\n} from '@vben-core/composables';\nimport { Menu } from '@vben-core/icons';\nimport { VbenIconButton } from '@vben-core/shadcn-ui';\nimport { ELEMENT_ID_MAIN_CONTENT } from '@vben-core/shared/constants';\n\nimport { useMouse, useScroll, useThrottleFn } from '@vueuse/core';\n\nimport {\n  LayoutContent,\n  LayoutFooter,\n  LayoutHeader,\n  LayoutSidebar,\n  LayoutTabbar,\n} from './components';\nimport { useLayout } from './hooks/use-layout';\n\ninterface Props extends VbenLayoutProps {}\n\ndefineOptions({\n  name: 'VbenLayout',\n});\n\nconst props = withDefaults(defineProps<Props>(), {\n  contentCompact: 'wide',\n  contentCompactWidth: 1200,\n  contentPadding: 0,\n  contentPaddingBottom: 0,\n  contentPaddingLeft: 0,\n  contentPaddingRight: 0,\n  contentPaddingTop: 0,\n  footerEnable: false,\n  footerFixed: true,\n  footerHeight: 32,\n  headerHeight: 50,\n  headerHidden: false,\n  headerMode: 'fixed',\n  headerToggleSidebarButton: true,\n  headerVisible: true,\n  isMobile: false,\n  layout: 'sidebar-nav',\n  sidebarCollapsedButton: true,\n  sidebarCollapseShowTitle: false,\n  sidebarExtraCollapsedWidth: 60,\n  sidebarFixedButton: true,\n  sidebarHidden: false,\n  sidebarMixedWidth: 80,\n  sidebarTheme: 'dark',\n  sidebarWidth: 180,\n  sideCollapseWidth: 60,\n  tabbarEnable: true,\n  tabbarHeight: 40,\n  zIndex: 200,\n});\n\nconst emit = defineEmits<{ sideMouseLeave: []; toggleSidebar: [] }>();\nconst sidebarCollapse = defineModel<boolean>('sidebarCollapse', {\n  default: false,\n});\nconst sidebarExtraVisible = defineModel<boolean>('sidebarExtraVisible');\nconst sidebarExtraCollapse = defineModel<boolean>('sidebarExtraCollapse', {\n  default: false,\n});\nconst sidebarExpandOnHover = defineModel<boolean>('sidebarExpandOnHover', {\n  default: false,\n});\nconst sidebarEnable = defineModel<boolean>('sidebarEnable', { default: true });\n\n// side是否处于hover状态展开菜单中\nconst sidebarExpandOnHovering = ref(false);\nconst headerIsHidden = ref(false);\nconst contentRef = ref();\n\nconst {\n  arrivedState,\n  directions,\n  isScrolling,\n  y: scrollY,\n} = useScroll(document);\n\nconst { setLayoutHeaderHeight } = useLayoutHeaderStyle();\nconst { setLayoutFooterHeight } = useLayoutFooterStyle();\n\nconst { y: mouseY } = useMouse({ target: contentRef, type: 'client' });\n\nconst {\n  currentLayout,\n  isFullContent,\n  isHeaderMixedNav,\n  isHeaderNav,\n  isMixedNav,\n  isSidebarMixedNav,\n} = useLayout(props);\n\n/**\n * 顶栏是否自动隐藏\n */\nconst isHeaderAutoMode = computed(() => props.headerMode === 'auto');\n\nconst headerWrapperHeight = computed(() => {\n  let height = 0;\n  if (props.headerVisible && !props.headerHidden) {\n    height += props.headerHeight;\n  }\n  if (props.tabbarEnable) {\n    height += props.tabbarHeight;\n  }\n  return height;\n});\n\nconst getSideCollapseWidth = computed(() => {\n  const { sidebarCollapseShowTitle, sidebarMixedWidth, sideCollapseWidth } =\n    props;\n\n  return sidebarCollapseShowTitle ||\n    isSidebarMixedNav.value ||\n    isHeaderMixedNav.value\n    ? sidebarMixedWidth\n    : sideCollapseWidth;\n});\n\n/**\n * 动态获取侧边区域是否可见\n */\nconst sidebarEnableState = computed(() => {\n  return !isHeaderNav.value && sidebarEnable.value;\n});\n\n/**\n * 侧边区域离顶部高度\n */\nconst sidebarMarginTop = computed(() => {\n  const { headerHeight, isMobile } = props;\n  return isMixedNav.value && !isMobile ? headerHeight : 0;\n});\n\n/**\n * 动态获取侧边宽度\n */\nconst getSidebarWidth = computed(() => {\n  const { isMobile, sidebarHidden, sidebarMixedWidth, sidebarWidth } = props;\n  let width = 0;\n\n  if (sidebarHidden) {\n    return width;\n  }\n\n  if (\n    !sidebarEnableState.value ||\n    (sidebarHidden &&\n      !isSidebarMixedNav.value &&\n      !isMixedNav.value &&\n      !isHeaderMixedNav.value)\n  ) {\n    return width;\n  }\n\n  if ((isHeaderMixedNav.value || isSidebarMixedNav.value) && !isMobile) {\n    width = sidebarMixedWidth;\n  } else if (sidebarCollapse.value) {\n    width = isMobile ? 0 : getSideCollapseWidth.value;\n  } else {\n    width = sidebarWidth;\n  }\n  return width;\n});\n\n/**\n * 获取扩展区域宽度\n */\nconst sidebarExtraWidth = computed(() => {\n  const { sidebarExtraCollapsedWidth, sidebarWidth } = props;\n\n  return sidebarExtraCollapse.value ? sidebarExtraCollapsedWidth : sidebarWidth;\n});\n\n/**\n * 是否侧边栏模式，包含混合侧边\n */\nconst isSideMode = computed(\n  () =>\n    currentLayout.value === 'mixed-nav' ||\n    currentLayout.value === 'sidebar-mixed-nav' ||\n    currentLayout.value === 'sidebar-nav' ||\n    currentLayout.value === 'header-mixed-nav' ||\n    currentLayout.value === 'header-sidebar-nav',\n);\n\n/**\n * header fixed值\n */\nconst headerFixed = computed(() => {\n  const { headerMode } = props;\n  return (\n    isMixedNav.value ||\n    headerMode === 'fixed' ||\n    headerMode === 'auto-scroll' ||\n    headerMode === 'auto'\n  );\n});\n\nconst showSidebar = computed(() => {\n  return isSideMode.value && sidebarEnable.value && !props.sidebarHidden;\n});\n\n/**\n * 遮罩可见性\n */\nconst maskVisible = computed(() => !sidebarCollapse.value && props.isMobile);\n\nconst mainStyle = computed(() => {\n  let width = '100%';\n  let sidebarAndExtraWidth = 'unset';\n  if (\n    headerFixed.value &&\n    currentLayout.value !== 'header-nav' &&\n    currentLayout.value !== 'mixed-nav' &&\n    currentLayout.value !== 'header-sidebar-nav' &&\n    showSidebar.value &&\n    !props.isMobile\n  ) {\n    // fixed模式下生效\n    const isSideNavEffective =\n      (isSidebarMixedNav.value || isHeaderMixedNav.value) &&\n      sidebarExpandOnHover.value &&\n      sidebarExtraVisible.value;\n\n    if (isSideNavEffective) {\n      const sideCollapseWidth = sidebarCollapse.value\n        ? getSideCollapseWidth.value\n        : props.sidebarMixedWidth;\n      const sideWidth = sidebarExtraCollapse.value\n        ? props.sidebarExtraCollapsedWidth\n        : props.sidebarWidth;\n\n      // 100% - 侧边菜单混合宽度 - 菜单宽度\n      sidebarAndExtraWidth = `${sideCollapseWidth + sideWidth}px`;\n      width = `calc(100% - ${sidebarAndExtraWidth})`;\n    } else {\n      sidebarAndExtraWidth =\n        sidebarExpandOnHovering.value && !sidebarExpandOnHover.value\n          ? `${getSideCollapseWidth.value}px`\n          : `${getSidebarWidth.value}px`;\n      width = `calc(100% - ${sidebarAndExtraWidth})`;\n    }\n  }\n  return {\n    sidebarAndExtraWidth,\n    width,\n  };\n});\n\n// 计算 tabbar 的样式\nconst tabbarStyle = computed((): CSSProperties => {\n  let width = '';\n  let marginLeft = 0;\n\n  // 如果不是混合导航，tabbar 的宽度为 100%\n  if (!isMixedNav.value || props.sidebarHidden) {\n    width = '100%';\n  } else if (sidebarEnable.value) {\n    // 鼠标在侧边栏上时，且侧边栏展开时的宽度\n    const onHoveringWidth = sidebarExpandOnHover.value\n      ? props.sidebarWidth\n      : getSideCollapseWidth.value;\n\n    // 设置 marginLeft，根据侧边栏是否折叠来决定\n    marginLeft = sidebarCollapse.value\n      ? getSideCollapseWidth.value\n      : onHoveringWidth;\n\n    // 设置 tabbar 的宽度，计算方式为 100% 减去侧边栏的宽度\n    width = `calc(100% - ${sidebarCollapse.value ? getSidebarWidth.value : onHoveringWidth}px)`;\n  } else {\n    // 默认情况下，tabbar 的宽度为 100%\n    width = '100%';\n  }\n\n  return {\n    marginLeft: `${marginLeft}px`,\n    width,\n  };\n});\n\nconst contentStyle = computed((): CSSProperties => {\n  const fixed = headerFixed.value;\n\n  const { footerEnable, footerFixed, footerHeight } = props;\n  return {\n    marginTop:\n      fixed &&\n      !isFullContent.value &&\n      !headerIsHidden.value &&\n      (!isHeaderAutoMode.value || scrollY.value < headerWrapperHeight.value)\n        ? `${headerWrapperHeight.value}px`\n        : 0,\n    paddingBottom: `${footerEnable && footerFixed ? footerHeight : 0}px`,\n  };\n});\n\nconst headerZIndex = computed(() => {\n  const { zIndex } = props;\n  const offset = isMixedNav.value ? 1 : 0;\n  return zIndex + offset;\n});\n\nconst headerWrapperStyle = computed((): CSSProperties => {\n  const fixed = headerFixed.value;\n  return {\n    height: isFullContent.value ? '0' : `${headerWrapperHeight.value}px`,\n    left: isMixedNav.value ? 0 : mainStyle.value.sidebarAndExtraWidth,\n    position: fixed ? 'fixed' : 'static',\n    top:\n      headerIsHidden.value || isFullContent.value\n        ? `-${headerWrapperHeight.value}px`\n        : 0,\n    width: mainStyle.value.width,\n    'z-index': headerZIndex.value,\n  };\n});\n\n/**\n * 侧边栏z-index\n */\nconst sidebarZIndex = computed(() => {\n  const { isMobile, zIndex } = props;\n  let offset = isMobile || isSideMode.value ? 1 : -1;\n\n  if (isMixedNav.value) {\n    offset += 1;\n  }\n\n  return zIndex + offset;\n});\n\nconst footerWidth = computed(() => {\n  if (!props.footerFixed) {\n    return '100%';\n  }\n\n  return mainStyle.value.width;\n});\n\nconst maskStyle = computed((): CSSProperties => {\n  return { zIndex: props.zIndex };\n});\n\nconst showHeaderToggleButton = computed(() => {\n  return (\n    props.isMobile ||\n    (props.headerToggleSidebarButton &&\n      isSideMode.value &&\n      !isSidebarMixedNav.value &&\n      !isMixedNav.value &&\n      !props.isMobile)\n  );\n});\n\nconst showHeaderLogo = computed(() => {\n  return !isSideMode.value || isMixedNav.value || props.isMobile;\n});\n\nwatch(\n  () => props.isMobile,\n  (val) => {\n    if (val) {\n      sidebarCollapse.value = true;\n    }\n  },\n  {\n    immediate: true,\n  },\n);\n\nwatch(\n  [() => headerWrapperHeight.value, () => isFullContent.value],\n  ([height]) => {\n    setLayoutHeaderHeight(isFullContent.value ? 0 : height);\n  },\n  {\n    immediate: true,\n  },\n);\n\nwatch(\n  () => props.footerHeight,\n  (height: number) => {\n    setLayoutFooterHeight(height);\n  },\n  {\n    immediate: true,\n  },\n);\n\n{\n  const mouseMove = () => {\n    mouseY.value > headerWrapperHeight.value\n      ? (headerIsHidden.value = true)\n      : (headerIsHidden.value = false);\n  };\n  watch(\n    [() => props.headerMode, () => mouseY.value],\n    () => {\n      if (!isHeaderAutoMode.value || isMixedNav.value || isFullContent.value) {\n        if (props.headerMode !== 'auto-scroll') {\n          headerIsHidden.value = false;\n        }\n        return;\n      }\n      headerIsHidden.value = true;\n      mouseMove();\n    },\n    {\n      immediate: true,\n    },\n  );\n}\n\n{\n  const checkHeaderIsHidden = useThrottleFn((top, bottom, topArrived) => {\n    if (scrollY.value < headerWrapperHeight.value) {\n      headerIsHidden.value = false;\n      return;\n    }\n    if (topArrived) {\n      headerIsHidden.value = false;\n      return;\n    }\n\n    if (top) {\n      headerIsHidden.value = false;\n    } else if (bottom) {\n      headerIsHidden.value = true;\n    }\n  }, 300);\n\n  watch(\n    () => scrollY.value,\n    () => {\n      if (\n        props.headerMode !== 'auto-scroll' ||\n        isMixedNav.value ||\n        isFullContent.value\n      ) {\n        return;\n      }\n      if (isScrolling.value) {\n        checkHeaderIsHidden(\n          directions.top,\n          directions.bottom,\n          arrivedState.top,\n        );\n      }\n    },\n  );\n}\n\nfunction handleClickMask() {\n  sidebarCollapse.value = true;\n}\n\nfunction handleHeaderToggle() {\n  if (props.isMobile) {\n    sidebarCollapse.value = false;\n  } else {\n    emit('toggleSidebar');\n  }\n}\n\nconst idMainContent = ELEMENT_ID_MAIN_CONTENT;\n</script>\n\n<template>\n  <div class=\"relative flex min-h-full w-full\">\n    <LayoutSidebar\n      v-if=\"sidebarEnableState\"\n      v-model:collapse=\"sidebarCollapse\"\n      v-model:expand-on-hover=\"sidebarExpandOnHover\"\n      v-model:expand-on-hovering=\"sidebarExpandOnHovering\"\n      v-model:extra-collapse=\"sidebarExtraCollapse\"\n      v-model:extra-visible=\"sidebarExtraVisible\"\n      :show-collapse-button=\"sidebarCollapsedButton\"\n      :show-fixed-button=\"sidebarFixedButton\"\n      :collapse-width=\"getSideCollapseWidth\"\n      :dom-visible=\"!isMobile\"\n      :extra-width=\"sidebarExtraWidth\"\n      :fixed-extra=\"sidebarExpandOnHover\"\n      :header-height=\"isMixedNav ? 0 : headerHeight\"\n      :is-sidebar-mixed=\"isSidebarMixedNav || isHeaderMixedNav\"\n      :margin-top=\"sidebarMarginTop\"\n      :mixed-width=\"sidebarMixedWidth\"\n      :show=\"showSidebar\"\n      :theme=\"sidebarTheme\"\n      :width=\"getSidebarWidth\"\n      :z-index=\"sidebarZIndex\"\n      @leave=\"() => emit('sideMouseLeave')\"\n    >\n      <template v-if=\"isSideMode && !isMixedNav\" #logo>\n        <slot name=\"logo\"></slot>\n      </template>\n\n      <template v-if=\"isSidebarMixedNav || isHeaderMixedNav\">\n        <slot name=\"mixed-menu\"></slot>\n      </template>\n      <template v-else>\n        <slot name=\"menu\"></slot>\n      </template>\n\n      <template #extra>\n        <slot name=\"side-extra\"></slot>\n      </template>\n      <template #extra-title>\n        <slot name=\"side-extra-title\"></slot>\n      </template>\n    </LayoutSidebar>\n\n    <div\n      ref=\"contentRef\"\n      class=\"flex flex-1 flex-col overflow-hidden transition-all duration-300 ease-in\"\n    >\n      <div\n        :class=\"[\n          {\n            'shadow-[0_16px_24px_hsl(var(--background))]': scrollY > 20,\n          },\n          SCROLL_FIXED_CLASS,\n        ]\"\n        :style=\"headerWrapperStyle\"\n        class=\"overflow-hidden transition-all duration-200\"\n      >\n        <LayoutHeader\n          v-if=\"headerVisible\"\n          :full-width=\"!isSideMode\"\n          :height=\"headerHeight\"\n          :is-mobile=\"isMobile\"\n          :show=\"!isFullContent && !headerHidden\"\n          :sidebar-width=\"sidebarWidth\"\n          :theme=\"headerTheme\"\n          :width=\"mainStyle.width\"\n          :z-index=\"headerZIndex\"\n        >\n          <template v-if=\"showHeaderLogo\" #logo>\n            <slot name=\"logo\"></slot>\n          </template>\n\n          <template #toggle-button>\n            <VbenIconButton\n              v-if=\"showHeaderToggleButton\"\n              class=\"my-0 mr-1 rounded-md\"\n              @click=\"handleHeaderToggle\"\n            >\n              <Menu class=\"size-4\" />\n            </VbenIconButton>\n          </template>\n          <slot name=\"header\"></slot>\n        </LayoutHeader>\n\n        <LayoutTabbar\n          v-if=\"tabbarEnable\"\n          :height=\"tabbarHeight\"\n          :style=\"tabbarStyle\"\n        >\n          <slot name=\"tabbar\"></slot>\n        </LayoutTabbar>\n      </div>\n\n      <!-- </div> -->\n      <LayoutContent\n        :id=\"idMainContent\"\n        :content-compact=\"contentCompact\"\n        :content-compact-width=\"contentCompactWidth\"\n        :padding=\"contentPadding\"\n        :padding-bottom=\"contentPaddingBottom\"\n        :padding-left=\"contentPaddingLeft\"\n        :padding-right=\"contentPaddingRight\"\n        :padding-top=\"contentPaddingTop\"\n        :style=\"contentStyle\"\n        class=\"transition-[margin-top] duration-200\"\n      >\n        <slot name=\"content\"></slot>\n\n        <template #overlay>\n          <slot name=\"content-overlay\"></slot>\n        </template>\n      </LayoutContent>\n\n      <LayoutFooter\n        v-if=\"footerEnable\"\n        :fixed=\"footerFixed\"\n        :height=\"footerHeight\"\n        :show=\"!isFullContent\"\n        :width=\"footerWidth\"\n        :z-index=\"zIndex\"\n      >\n        <slot name=\"footer\"></slot>\n      </LayoutFooter>\n    </div>\n    <slot name=\"extra\"></slot>\n    <div\n      v-if=\"maskVisible\"\n      :style=\"maskStyle\"\n      class=\"bg-overlay fixed left-0 top-0 h-full w-full transition-[background-color] duration-200\"\n      @click=\"handleClickMask\"\n    ></div>\n  </div>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/layout-ui/tailwind.config.mjs",
    "content": "export { default } from '@vben/tailwind-config';\n"
  },
  {
    "path": "packages/@core/ui-kit/layout-ui/tsconfig.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/tsconfig\",\n  \"extends\": \"@vben/tsconfig/web.json\",\n  \"include\": [\"src\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "packages/@core/ui-kit/menu-ui/README.md",
    "content": "# 菜单组件\n"
  },
  {
    "path": "packages/@core/ui-kit/menu-ui/build.config.ts",
    "content": "import { defineBuildConfig } from 'unbuild';\n\nexport default defineBuildConfig({\n  clean: true,\n  declaration: true,\n  entries: [\n    {\n      builder: 'mkdist',\n      input: './src',\n      pattern: ['**/*'],\n    },\n    {\n      builder: 'mkdist',\n      input: './src',\n      loaders: ['vue'],\n      pattern: ['**/*.vue'],\n    },\n    {\n      builder: 'mkdist',\n      format: 'esm',\n      input: './src',\n      loaders: ['js'],\n      pattern: ['**/*.ts'],\n    },\n  ],\n});\n"
  },
  {
    "path": "packages/@core/ui-kit/menu-ui/package.json",
    "content": "{\n  \"name\": \"@vben-core/menu-ui\",\n  \"version\": \"5.5.9\",\n  \"homepage\": \"https://github.com/vbenjs/vue-vben-admin\",\n  \"bugs\": \"https://github.com/vbenjs/vue-vben-admin/issues\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/vbenjs/vue-vben-admin.git\",\n    \"directory\": \"packages/@vben-core/uikit/menu-ui\"\n  },\n  \"license\": \"MIT\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"build\": \"pnpm unbuild\",\n    \"prepublishOnly\": \"npm run build\"\n  },\n  \"files\": [\n    \"dist\"\n  ],\n  \"sideEffects\": [\n    \"**/*.css\"\n  ],\n  \"main\": \"./dist/index.mjs\",\n  \"module\": \"./dist/index.mjs\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./src/index.ts\",\n      \"development\": \"./src/index.ts\",\n      \"default\": \"./dist/index.mjs\"\n    }\n  },\n  \"publishConfig\": {\n    \"exports\": {\n      \".\": {\n        \"default\": \"./dist/index.mjs\"\n      }\n    }\n  },\n  \"dependencies\": {\n    \"@vben-core/composables\": \"workspace:*\",\n    \"@vben-core/icons\": \"workspace:*\",\n    \"@vben-core/shadcn-ui\": \"workspace:*\",\n    \"@vben-core/shared\": \"workspace:*\",\n    \"@vben-core/typings\": \"workspace:*\",\n    \"@vueuse/core\": \"catalog:\",\n    \"vue\": \"catalog:\"\n  }\n}\n"
  },
  {
    "path": "packages/@core/ui-kit/menu-ui/postcss.config.mjs",
    "content": "export { default } from '@vben/tailwind-config/postcss';\n"
  },
  {
    "path": "packages/@core/ui-kit/menu-ui/src/components/collapse-transition.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { RendererElement } from 'vue';\n\ndefineOptions({\n  name: 'CollapseTransition',\n});\n\nconst reset = (el: RendererElement) => {\n  el.style.maxHeight = '';\n  el.style.overflow = el.dataset.oldOverflow;\n  el.style.paddingTop = el.dataset.oldPaddingTop;\n  el.style.paddingBottom = el.dataset.oldPaddingBottom;\n};\n\nconst on = {\n  afterEnter(el: RendererElement) {\n    el.style.maxHeight = '';\n    el.style.overflow = el.dataset.oldOverflow;\n  },\n\n  afterLeave(el: RendererElement) {\n    reset(el);\n  },\n\n  beforeEnter(el: RendererElement) {\n    if (!el.dataset) el.dataset = {};\n\n    el.dataset.oldPaddingTop = el.style.paddingTop;\n    el.dataset.oldMarginTop = el.style.marginTop;\n\n    el.dataset.oldPaddingBottom = el.style.paddingBottom;\n    el.dataset.oldMarginBottom = el.style.marginBottom;\n    if (el.style.height) el.dataset.elExistsHeight = el.style.height;\n\n    el.style.maxHeight = 0;\n    el.style.paddingTop = 0;\n    el.style.marginTop = 0;\n    el.style.paddingBottom = 0;\n    el.style.marginBottom = 0;\n  },\n\n  beforeLeave(el: RendererElement) {\n    if (!el.dataset) el.dataset = {};\n    el.dataset.oldPaddingTop = el.style.paddingTop;\n    el.dataset.oldMarginTop = el.style.marginTop;\n    el.dataset.oldPaddingBottom = el.style.paddingBottom;\n    el.dataset.oldMarginBottom = el.style.marginBottom;\n    el.dataset.oldOverflow = el.style.overflow;\n    el.style.maxHeight = `${el.scrollHeight}px`;\n    el.style.overflow = 'hidden';\n  },\n\n  enter(el: RendererElement) {\n    requestAnimationFrame(() => {\n      el.dataset.oldOverflow = el.style.overflow;\n      if (el.dataset.elExistsHeight) {\n        el.style.maxHeight = el.dataset.elExistsHeight;\n      } else if (el.scrollHeight === 0) {\n        el.style.maxHeight = 0;\n      } else {\n        el.style.maxHeight = `${el.scrollHeight}px`;\n      }\n\n      el.style.paddingTop = el.dataset.oldPaddingTop;\n      el.style.paddingBottom = el.dataset.oldPaddingBottom;\n      el.style.marginTop = el.dataset.oldMarginTop;\n      el.style.marginBottom = el.dataset.oldMarginBottom;\n      el.style.overflow = 'hidden';\n    });\n  },\n\n  enterCancelled(el: RendererElement) {\n    reset(el);\n  },\n\n  leave(el: RendererElement) {\n    if (el.scrollHeight !== 0) {\n      el.style.maxHeight = 0;\n      el.style.paddingTop = 0;\n      el.style.paddingBottom = 0;\n      el.style.marginTop = 0;\n      el.style.marginBottom = 0;\n    }\n  },\n\n  leaveCancelled(el: RendererElement) {\n    reset(el);\n  },\n};\n</script>\n\n<template>\n  <transition name=\"collapse-transition\" v-on=\"on\">\n    <slot></slot>\n  </transition>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/menu-ui/src/components/index.ts",
    "content": "export { default as Menu } from './menu.vue';\nexport { default as MenuBadge } from './menu-badge.vue';\nexport { default as MenuItem } from './menu-item.vue';\nexport { default as SubMenu } from './sub-menu.vue';\n"
  },
  {
    "path": "packages/@core/ui-kit/menu-ui/src/components/menu-badge-dot.vue",
    "content": "<script setup lang=\"ts\">\nimport type { CSSProperties } from 'vue';\n\ninterface Props {\n  dotClass?: string;\n  dotStyle?: CSSProperties;\n}\n\nwithDefaults(defineProps<Props>(), {\n  dotClass: '',\n  dotStyle: () => ({}),\n});\n</script>\n<template>\n  <span class=\"relative mr-1 flex size-1.5\">\n    <span\n      :class=\"dotClass\"\n      :style=\"dotStyle\"\n      class=\"absolute inline-flex h-full w-full animate-ping rounded-full opacity-75\"\n    >\n    </span>\n    <span\n      :class=\"dotClass\"\n      :style=\"dotStyle\"\n      class=\"relative inline-flex size-1.5 rounded-full\"\n    ></span>\n  </span>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/menu-ui/src/components/menu-badge.vue",
    "content": "<script setup lang=\"ts\">\nimport type { MenuRecordBadgeRaw } from '@vben-core/typings';\n\nimport { computed } from 'vue';\n\nimport { isValidColor } from '@vben-core/shared/color';\n\nimport BadgeDot from './menu-badge-dot.vue';\n\ninterface Props extends MenuRecordBadgeRaw {\n  hasChildren?: boolean;\n}\n\nconst props = withDefaults(defineProps<Props>(), {});\n\nconst variantsMap: Record<string, string> = {\n  default: 'bg-green-500',\n  destructive: 'bg-destructive',\n  primary: 'bg-primary',\n  success: 'bg-green-500',\n  warning: 'bg-yellow-500',\n};\n\nconst isDot = computed(() => props.badgeType === 'dot');\n\nconst badgeClass = computed(() => {\n  const { badgeVariants } = props;\n\n  if (!badgeVariants) {\n    return variantsMap.default;\n  }\n\n  return variantsMap[badgeVariants] || badgeVariants;\n});\n\nconst badgeStyle = computed(() => {\n  if (badgeClass.value && isValidColor(badgeClass.value)) {\n    return {\n      backgroundColor: badgeClass.value,\n    };\n  }\n  return {};\n});\n</script>\n<template>\n  <span v-if=\"isDot || badge\" :class=\"$attrs.class\" class=\"absolute\">\n    <BadgeDot v-if=\"isDot\" :dot-class=\"badgeClass\" :dot-style=\"badgeStyle\" />\n    <div\n      v-else\n      :class=\"badgeClass\"\n      :style=\"badgeStyle\"\n      class=\"text-primary-foreground flex-center rounded-xl px-1.5 py-0.5 text-[10px]\"\n    >\n      {{ badge }}\n    </div>\n  </span>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/menu-ui/src/components/menu-item.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { MenuItemProps, MenuItemRegistered } from '../types';\n\nimport { computed, onBeforeUnmount, onMounted, reactive, useSlots } from 'vue';\n\nimport { useNamespace } from '@vben-core/composables';\nimport { VbenIcon, VbenTooltip } from '@vben-core/shadcn-ui';\n\nimport { MenuBadge } from '../components';\nimport { useMenu, useMenuContext, useSubMenuContext } from '../hooks';\n\ninterface Props extends MenuItemProps {}\n\ndefineOptions({ name: 'MenuItem' });\n\nconst props = withDefaults(defineProps<Props>(), {\n  disabled: false,\n});\n\nconst emit = defineEmits<{ click: [MenuItemRegistered] }>();\n\nconst slots = useSlots();\nconst { b, e, is } = useNamespace('menu-item');\nconst nsMenu = useNamespace('menu');\nconst rootMenu = useMenuContext();\nconst subMenu = useSubMenuContext();\nconst { parentMenu, parentPaths } = useMenu();\n\nconst active = computed(() => props.path === rootMenu?.activePath);\nconst menuIcon = computed(() =>\n  active.value ? props.activeIcon || props.icon : props.icon,\n);\n\nconst isTopLevelMenuItem = computed(\n  () => parentMenu.value?.type.name === 'Menu',\n);\n\nconst collapseShowTitle = computed(\n  () =>\n    rootMenu.props?.collapseShowTitle &&\n    isTopLevelMenuItem.value &&\n    rootMenu.props.collapse,\n);\n\nconst showTooltip = computed(\n  () =>\n    rootMenu.props.mode === 'vertical' &&\n    isTopLevelMenuItem.value &&\n    rootMenu.props?.collapse &&\n    slots.title,\n);\n\nconst item: MenuItemRegistered = reactive({\n  active,\n  parentPaths: parentPaths.value,\n  path: props.path || '',\n});\n\n/**\n * 菜单项点击事件\n */\nfunction handleClick() {\n  if (props.disabled) {\n    return;\n  }\n  rootMenu?.handleMenuItemClick?.({\n    parentPaths: parentPaths.value,\n    path: props.path,\n  });\n  emit('click', item);\n}\n\nonMounted(() => {\n  subMenu?.addSubMenu?.(item);\n  rootMenu?.addMenuItem?.(item);\n});\n\nonBeforeUnmount(() => {\n  subMenu?.removeSubMenu?.(item);\n  rootMenu?.removeMenuItem?.(item);\n});\n</script>\n<template>\n  <li\n    :class=\"[\n      rootMenu.theme,\n      b(),\n      is('active', active),\n      is('disabled', disabled),\n      is('collapse-show-title', collapseShowTitle),\n    ]\"\n    role=\"menuitem\"\n    @click.stop=\"handleClick\"\n  >\n    <VbenTooltip\n      v-if=\"showTooltip\"\n      :content-class=\"[rootMenu.theme]\"\n      side=\"right\"\n    >\n      <template #trigger>\n        <div :class=\"[nsMenu.be('tooltip', 'trigger')]\">\n          <VbenIcon :class=\"nsMenu.e('icon')\" :icon=\"menuIcon\" />\n          <slot></slot>\n          <span v-if=\"collapseShowTitle\" :class=\"nsMenu.e('name')\">\n            <slot name=\"title\"></slot>\n          </span>\n        </div>\n      </template>\n      <slot name=\"title\"></slot>\n    </VbenTooltip>\n    <div v-show=\"!showTooltip\" :class=\"[e('content')]\">\n      <MenuBadge\n        v-if=\"rootMenu.props.mode !== 'horizontal'\"\n        class=\"right-2\"\n        v-bind=\"props\"\n      />\n      <VbenIcon :class=\"nsMenu.e('icon')\" :icon=\"menuIcon\" />\n      <slot></slot>\n      <slot name=\"title\"></slot>\n    </div>\n  </li>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/menu-ui/src/components/menu.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { UseResizeObserverReturn } from '@vueuse/core';\n\nimport type { SetupContext, VNodeArrayChildren } from 'vue';\n\nimport type {\n  MenuItemClicked,\n  MenuItemRegistered,\n  MenuProps,\n  MenuProvider,\n} from '../types';\n\nimport {\n  computed,\n  nextTick,\n  reactive,\n  ref,\n  toRef,\n  useSlots,\n  watch,\n  watchEffect,\n} from 'vue';\n\nimport { useNamespace } from '@vben-core/composables';\nimport { Ellipsis } from '@vben-core/icons';\n\nimport { useResizeObserver } from '@vueuse/core';\n\nimport {\n  createMenuContext,\n  createSubMenuContext,\n  useMenuStyle,\n} from '../hooks';\nimport { useMenuScroll } from '../hooks/use-menu-scroll';\nimport { flattedChildren } from '../utils';\nimport SubMenu from './sub-menu.vue';\n\ninterface Props extends MenuProps {}\n\ndefineOptions({ name: 'Menu' });\n\nconst props = withDefaults(defineProps<Props>(), {\n  accordion: true,\n  collapse: false,\n  mode: 'vertical',\n  rounded: true,\n  theme: 'dark',\n  scrollToActive: false,\n});\n\nconst emit = defineEmits<{\n  close: [string, string[]];\n  open: [string, string[]];\n  select: [string, string[]];\n}>();\n\nconst { b, is } = useNamespace('menu');\nconst menuStyle = useMenuStyle();\nconst slots: SetupContext['slots'] = useSlots();\nconst menu = ref<HTMLUListElement>();\nconst sliceIndex = ref(-1);\nconst openedMenus = ref<MenuProvider['openedMenus']>(\n  props.defaultOpeneds && !props.collapse ? [...props.defaultOpeneds] : [],\n);\nconst activePath = ref<MenuProvider['activePath']>(props.defaultActive);\nconst items = ref<MenuProvider['items']>({});\nconst subMenus = ref<MenuProvider['subMenus']>({});\nconst mouseInChild = ref(false);\n\nconst isMenuPopup = computed<MenuProvider['isMenuPopup']>(() => {\n  return (\n    props.mode === 'horizontal' || (props.mode === 'vertical' && props.collapse)\n  );\n});\n\nconst getSlot = computed(() => {\n  // 更新插槽内容\n  const defaultSlots: VNodeArrayChildren = slots.default?.() ?? [];\n\n  const originalSlot = flattedChildren(defaultSlots) as VNodeArrayChildren;\n  const slotDefault =\n    sliceIndex.value === -1\n      ? originalSlot\n      : originalSlot.slice(0, sliceIndex.value);\n\n  const slotMore =\n    sliceIndex.value === -1 ? [] : originalSlot.slice(sliceIndex.value);\n\n  return { showSlotMore: slotMore.length > 0, slotDefault, slotMore };\n});\n\nwatch(\n  () => props.collapse,\n  (value) => {\n    if (value) openedMenus.value = [];\n  },\n);\n\nwatch(items.value, initMenu);\n\nwatch(\n  () => props.defaultActive,\n  (currentActive = '') => {\n    if (!items.value[currentActive]) {\n      activePath.value = '';\n    }\n    updateActiveName(currentActive);\n  },\n);\n\nlet resizeStopper: UseResizeObserverReturn['stop'];\nwatchEffect(() => {\n  if (props.mode === 'horizontal') {\n    resizeStopper = useResizeObserver(menu, handleResize).stop;\n  } else {\n    resizeStopper?.();\n  }\n});\n\n// 注入上下文\ncreateMenuContext(\n  reactive({\n    activePath,\n    addMenuItem,\n    addSubMenu,\n    closeMenu,\n    handleMenuItemClick,\n    handleSubMenuClick,\n    isMenuPopup,\n    openedMenus,\n    openMenu,\n    props,\n    removeMenuItem,\n    removeSubMenu,\n    subMenus,\n    theme: toRef(props, 'theme'),\n    items,\n  }),\n);\n\ncreateSubMenuContext({\n  addSubMenu,\n  level: 1,\n  mouseInChild,\n  removeSubMenu,\n});\n\nfunction calcMenuItemWidth(menuItem: HTMLElement) {\n  const computedStyle = getComputedStyle(menuItem);\n  const marginLeft = Number.parseInt(computedStyle.marginLeft, 10);\n  const marginRight = Number.parseInt(computedStyle.marginRight, 10);\n  return menuItem.offsetWidth + marginLeft + marginRight || 0;\n}\n\nfunction calcSliceIndex() {\n  if (!menu.value) {\n    return -1;\n  }\n  const items = [...(menu.value?.childNodes ?? [])].filter(\n    (item) =>\n      // remove comment type node #12634\n      item.nodeName !== '#comment' &&\n      (item.nodeName !== '#text' || item.nodeValue),\n  ) as HTMLElement[];\n\n  const moreItemWidth = 46;\n  const computedMenuStyle = getComputedStyle(menu?.value);\n\n  const paddingLeft = Number.parseInt(computedMenuStyle.paddingLeft, 10);\n  const paddingRight = Number.parseInt(computedMenuStyle.paddingRight, 10);\n  const menuWidth = menu.value?.clientWidth - paddingLeft - paddingRight;\n\n  let calcWidth = 0;\n  let sliceIndex = 0;\n  items.forEach((item, index) => {\n    calcWidth += calcMenuItemWidth(item);\n    if (calcWidth <= menuWidth - moreItemWidth) {\n      sliceIndex = index + 1;\n    }\n  });\n  return sliceIndex === items.length ? -1 : sliceIndex;\n}\n\nfunction debounce(fn: () => void, wait = 33.34) {\n  let timer: null | ReturnType<typeof setTimeout>;\n  return () => {\n    timer && clearTimeout(timer);\n    timer = setTimeout(() => {\n      fn();\n    }, wait);\n  };\n}\n\nlet isFirstTimeRender = true;\nfunction handleResize() {\n  if (sliceIndex.value === calcSliceIndex()) {\n    return;\n  }\n  const callback = () => {\n    sliceIndex.value = -1;\n    nextTick(() => {\n      sliceIndex.value = calcSliceIndex();\n    });\n  };\n  callback();\n  // // execute callback directly when first time resize to avoid shaking\n  isFirstTimeRender ? callback() : debounce(callback)();\n  isFirstTimeRender = false;\n}\n\nconst enableScroll = computed(\n  () => props.scrollToActive && props.mode === 'vertical' && !props.collapse,\n);\n\nconst { scrollToActiveItem } = useMenuScroll(activePath, {\n  enable: enableScroll,\n  delay: 320,\n});\n\n// 监听 activePath 变化，自动滚动到激活项\nwatch(activePath, () => {\n  scrollToActiveItem();\n});\n\n// 默认展开菜单\nfunction initMenu() {\n  const parentPaths = getActivePaths();\n\n  // 展开该菜单项的路径上所有子菜单\n  // expand all subMenus of the menu item\n  parentPaths.forEach((path) => {\n    const subMenu = subMenus.value[path];\n    subMenu && openMenu(path, subMenu.parentPaths);\n  });\n}\n\nfunction updateActiveName(val: string) {\n  const itemsInData = items.value;\n  const item =\n    itemsInData[val] ||\n    (activePath.value && itemsInData[activePath.value]) ||\n    itemsInData[props.defaultActive || ''];\n\n  activePath.value = item ? item.path : val;\n}\n\nfunction handleMenuItemClick(data: MenuItemClicked) {\n  const { collapse, mode } = props;\n  if (mode === 'horizontal' || collapse) {\n    openedMenus.value = [];\n  }\n  const { parentPaths, path } = data;\n  if (!path || !parentPaths) {\n    return;\n  }\n\n  emit('select', path, parentPaths);\n}\n\nfunction handleSubMenuClick({ parentPaths, path }: MenuItemRegistered) {\n  const isOpened = openedMenus.value.includes(path);\n\n  if (isOpened) {\n    closeMenu(path, parentPaths);\n  } else {\n    openMenu(path, parentPaths);\n  }\n}\n\nfunction close(path: string) {\n  const i = openedMenus.value.indexOf(path);\n\n  if (i !== -1) {\n    openedMenus.value.splice(i, 1);\n  }\n}\n\n/**\n * 关闭、折叠菜单\n */\nfunction closeMenu(path: string, parentPaths: string[]) {\n  if (props.accordion) {\n    openedMenus.value = subMenus.value[path]?.parentPaths ?? [];\n  }\n\n  close(path);\n\n  emit('close', path, parentPaths);\n}\n\n/**\n * 点击展开菜单\n */\nfunction openMenu(path: string, parentPaths: string[]) {\n  if (openedMenus.value.includes(path)) {\n    return;\n  }\n  // 手风琴模式菜单\n  if (props.accordion) {\n    const activeParentPaths = getActivePaths();\n    if (activeParentPaths.includes(path)) {\n      parentPaths = activeParentPaths;\n    }\n    openedMenus.value = openedMenus.value.filter((path: string) =>\n      parentPaths.includes(path),\n    );\n  }\n  openedMenus.value.push(path);\n  emit('open', path, parentPaths);\n}\n\nfunction addMenuItem(item: MenuItemRegistered) {\n  items.value[item.path] = item;\n}\n\nfunction addSubMenu(subMenu: MenuItemRegistered) {\n  subMenus.value[subMenu.path] = subMenu;\n}\n\nfunction removeSubMenu(subMenu: MenuItemRegistered) {\n  Reflect.deleteProperty(subMenus.value, subMenu.path);\n}\n\nfunction removeMenuItem(item: MenuItemRegistered) {\n  Reflect.deleteProperty(items.value, item.path);\n}\n\nfunction getActivePaths() {\n  const activeItem = activePath.value && items.value[activePath.value];\n\n  if (!activeItem || props.mode === 'horizontal' || props.collapse) {\n    return [];\n  }\n\n  return activeItem.parentPaths;\n}\n</script>\n<template>\n  <ul\n    ref=\"menu\"\n    :class=\"[\n      theme,\n      b(),\n      is(mode, true),\n      is(theme, true),\n      is('rounded', rounded),\n      is('collapse', collapse),\n      is('menu-align', mode === 'horizontal'),\n    ]\"\n    :style=\"menuStyle\"\n    role=\"menu\"\n  >\n    <template v-if=\"mode === 'horizontal' && getSlot.showSlotMore\">\n      <template v-for=\"item in getSlot.slotDefault\" :key=\"item.key\">\n        <component :is=\"item\" />\n      </template>\n      <SubMenu is-sub-menu-more path=\"sub-menu-more\">\n        <template #title>\n          <Ellipsis class=\"size-4\" />\n        </template>\n        <template v-for=\"item in getSlot.slotMore\" :key=\"item.key\">\n          <component :is=\"item\" />\n        </template>\n      </SubMenu>\n    </template>\n    <template v-else>\n      <slot></slot>\n    </template>\n  </ul>\n</template>\n\n<style lang=\"scss\">\n$namespace: vben;\n\n@mixin menu-item-active {\n  color: var(--menu-item-active-color);\n  text-decoration: none;\n  cursor: pointer;\n  background: var(--menu-item-active-background-color);\n}\n\n@mixin menu-item {\n  position: relative;\n  display: flex;\n  // gap: 12px;\n  align-items: center;\n  height: var(--menu-item-height);\n  padding: var(--menu-item-padding-y) var(--menu-item-padding-x);\n  margin: 0 var(--menu-item-margin-x) var(--menu-item-margin-y)\n    var(--menu-item-margin-x);\n  font-size: var(--menu-font-size);\n  color: var(--menu-item-color);\n  white-space: nowrap;\n  text-decoration: none;\n  cursor: pointer;\n  list-style: none;\n  background: var(--menu-item-background-color);\n  border: none;\n  border-radius: var(--menu-item-radius);\n  transition:\n    background 0.15s ease,\n    color 0.15s ease,\n    padding 0.15s ease,\n    border-color 0.15s ease;\n\n  &.is-disabled {\n    cursor: not-allowed;\n    background: none !important;\n    opacity: 0.25;\n  }\n\n  .#{$namespace}-menu__icon {\n    transition: transform 0.25s;\n  }\n\n  &:hover {\n    .#{$namespace}-menu__icon {\n      transform: scale(1.2);\n    }\n  }\n\n  &:hover,\n  &:focus {\n    outline: none;\n  }\n\n  * {\n    vertical-align: bottom;\n  }\n}\n\n@mixin menu-title {\n  max-width: var(--menu-title-width);\n  overflow: hidden;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n  opacity: 1;\n}\n\n.is-menu-align {\n  justify-content: var(--menu-align, start);\n}\n\n.#{$namespace}-menu__popup-container,\n.#{$namespace}-menu {\n  --menu-title-width: 140px;\n  --menu-item-icon-size: 16px;\n  --menu-item-height: 38px;\n  --menu-item-padding-y: 21px;\n  --menu-item-padding-x: 12px;\n  --menu-item-popup-padding-y: 20px;\n  --menu-item-popup-padding-x: 12px;\n  --menu-item-margin-y: 2px;\n  --menu-item-margin-x: 0px;\n  --menu-item-collapse-padding-y: 23.5px;\n  --menu-item-collapse-padding-x: 0px;\n  --menu-item-collapse-margin-y: 4px;\n  --menu-item-collapse-margin-x: 0px;\n  --menu-item-radius: 0px;\n  --menu-item-indent: 16px;\n  --menu-font-size: 14px;\n\n  &.is-dark {\n    --menu-background-color: hsl(var(--menu));\n    // --menu-submenu-opened-background-color: hsl(var(--menu-opened-dark));\n    --menu-item-background-color: var(--menu-background-color);\n    --menu-item-color: hsl(var(--foreground) / 80%);\n    --menu-item-hover-color: hsl(var(--accent-foreground));\n    --menu-item-hover-background-color: hsl(var(--accent));\n    --menu-item-active-color: hsl(var(--accent-foreground));\n    --menu-item-active-background-color: hsl(var(--accent));\n    --menu-submenu-hover-color: hsl(var(--foreground));\n    --menu-submenu-hover-background-color: hsl(var(--accent));\n    --menu-submenu-active-color: hsl(var(--foreground));\n    --menu-submenu-active-background-color: transparent;\n    --menu-submenu-background-color: var(--menu-background-color);\n  }\n\n  &.is-light {\n    --menu-background-color: hsl(var(--menu));\n    // --menu-submenu-opened-background-color: hsl(var(--menu-opened));\n    --menu-item-background-color: var(--menu-background-color);\n    --menu-item-color: hsl(var(--foreground));\n    --menu-item-hover-color: var(--menu-item-color);\n    --menu-item-hover-background-color: hsl(var(--accent));\n    --menu-item-active-color: hsl(var(--primary));\n    --menu-item-active-background-color: hsl(var(--primary) / 15%);\n    --menu-submenu-hover-color: hsl(var(--primary));\n    --menu-submenu-hover-background-color: hsl(var(--accent));\n    --menu-submenu-active-color: hsl(var(--primary));\n    --menu-submenu-active-background-color: transparent;\n    --menu-submenu-background-color: var(--menu-background-color);\n  }\n\n  &.is-rounded {\n    --menu-item-margin-x: 8px;\n    --menu-item-collapse-margin-x: 6px;\n    --menu-item-radius: 6px;\n  }\n\n  &.is-horizontal:not(.is-rounded) {\n    --menu-item-height: 40px;\n    --menu-item-radius: 6px;\n  }\n\n  &.is-horizontal.is-rounded {\n    --menu-item-height: 40px;\n    --menu-item-radius: 6px;\n    --menu-item-padding-x: 12px;\n  }\n\n  // .vben-menu__popup,\n  &.is-horizontal {\n    --menu-item-padding-y: 0px;\n    --menu-item-padding-x: 10px;\n    --menu-item-margin-y: 0px;\n    --menu-item-margin-x: 1px;\n    --menu-background-color: transparent;\n\n    &.is-dark {\n      --menu-item-hover-color: hsl(var(--accent-foreground));\n      --menu-item-hover-background-color: hsl(var(--accent));\n      --menu-item-active-color: hsl(var(--accent-foreground));\n      --menu-item-active-background-color: hsl(var(--accent));\n      --menu-submenu-active-color: hsl(var(--foreground));\n      --menu-submenu-active-background-color: hsl(var(--accent));\n      --menu-submenu-hover-color: hsl(var(--accent-foreground));\n      --menu-submenu-hover-background-color: hsl(var(--accent));\n    }\n\n    &.is-light {\n      --menu-item-active-color: hsl(var(--primary));\n      --menu-item-active-background-color: hsl(var(--primary) / 15%);\n      --menu-item-hover-background-color: hsl(var(--accent));\n      --menu-item-hover-color: hsl(var(--primary));\n      --menu-submenu-active-color: hsl(var(--primary));\n      --menu-submenu-active-background-color: hsl(var(--primary) / 15%);\n      --menu-submenu-hover-color: hsl(var(--primary));\n      --menu-submenu-hover-background-color: hsl(var(--accent));\n    }\n  }\n}\n\n.#{$namespace}-menu {\n  position: relative;\n  box-sizing: border-box;\n  padding-left: 0;\n  margin: 0;\n  list-style: none;\n  background: hsl(var(--menu-background-color));\n\n  // 垂直菜单\n  &.is-vertical {\n    &:not(.#{$namespace}-menu.is-collapse) {\n      & .#{$namespace}-menu-item,\n      & .#{$namespace}-sub-menu-content,\n      & .#{$namespace}-menu-item-group__title {\n        padding-left: calc(\n          var(--menu-item-indent) + var(--menu-level) * var(--menu-item-indent)\n        );\n        white-space: nowrap;\n      }\n\n      & > .#{$namespace}-sub-menu {\n        & > .#{$namespace}-menu {\n          & > .#{$namespace}-menu-item {\n            padding-left: calc(\n              0px + var(--menu-item-indent) + var(--menu-level) *\n                var(--menu-item-indent)\n            );\n          }\n        }\n\n        & > .#{$namespace}-sub-menu-content {\n          padding-left: calc(var(--menu-item-indent) - 8px);\n        }\n      }\n      & > .#{$namespace}-menu-item {\n        padding-left: calc(var(--menu-item-indent) - 8px);\n      }\n    }\n  }\n\n  &.is-horizontal {\n    display: flex;\n    flex-wrap: nowrap;\n    max-width: 100%;\n    height: var(--height-horizontal-height);\n    border-right: none;\n\n    .#{$namespace}-menu-item {\n      display: inline-flex;\n      align-items: center;\n      justify-content: center;\n      height: var(--menu-item-height);\n      padding-right: calc(var(--menu-item-padding-x) + 6px);\n      margin: 0;\n      margin-right: 2px;\n      // border-bottom: 2px solid transparent;\n      border-radius: var(--menu-item-radius);\n    }\n\n    & > .#{$namespace}-sub-menu {\n      height: var(--menu-item-height);\n      margin-right: 2px;\n\n      &:focus,\n      &:hover {\n        outline: none;\n      }\n\n      & .#{$namespace}-sub-menu-content {\n        height: 100%;\n        padding-right: 40px;\n        // border-bottom: 2px solid transparent;\n        border-radius: var(--menu-item-radius);\n      }\n    }\n\n    & .#{$namespace}-menu-item:not(.is-disabled):hover,\n    & .#{$namespace}-menu-item:not(.is-disabled):focus {\n      outline: none;\n    }\n\n    & > .#{$namespace}-menu-item.is-active {\n      color: var(--menu-item-active-color);\n    }\n\n    // &.is-light {\n    //   & > .#{$namespace}-sub-menu {\n    //     &.is-active {\n    //       border-bottom: 2px solid var(--menu-item-active-color);\n    //     }\n    //     &:not(.is-active) .#{$namespace}-sub-menu-content {\n    //       &:hover {\n    //         border-bottom: 2px solid var(--menu-item-active-color);\n    //       }\n    //     }\n    //   }\n    //   & > .#{$namespace}-menu-item.is-active {\n    //     border-bottom: 2px solid var(--menu-item-active-color);\n    //   }\n\n    //   & .#{$namespace}-menu-item:not(.is-disabled):hover,\n    //   & .#{$namespace}-menu-item:not(.is-disabled):focus {\n    //     border-bottom: 2px solid var(--menu-item-active-color);\n    //   }\n    // }\n  }\n  // 折叠菜单\n\n  &.is-collapse {\n    .#{$namespace}-menu__icon {\n      margin-right: 0;\n    }\n    .#{$namespace}-sub-menu__icon-arrow {\n      display: none;\n    }\n\n    .#{$namespace}-sub-menu-content,\n    .#{$namespace}-menu-item {\n      display: flex;\n      align-items: center;\n      justify-content: center;\n      padding: var(--menu-item-collapse-padding-y)\n        var(--menu-item-collapse-padding-x);\n      margin: var(--menu-item-collapse-margin-y)\n        var(--menu-item-collapse-margin-x);\n      transition: all 0.3s;\n\n      &.is-active {\n        background: var(--menu-item-active-background-color) !important;\n        border-radius: var(--menu-item-radius);\n      }\n    }\n\n    &.is-light {\n      .#{$namespace}-sub-menu-content,\n      .#{$namespace}-menu-item {\n        &.is-active {\n          // color: hsl(var(--primary-foreground)) !important;\n          background: var(--menu-item-active-background-color) !important;\n        }\n      }\n    }\n\n    &.is-rounded {\n      .#{$namespace}-sub-menu-content,\n      .#{$namespace}-menu-item {\n        &.is-collapse-show-title {\n          // padding: 32px 0 !important;\n          margin: 4px 8px !important;\n        }\n      }\n    }\n  }\n\n  &__popup-container {\n    max-width: 240px;\n    height: unset;\n    padding: 0;\n    background: var(--menu-background-color);\n  }\n\n  &__popup {\n    padding: 10px 0;\n    border-radius: var(--menu-item-radius);\n\n    .#{$namespace}-sub-menu-content,\n    .#{$namespace}-menu-item {\n      padding: var(--menu-item-popup-padding-y) var(--menu-item-popup-padding-x);\n    }\n  }\n\n  &__icon {\n    flex-shrink: 0;\n    width: var(--menu-item-icon-size);\n    height: var(--menu-item-icon-size);\n    margin-right: 8px;\n    vertical-align: middle;\n    text-align: center;\n  }\n}\n\n.#{$namespace}-menu-item {\n  fill: var(--menu-item-color);\n\n  @include menu-item;\n\n  &.is-active {\n    fill: var(--menu-item-active-color);\n\n    @include menu-item-active;\n  }\n\n  &__content {\n    display: inline-flex;\n    align-items: center;\n    width: 100%;\n    height: var(--menu-item-height);\n\n    span {\n      @include menu-title;\n    }\n  }\n\n  &.is-collapse-show-title {\n    padding: 32px 0 !important;\n    // margin: 4px 8px !important;\n    .#{$namespace}-menu-tooltip__trigger {\n      flex-direction: column;\n    }\n    .#{$namespace}-menu__icon {\n      display: block;\n      font-size: 20px !important;\n      transition: all 0.25s ease;\n    }\n\n    .#{$namespace}-menu__name {\n      display: inline-flex;\n      margin-top: 8px;\n      margin-bottom: 0;\n      font-size: 12px;\n      font-weight: 400;\n      line-height: normal;\n      transition: all 0.25s ease;\n    }\n  }\n\n  &:not(.is-active):hover {\n    color: var(--menu-item-hover-color);\n    text-decoration: none;\n    cursor: pointer;\n    background: var(--menu-item-hover-background-color) !important;\n  }\n\n  .#{$namespace}-menu-tooltip__trigger {\n    position: absolute;\n    top: 0;\n    left: 0;\n    box-sizing: border-box;\n    display: inline-flex;\n    align-items: center;\n    justify-content: center;\n    width: 100%;\n    height: 100%;\n    padding: 0 var(--menu-item-padding-x);\n    font-size: var(--menu-font-size);\n    line-height: var(--menu-item-height);\n  }\n}\n\n.#{$namespace}-sub-menu {\n  padding-left: 0;\n  margin: 0;\n  list-style: none;\n  background: var(--menu-submenu-background-color);\n  fill: var(--menu-item-color);\n\n  &.is-active {\n    div[data-state='open'] > .#{$namespace}-sub-menu-content,\n    > .#{$namespace}-sub-menu-content {\n      // font-weight: 500;\n      color: var(--menu-submenu-active-color);\n      text-decoration: none;\n      cursor: pointer;\n      background: var(--menu-submenu-active-background-color);\n      fill: var(--menu-submenu-active-color);\n    }\n  }\n}\n\n.#{$namespace}-sub-menu-content {\n  height: var(--menu-item-height);\n\n  @include menu-item;\n\n  &__icon-arrow {\n    position: absolute;\n    top: 50%;\n    right: 10px;\n    width: inherit;\n    margin-top: -8px;\n    margin-right: 0;\n    // font-size: 16px;\n    font-weight: normal;\n    opacity: 1;\n    transition: transform 0.25s ease;\n  }\n\n  &__title {\n    @include menu-title;\n  }\n\n  &.is-collapse-show-title {\n    flex-direction: column;\n    padding: 32px 0 !important;\n    // margin: 4px 8px !important;\n    .#{$namespace}-menu__icon {\n      display: block;\n      font-size: 20px !important;\n      transition: all 0.25s ease;\n    }\n    .#{$namespace}-sub-menu-content__title {\n      display: inline-flex;\n      flex-shrink: 0;\n      margin-top: 8px;\n      margin-bottom: 0;\n      font-size: 12px;\n      font-weight: 400;\n      line-height: normal;\n      transition: all 0.25s ease;\n    }\n  }\n\n  &.is-more {\n    padding-right: 12px !important;\n  }\n\n  // &:not(.is-active):hover {\n  &:hover {\n    color: var(--menu-submenu-hover-color);\n    text-decoration: none;\n    cursor: pointer;\n    background: var(--menu-submenu-hover-background-color) !important;\n\n    // svg {\n    //   fill: var(--menu-submenu-hover-color);\n    // }\n  }\n}\n</style>\n"
  },
  {
    "path": "packages/@core/ui-kit/menu-ui/src/components/normal-menu/index.ts",
    "content": "export type * from './normal-menu';\nexport { default as NormalMenu } from './normal-menu.vue';\n"
  },
  {
    "path": "packages/@core/ui-kit/menu-ui/src/components/normal-menu/normal-menu.ts",
    "content": "import type { MenuRecordRaw } from '@vben-core/typings';\n\ninterface NormalMenuProps {\n  /**\n   * 菜单数据\n   */\n  activePath?: string;\n  /**\n   * 是否折叠\n   */\n  collapse?: boolean;\n  /**\n   * 菜单项\n   */\n  menus?: MenuRecordRaw[];\n  /**\n   * @zh_CN 是否圆润风格\n   * @default true\n   */\n  rounded?: boolean;\n  /**\n   * 主题\n   */\n  theme?: 'dark' | 'light';\n}\n\nexport type { NormalMenuProps };\n"
  },
  {
    "path": "packages/@core/ui-kit/menu-ui/src/components/normal-menu/normal-menu.vue",
    "content": "<script setup lang=\"ts\">\nimport type { MenuRecordRaw } from '@vben-core/typings';\n\nimport type { NormalMenuProps } from './normal-menu';\n\nimport { useNamespace } from '@vben-core/composables';\nimport { VbenIcon } from '@vben-core/shadcn-ui';\n\ninterface Props extends NormalMenuProps {}\n\ndefineOptions({\n  name: 'NormalMenu',\n});\n\nconst props = withDefaults(defineProps<Props>(), {\n  activePath: '',\n  collapse: false,\n  menus: () => [],\n  theme: 'dark',\n});\n\nconst emit = defineEmits<{\n  enter: [MenuRecordRaw];\n  select: [MenuRecordRaw];\n}>();\n\nconst { b, e, is } = useNamespace('normal-menu');\n\nfunction menuIcon(menu: MenuRecordRaw) {\n  return props.activePath === menu.path\n    ? menu.activeIcon || menu.icon\n    : menu.icon;\n}\n</script>\n\n<template>\n  <ul\n    :class=\"[\n      theme,\n      b(),\n      is('collapse', collapse),\n      is(theme, true),\n      is('rounded', rounded),\n    ]\"\n    class=\"relative\"\n  >\n    <template v-for=\"menu in menus\" :key=\"menu.path\">\n      <li\n        :class=\"[e('item'), is('active', activePath === menu.path)]\"\n        @click=\"() => emit('select', menu)\"\n        @mouseenter=\"() => emit('enter', menu)\"\n      >\n        <VbenIcon :class=\"e('icon')\" :icon=\"menuIcon(menu)\" fallback />\n\n        <span :class=\"e('name')\" class=\"truncate\"> {{ menu.name }}</span>\n      </li>\n    </template>\n  </ul>\n</template>\n<style lang=\"scss\" scoped>\n$namespace: vben;\n\n.#{$namespace}-normal-menu {\n  --menu-item-margin-y: 4px;\n  --menu-item-margin-x: 0px;\n  --menu-item-padding-y: 9px;\n  --menu-item-padding-x: 0px;\n  --menu-item-radius: 0px;\n\n  height: calc(100% - 4px);\n\n  &.is-rounded {\n    --menu-item-radius: 6px;\n    --menu-item-margin-x: 8px;\n  }\n\n  &.is-dark {\n    .#{$namespace}-normal-menu__item {\n      @apply text-foreground/80;\n      // color: hsl(var(--foreground) / 80%);\n\n      &:not(.is-active):hover {\n        @apply text-foreground;\n      }\n\n      &.is-active {\n        .#{$namespace}-normal-menu__name,\n        .#{$namespace}-normal-menu__icon {\n          @apply text-foreground;\n        }\n      }\n    }\n  }\n\n  &.is-collapse {\n    .#{$namespace}-normal-menu__name {\n      width: 0;\n      height: 0;\n      margin-top: 0;\n      overflow: hidden;\n      opacity: 0;\n    }\n\n    .#{$namespace}-normal-menu__icon {\n      font-size: 20px;\n    }\n  }\n\n  &__item {\n    position: relative;\n    display: flex;\n    flex-direction: column;\n    align-items: center;\n    justify-content: center;\n    // max-width: 64px;\n    // max-height: 64px;\n    padding: var(--menu-item-padding-y) var(--menu-item-padding-x);\n    margin: var(--menu-item-margin-y) var(--menu-item-margin-x);\n    color: hsl(var(--foreground) / 90%);\n    cursor: pointer;\n    border-radius: var(--menu-item-radius);\n    transition:\n      background 0.15s ease,\n      padding 0.15s ease,\n      border-color 0.15s ease;\n\n    &.is-active {\n      @apply text-primary bg-primary dark:bg-accent;\n\n      .#{$namespace}-normal-menu__name,\n      .#{$namespace}-normal-menu__icon {\n        @apply text-primary-foreground font-semibold;\n      }\n    }\n\n    &:not(.is-active):hover {\n      @apply dark:bg-accent text-primary bg-heavy dark:text-foreground;\n    }\n\n    &:hover {\n      .#{$namespace}-normal-menu__icon {\n        transform: scale(1.2);\n      }\n    }\n  }\n\n  &__icon {\n    max-height: 20px;\n    font-size: 20px;\n    transition: all 0.25s ease;\n  }\n\n  &__name {\n    margin-top: 8px;\n    margin-bottom: 0;\n    font-size: 12px;\n    font-weight: 400;\n    transition: all 0.25s ease;\n  }\n}\n</style>\n"
  },
  {
    "path": "packages/@core/ui-kit/menu-ui/src/components/sub-menu-content.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { MenuItemProps } from '../types';\n\nimport { computed } from 'vue';\n\nimport { useNamespace } from '@vben-core/composables';\nimport { ChevronDown, ChevronRight } from '@vben-core/icons';\nimport { VbenIcon } from '@vben-core/shadcn-ui';\n\nimport { useMenuContext } from '../hooks';\n\ninterface Props extends MenuItemProps {\n  isMenuMore?: boolean;\n  isTopLevelMenuSubmenu: boolean;\n  level?: number;\n}\n\ndefineOptions({ name: 'SubMenuContent' });\n\nconst props = withDefaults(defineProps<Props>(), {\n  isMenuMore: false,\n  level: 0,\n});\n\nconst rootMenu = useMenuContext();\nconst { b, e, is } = useNamespace('sub-menu-content');\nconst nsMenu = useNamespace('menu');\n\nconst opened = computed(() => {\n  return rootMenu?.openedMenus.includes(props.path);\n});\n\nconst collapse = computed(() => {\n  return rootMenu.props.collapse;\n});\n\nconst isFirstLevel = computed(() => {\n  return props.level === 1;\n});\n\nconst getCollapseShowTitle = computed(() => {\n  return (\n    rootMenu.props.collapseShowTitle && isFirstLevel.value && collapse.value\n  );\n});\n\nconst mode = computed(() => {\n  return rootMenu?.props.mode;\n});\n\nconst showArrowIcon = computed(() => {\n  return mode.value === 'horizontal' || !(isFirstLevel.value && collapse.value);\n});\n\nconst hiddenTitle = computed(() => {\n  return (\n    mode.value === 'vertical' &&\n    isFirstLevel.value &&\n    collapse.value &&\n    !getCollapseShowTitle.value\n  );\n});\n\nconst iconComp = computed(() => {\n  return (mode.value === 'horizontal' && !isFirstLevel.value) ||\n    (mode.value === 'vertical' && collapse.value)\n    ? ChevronRight\n    : ChevronDown;\n});\n\nconst iconArrowStyle = computed(() => {\n  return opened.value ? { transform: `rotate(180deg)` } : {};\n});\n</script>\n<template>\n  <div\n    :class=\"[\n      b(),\n      is('collapse-show-title', getCollapseShowTitle),\n      is('more', isMenuMore),\n    ]\"\n  >\n    <slot></slot>\n\n    <VbenIcon\n      v-if=\"!isMenuMore\"\n      :class=\"nsMenu.e('icon')\"\n      :icon=\"icon\"\n      fallback\n    />\n\n    <div v-if=\"!hiddenTitle\" :class=\"[e('title')]\">\n      <slot name=\"title\"></slot>\n    </div>\n\n    <component\n      :is=\"iconComp\"\n      v-if=\"!isMenuMore\"\n      v-show=\"showArrowIcon\"\n      :class=\"[e('icon-arrow')]\"\n      :style=\"iconArrowStyle\"\n      class=\"size-4\"\n    />\n  </div>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/menu-ui/src/components/sub-menu.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { HoverCardContentProps } from '@vben-core/shadcn-ui';\n\nimport type { MenuItemRegistered, MenuProvider, SubMenuProps } from '../types';\n\nimport { computed, onBeforeUnmount, onMounted, reactive, ref } from 'vue';\n\nimport { useNamespace } from '@vben-core/composables';\nimport { VbenHoverCard } from '@vben-core/shadcn-ui';\n\nimport {\n  createSubMenuContext,\n  useMenu,\n  useMenuContext,\n  useMenuStyle,\n  useSubMenuContext,\n} from '../hooks';\nimport CollapseTransition from './collapse-transition.vue';\nimport SubMenuContent from './sub-menu-content.vue';\n\ninterface Props extends SubMenuProps {\n  isSubMenuMore?: boolean;\n}\n\ndefineOptions({ name: 'SubMenu' });\n\nconst props = withDefaults(defineProps<Props>(), {\n  disabled: false,\n  isSubMenuMore: false,\n});\n\nconst { parentMenu, parentPaths } = useMenu();\nconst { b, is } = useNamespace('sub-menu');\nconst nsMenu = useNamespace('menu');\nconst rootMenu = useMenuContext();\nconst subMenu = useSubMenuContext();\nconst subMenuStyle = useMenuStyle(subMenu);\n\nconst mouseInChild = ref(false);\n\nconst items = ref<MenuProvider['items']>({});\nconst subMenus = ref<MenuProvider['subMenus']>({});\nconst timer = ref<null | ReturnType<typeof setTimeout>>(null);\n\ncreateSubMenuContext({\n  addSubMenu,\n  handleMouseleave,\n  level: (subMenu?.level ?? 0) + 1,\n  mouseInChild,\n  removeSubMenu,\n});\n\nconst opened = computed(() => {\n  return rootMenu?.openedMenus.includes(props.path);\n});\nconst isTopLevelMenuSubmenu = computed(\n  () => parentMenu.value?.type.name === 'Menu',\n);\nconst mode = computed(() => rootMenu?.props.mode ?? 'vertical');\nconst rounded = computed(() => rootMenu?.props.rounded);\nconst currentLevel = computed(() => subMenu?.level ?? 0);\nconst isFirstLevel = computed(() => {\n  return currentLevel.value === 1;\n});\n\nconst contentProps = computed((): HoverCardContentProps => {\n  const isHorizontal = mode.value === 'horizontal';\n  const side = isHorizontal && isFirstLevel.value ? 'bottom' : 'right';\n  return {\n    collisionPadding: { top: 20 },\n    side,\n    sideOffset: isHorizontal ? 5 : 10,\n  };\n});\n\nconst active = computed(() => {\n  let isActive = false;\n\n  Object.values(items.value).forEach((item) => {\n    if (item.active) {\n      isActive = true;\n    }\n  });\n\n  Object.values(subMenus.value).forEach((subItem) => {\n    if (subItem.active) {\n      isActive = true;\n    }\n  });\n  return isActive;\n});\n\nfunction addSubMenu(subMenu: MenuItemRegistered) {\n  subMenus.value[subMenu.path] = subMenu;\n}\n\nfunction removeSubMenu(subMenu: MenuItemRegistered) {\n  Reflect.deleteProperty(subMenus.value, subMenu.path);\n}\n\n/**\n * 点击submenu展开/关闭\n */\nfunction handleClick() {\n  const mode = rootMenu?.props.mode;\n  if (\n    // 当前菜单禁用时，不展开\n    props.disabled ||\n    (rootMenu?.props.collapse && mode === 'vertical') ||\n    // 水平模式下不展开\n    mode === 'horizontal'\n  ) {\n    return;\n  }\n\n  rootMenu?.handleSubMenuClick({\n    active: active.value,\n    parentPaths: parentPaths.value,\n    path: props.path,\n  });\n}\n\nfunction handleMouseenter(event: FocusEvent | MouseEvent, showTimeout = 300) {\n  if (event.type === 'focus') {\n    return;\n  }\n\n  if (\n    (!rootMenu?.props.collapse && rootMenu?.props.mode === 'vertical') ||\n    props.disabled\n  ) {\n    if (subMenu) {\n      subMenu.mouseInChild.value = true;\n    }\n    return;\n  }\n  if (subMenu) {\n    subMenu.mouseInChild.value = true;\n  }\n\n  timer.value && window.clearTimeout(timer.value);\n  timer.value = setTimeout(() => {\n    rootMenu?.openMenu(props.path, parentPaths.value);\n  }, showTimeout);\n  parentMenu.value?.vnode.el?.dispatchEvent(new MouseEvent('mouseenter'));\n}\n\nfunction handleMouseleave(deepDispatch = false) {\n  if (\n    !rootMenu?.props.collapse &&\n    rootMenu?.props.mode === 'vertical' &&\n    subMenu\n  ) {\n    subMenu.mouseInChild.value = false;\n    return;\n  }\n\n  timer.value && window.clearTimeout(timer.value);\n\n  if (subMenu) {\n    subMenu.mouseInChild.value = false;\n  }\n  timer.value = setTimeout(() => {\n    !mouseInChild.value && rootMenu?.closeMenu(props.path, parentPaths.value);\n  }, 300);\n\n  if (deepDispatch) {\n    subMenu?.handleMouseleave?.(true);\n  }\n}\n\nconst menuIcon = computed(() =>\n  active.value ? props.activeIcon || props.icon : props.icon,\n);\n\nconst item = reactive({\n  active,\n  parentPaths,\n  path: props.path,\n});\n\nonMounted(() => {\n  subMenu?.addSubMenu?.(item);\n  rootMenu?.addSubMenu?.(item);\n});\n\nonBeforeUnmount(() => {\n  subMenu?.removeSubMenu?.(item);\n  rootMenu?.removeSubMenu?.(item);\n});\n</script>\n<template>\n  <li\n    :class=\"[\n      b(),\n      is('opened', opened),\n      is('active', active),\n      is('disabled', disabled),\n    ]\"\n    @focus=\"handleMouseenter\"\n    @mouseenter=\"handleMouseenter\"\n    @mouseleave=\"() => handleMouseleave()\"\n  >\n    <template v-if=\"rootMenu.isMenuPopup\">\n      <VbenHoverCard\n        :content-class=\"[\n          rootMenu.theme,\n          nsMenu.e('popup-container'),\n          is(rootMenu.theme, true),\n          opened ? '' : 'hidden',\n          'overflow-auto',\n          'max-h-[calc(var(--radix-hover-card-content-available-height)-20px)]',\n        ]\"\n        :content-props=\"contentProps\"\n        :open=\"true\"\n        :open-delay=\"0\"\n      >\n        <template #trigger>\n          <SubMenuContent\n            :class=\"is('active', active)\"\n            :icon=\"menuIcon\"\n            :is-menu-more=\"isSubMenuMore\"\n            :is-top-level-menu-submenu=\"isTopLevelMenuSubmenu\"\n            :level=\"currentLevel\"\n            :path=\"path\"\n            @click.stop=\"handleClick\"\n          >\n            <template #title>\n              <slot name=\"title\"></slot>\n            </template>\n          </SubMenuContent>\n        </template>\n        <div\n          :class=\"[nsMenu.is(mode, true), nsMenu.e('popup')]\"\n          @focus=\"(e) => handleMouseenter(e, 100)\"\n          @mouseenter=\"(e) => handleMouseenter(e, 100)\"\n          @mouseleave=\"() => handleMouseleave(true)\"\n        >\n          <ul\n            :class=\"[nsMenu.b(), is('rounded', rounded)]\"\n            :style=\"subMenuStyle\"\n          >\n            <slot></slot>\n          </ul>\n        </div>\n      </VbenHoverCard>\n    </template>\n\n    <template v-else>\n      <SubMenuContent\n        :class=\"is('active', active)\"\n        :icon=\"menuIcon\"\n        :is-menu-more=\"isSubMenuMore\"\n        :is-top-level-menu-submenu=\"isTopLevelMenuSubmenu\"\n        :level=\"currentLevel\"\n        :path=\"path\"\n        @click.stop=\"handleClick\"\n      >\n        <slot name=\"content\"></slot>\n        <template #title>\n          <slot name=\"title\"></slot>\n        </template>\n      </SubMenuContent>\n      <CollapseTransition>\n        <ul\n          v-show=\"opened\"\n          :class=\"[nsMenu.b(), is('rounded', rounded)]\"\n          :style=\"subMenuStyle\"\n        >\n          <slot></slot>\n        </ul>\n      </CollapseTransition>\n    </template>\n  </li>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/menu-ui/src/hooks/index.ts",
    "content": "export * from './use-menu';\nexport * from './use-menu-context';\n"
  },
  {
    "path": "packages/@core/ui-kit/menu-ui/src/hooks/use-menu-context.ts",
    "content": "import type { MenuProvider, SubMenuProvider } from '../types';\n\nimport { getCurrentInstance, inject, provide } from 'vue';\n\nimport { findComponentUpward } from '../utils';\n\nconst menuContextKey = Symbol('menuContext');\n\n/**\n * @zh_CN Provide menu context\n */\nfunction createMenuContext(injectMenuData: MenuProvider) {\n  provide(menuContextKey, injectMenuData);\n}\n\n/**\n * @zh_CN Provide menu context\n */\nfunction createSubMenuContext(injectSubMenuData: SubMenuProvider) {\n  const instance = getCurrentInstance();\n\n  provide(`subMenu:${instance?.uid}`, injectSubMenuData);\n}\n\n/**\n * @zh_CN Inject menu context\n */\nfunction useMenuContext() {\n  const instance = getCurrentInstance();\n  if (!instance) {\n    throw new Error('instance is required');\n  }\n  const rootMenu = inject(menuContextKey) as MenuProvider;\n  return rootMenu;\n}\n\n/**\n * @zh_CN Inject menu context\n */\nfunction useSubMenuContext() {\n  const instance = getCurrentInstance();\n  if (!instance) {\n    throw new Error('instance is required');\n  }\n  const parentMenu = findComponentUpward(instance, ['Menu', 'SubMenu']);\n  const subMenu = inject(`subMenu:${parentMenu?.uid}`) as SubMenuProvider;\n  return subMenu;\n}\n\nexport {\n  createMenuContext,\n  createSubMenuContext,\n  useMenuContext,\n  useSubMenuContext,\n};\n"
  },
  {
    "path": "packages/@core/ui-kit/menu-ui/src/hooks/use-menu-scroll.ts",
    "content": "import type { Ref } from 'vue';\n\nimport { watch } from 'vue';\n\nimport { useDebounceFn } from '@vueuse/core';\n\ninterface UseMenuScrollOptions {\n  delay?: number;\n  enable?: boolean | Ref<boolean>;\n}\n\nexport function useMenuScroll(\n  activePath: Ref<string | undefined>,\n  options: UseMenuScrollOptions = {},\n) {\n  const { enable = true, delay = 320 } = options;\n\n  function scrollToActiveItem() {\n    const isEnabled = typeof enable === 'boolean' ? enable : enable.value;\n    if (!isEnabled) return;\n\n    const activeElement = document.querySelector(\n      `aside li[role=menuitem].is-active`,\n    );\n    if (activeElement) {\n      activeElement.scrollIntoView({\n        behavior: 'smooth',\n        block: 'center',\n        inline: 'center',\n      });\n    }\n  }\n\n  const debouncedScroll = useDebounceFn(scrollToActiveItem, delay);\n\n  watch(activePath, () => {\n    const isEnabled = typeof enable === 'boolean' ? enable : enable.value;\n    if (!isEnabled) return;\n\n    debouncedScroll();\n  });\n\n  return {\n    scrollToActiveItem,\n  };\n}\n"
  },
  {
    "path": "packages/@core/ui-kit/menu-ui/src/hooks/use-menu.ts",
    "content": "import type { SubMenuProvider } from '../types';\n\nimport { computed, getCurrentInstance } from 'vue';\n\nimport { findComponentUpward } from '../utils';\n\nfunction useMenu() {\n  const instance = getCurrentInstance();\n  if (!instance) {\n    throw new Error('instance is required');\n  }\n\n  /**\n   * @zh_CN 获取所有父级菜单链路\n   */\n  const parentPaths = computed(() => {\n    let parent = instance.parent;\n    const paths: string[] = [instance.props.path as string];\n    while (parent?.type.name !== 'Menu') {\n      if (parent?.props.path) {\n        paths.unshift(parent.props.path as string);\n      }\n      parent = parent?.parent ?? null;\n    }\n\n    return paths;\n  });\n\n  const parentMenu = computed(() => {\n    return findComponentUpward(instance, ['Menu', 'SubMenu']);\n  });\n\n  return {\n    parentMenu,\n    parentPaths,\n  };\n}\n\nfunction useMenuStyle(menu?: SubMenuProvider) {\n  const subMenuStyle = computed(() => {\n    return {\n      '--menu-level': menu ? (menu?.level ?? 0 + 1) : 0,\n    };\n  });\n  return subMenuStyle;\n}\n\nexport { useMenu, useMenuStyle };\n"
  },
  {
    "path": "packages/@core/ui-kit/menu-ui/src/index.ts",
    "content": "export { default as MenuBadge } from './components/menu-badge.vue';\nexport * from './components/normal-menu';\nexport { default as Menu } from './menu.vue';\nexport type * from './types';\n"
  },
  {
    "path": "packages/@core/ui-kit/menu-ui/src/menu.vue",
    "content": "<script setup lang=\"ts\">\nimport type { MenuRecordRaw } from '@vben-core/typings';\n\nimport type { MenuProps } from './types';\n\nimport { useForwardProps } from '@vben-core/composables';\n\nimport { Menu } from './components';\nimport SubMenu from './sub-menu.vue';\n\ninterface Props extends MenuProps {\n  menus: MenuRecordRaw[];\n}\n\ndefineOptions({\n  name: 'MenuView',\n});\n\nconst props = withDefaults(defineProps<Props>(), {\n  collapse: false,\n});\n\nconst forward = useForwardProps(props);\n</script>\n\n<template>\n  <Menu v-bind=\"forward\">\n    <template v-for=\"menu in menus\" :key=\"menu.path\">\n      <SubMenu :menu=\"menu\" />\n    </template>\n  </Menu>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/menu-ui/src/sub-menu.vue",
    "content": "<script setup lang=\"ts\">\nimport type { MenuRecordRaw } from '@vben-core/typings';\n\nimport { computed } from 'vue';\n\nimport { MenuBadge, MenuItem, SubMenu as SubMenuComp } from './components';\n// eslint-disable-next-line import/no-self-import\nimport SubMenu from './sub-menu.vue';\n\ninterface Props {\n  /**\n   * 菜单项\n   */\n  menu: MenuRecordRaw;\n}\n\ndefineOptions({\n  name: 'SubMenuUi',\n});\n\nconst props = withDefaults(defineProps<Props>(), {});\n\n/**\n * 判断是否有子节点，动态渲染 menu-item/sub-menu-item\n */\nconst hasChildren = computed(() => {\n  const { menu } = props;\n  return (\n    Reflect.has(menu, 'children') && !!menu.children && menu.children.length > 0\n  );\n});\n</script>\n\n<template>\n  <MenuItem\n    v-if=\"!hasChildren\"\n    :key=\"menu.path\"\n    :active-icon=\"menu.activeIcon\"\n    :badge=\"menu.badge\"\n    :badge-type=\"menu.badgeType\"\n    :badge-variants=\"menu.badgeVariants\"\n    :icon=\"menu.icon\"\n    :path=\"menu.path\"\n  >\n    <template #title>\n      <span>{{ menu.name }}</span>\n    </template>\n  </MenuItem>\n  <SubMenuComp\n    v-else\n    :key=\"`${menu.path}_sub`\"\n    :active-icon=\"menu.activeIcon\"\n    :icon=\"menu.icon\"\n    :path=\"menu.path\"\n  >\n    <template #content>\n      <MenuBadge\n        :badge=\"menu.badge\"\n        :badge-type=\"menu.badgeType\"\n        :badge-variants=\"menu.badgeVariants\"\n        class=\"right-6\"\n      />\n    </template>\n    <template #title>\n      <span>{{ menu.name }}</span>\n    </template>\n    <template v-for=\"childItem in menu.children || []\" :key=\"childItem.path\">\n      <SubMenu :menu=\"childItem\" />\n    </template>\n  </SubMenuComp>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/menu-ui/src/types.ts",
    "content": "import type { MenuRecordBadgeRaw, ThemeModeType } from '@vben-core/typings';\nimport type { Component, Ref } from 'vue';\n\ninterface MenuProps {\n  /**\n   * @zh_CN 是否开启手风琴模式\n   * @default true\n   */\n  accordion?: boolean;\n  /**\n   * @zh_CN 菜单是否折叠\n   * @default false\n   */\n  collapse?: boolean;\n\n  /**\n   * @zh_CN 菜单折叠时是否显示菜单名称\n   * @default false\n   */\n  collapseShowTitle?: boolean;\n\n  /**\n   * @zh_CN 默认激活的菜单\n   */\n  defaultActive?: string;\n\n  /**\n   * @zh_CN 默认展开的菜单\n   */\n  defaultOpeneds?: string[];\n\n  /**\n   * @zh_CN 菜单模式\n   * @default vertical\n   */\n  mode?: 'horizontal' | 'vertical';\n\n  /**\n   * @zh_CN 是否圆润风格\n   * @default true\n   */\n  rounded?: boolean;\n\n  /**\n   * @zh_CN 是否自动滚动到激活的菜单项\n   * @default false\n   */\n  scrollToActive?: boolean;\n\n  /**\n   * @zh_CN 菜单主题\n   * @default dark\n   */\n  theme?: ThemeModeType;\n}\n\ninterface SubMenuProps extends MenuRecordBadgeRaw {\n  /**\n   * @zh_CN 激活图标\n   */\n  activeIcon?: string;\n  /**\n   * @zh_CN 是否禁用\n   */\n  disabled?: boolean;\n  /**\n   * @zh_CN 图标\n   */\n  icon?: Component | string;\n  /**\n   * @zh_CN submenu 名称\n   */\n  path: string;\n}\n\ninterface MenuItemProps extends MenuRecordBadgeRaw {\n  /**\n   * @zh_CN 图标\n   */\n  activeIcon?: string;\n  /**\n   * @zh_CN 是否禁用\n   */\n  disabled?: boolean;\n  /**\n   * @zh_CN 图标\n   */\n  icon?: Component | string;\n  /**\n   * @zh_CN menuitem 名称\n   */\n  path: string;\n}\n\ninterface MenuItemRegistered {\n  active: boolean;\n  parentPaths: string[];\n  path: string;\n}\n\ninterface MenuItemClicked {\n  parentPaths: string[];\n  path: string;\n}\n\ninterface MenuProvider {\n  activePath?: string;\n  addMenuItem: (item: MenuItemRegistered) => void;\n\n  addSubMenu: (item: MenuItemRegistered) => void;\n  closeMenu: (path: string, parentLinks: string[]) => void;\n  handleMenuItemClick: (item: MenuItemClicked) => void;\n  handleSubMenuClick: (subMenu: MenuItemRegistered) => void;\n  isMenuPopup: boolean;\n  items: Record<string, MenuItemRegistered>;\n\n  openedMenus: string[];\n  openMenu: (path: string, parentLinks: string[]) => void;\n  props: MenuProps;\n  removeMenuItem: (item: MenuItemRegistered) => void;\n\n  removeSubMenu: (item: MenuItemRegistered) => void;\n\n  subMenus: Record<string, MenuItemRegistered>;\n  theme: string;\n}\n\ninterface SubMenuProvider {\n  addSubMenu: (item: MenuItemRegistered) => void;\n  handleMouseleave?: (deepDispatch: boolean) => void;\n  level: number;\n  mouseInChild: Ref<boolean>;\n  removeSubMenu: (item: MenuItemRegistered) => void;\n}\n\nexport type {\n  MenuItemClicked,\n  MenuItemProps,\n  MenuItemRegistered,\n  MenuProps,\n  MenuProvider,\n  SubMenuProps,\n  SubMenuProvider,\n};\n"
  },
  {
    "path": "packages/@core/ui-kit/menu-ui/src/utils/index.ts",
    "content": "import type {\n  ComponentInternalInstance,\n  VNode,\n  VNodeChild,\n  VNodeNormalizedChildren,\n} from 'vue';\n\nimport { isVNode } from 'vue';\n\ntype VNodeChildAtom = Exclude<VNodeChild, Array<any>>;\ntype RawSlots = Exclude<VNodeNormalizedChildren, Array<any> | null | string>;\n\ntype FlattenVNodes = Array<RawSlots | VNodeChildAtom>;\n\n/**\n * @zh_CN Find the parent component upward\n * @param instance\n * @param parentNames\n */\nfunction findComponentUpward(\n  instance: ComponentInternalInstance,\n  parentNames: string[],\n) {\n  let parent = instance.parent;\n  while (parent && !parentNames.includes(parent?.type?.name ?? '')) {\n    parent = parent.parent;\n  }\n  return parent;\n}\n\nconst flattedChildren = (\n  children: FlattenVNodes | VNode | VNodeNormalizedChildren,\n): FlattenVNodes => {\n  const vNodes = Array.isArray(children) ? children : [children];\n  const result: FlattenVNodes = [];\n\n  vNodes.forEach((child) => {\n    if (Array.isArray(child)) {\n      result.push(...flattedChildren(child));\n    } else if (isVNode(child) && Array.isArray(child.children)) {\n      result.push(...flattedChildren(child.children));\n    } else {\n      result.push(child);\n      if (isVNode(child) && child.component?.subTree) {\n        result.push(...flattedChildren(child.component.subTree));\n      }\n    }\n  });\n  return result;\n};\n\nexport { findComponentUpward, flattedChildren };\n"
  },
  {
    "path": "packages/@core/ui-kit/menu-ui/tailwind.config.mjs",
    "content": "export { default } from '@vben/tailwind-config';\n"
  },
  {
    "path": "packages/@core/ui-kit/menu-ui/tsconfig.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/tsconfig\",\n  \"extends\": \"@vben/tsconfig/web.json\",\n  \"include\": [\"src\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "packages/@core/ui-kit/popup-ui/build.config.ts",
    "content": "import { defineBuildConfig } from 'unbuild';\n\nexport default defineBuildConfig({\n  clean: true,\n  declaration: true,\n  entries: [\n    {\n      builder: 'mkdist',\n      input: './src',\n      loaders: ['vue'],\n      pattern: ['**/*.vue'],\n    },\n    {\n      builder: 'mkdist',\n      format: 'esm',\n      input: './src',\n      loaders: ['js'],\n      pattern: ['**/*.ts'],\n    },\n  ],\n});\n"
  },
  {
    "path": "packages/@core/ui-kit/popup-ui/package.json",
    "content": "{\n  \"name\": \"@vben-core/popup-ui\",\n  \"version\": \"5.2.1\",\n  \"homepage\": \"https://github.com/vbenjs/vue-vben-admin\",\n  \"bugs\": \"https://github.com/vbenjs/vue-vben-admin/issues\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/vbenjs/vue-vben-admin.git\",\n    \"directory\": \"packages/@vben-core/uikit/popup-ui\"\n  },\n  \"license\": \"MIT\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"build\": \"pnpm unbuild\",\n    \"prepublishOnly\": \"npm run build\"\n  },\n  \"files\": [\n    \"dist\"\n  ],\n  \"sideEffects\": [\n    \"**/*.css\"\n  ],\n  \"main\": \"./dist/index.mjs\",\n  \"module\": \"./dist/index.mjs\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./src/index.ts\",\n      \"development\": \"./src/index.ts\",\n      \"default\": \"./dist/index.mjs\"\n    }\n  },\n  \"publishConfig\": {\n    \"exports\": {\n      \".\": {\n        \"default\": \"./dist/index.mjs\"\n      }\n    }\n  },\n  \"dependencies\": {\n    \"@vben-core/composables\": \"workspace:*\",\n    \"@vben-core/icons\": \"workspace:*\",\n    \"@vben-core/shadcn-ui\": \"workspace:*\",\n    \"@vben-core/shared\": \"workspace:*\",\n    \"@vben-core/typings\": \"workspace:*\",\n    \"@vueuse/core\": \"catalog:\",\n    \"vue\": \"catalog:\"\n  }\n}\n"
  },
  {
    "path": "packages/@core/ui-kit/popup-ui/postcss.config.mjs",
    "content": "export { default } from '@vben/tailwind-config/postcss';\n"
  },
  {
    "path": "packages/@core/ui-kit/popup-ui/src/alert/AlertBuilder.ts",
    "content": "import type { Component, VNode } from 'vue';\n\nimport type { Recordable } from '@vben-core/typings';\n\nimport type { AlertProps, BeforeCloseScope, PromptProps } from './alert';\n\nimport { h, nextTick, ref, render } from 'vue';\n\nimport { useSimpleLocale } from '@vben-core/composables';\nimport { Input, VbenRenderContent } from '@vben-core/shadcn-ui';\nimport { isFunction, isString } from '@vben-core/shared/utils';\n\nimport Alert from './alert.vue';\n\nconst alerts = ref<Array<{ container: HTMLElement; instance: Component }>>([]);\n\nconst { $t } = useSimpleLocale();\n\nexport function vbenAlert(options: AlertProps): Promise<void>;\nexport function vbenAlert(\n  message: string,\n  options?: Partial<AlertProps>,\n): Promise<void>;\nexport function vbenAlert(\n  message: string,\n  title?: string,\n  options?: Partial<AlertProps>,\n): Promise<void>;\n\nexport function vbenAlert(\n  arg0: AlertProps | string,\n  arg1?: Partial<AlertProps> | string,\n  arg2?: Partial<AlertProps>,\n): Promise<void> {\n  return new Promise((resolve, reject) => {\n    const options: AlertProps = isString(arg0)\n      ? {\n          content: arg0,\n        }\n      : { ...arg0 };\n    if (arg1) {\n      if (isString(arg1)) {\n        options.title = arg1;\n      } else if (!isString(arg1)) {\n        // 如果第二个参数是对象，则合并到选项中\n        Object.assign(options, arg1);\n      }\n    }\n\n    if (arg2 && !isString(arg2)) {\n      Object.assign(options, arg2);\n    }\n    // 创建容器元素\n    const container = document.createElement('div');\n    document.body.append(container);\n\n    // 创建一个引用，用于在回调中访问实例\n    const alertRef = { container, instance: null as any };\n\n    const props: AlertProps & Recordable<any> = {\n      onClosed: (isConfirm: boolean) => {\n        // 移除组件实例以及创建的所有dom（恢复页面到打开前的状态）\n        // 从alerts数组中移除该实例\n        alerts.value = alerts.value.filter((item) => item !== alertRef);\n\n        // 从DOM中移除容器\n        render(null, container);\n        if (container.parentNode) {\n          container.remove();\n        }\n\n        // 解析 Promise，传递用户操作结果\n        if (isConfirm) {\n          resolve();\n        } else {\n          reject(new Error('dialog cancelled'));\n        }\n      },\n      ...options,\n      open: true,\n      title: options.title ?? $t.value('prompt'),\n    };\n\n    // 创建Alert组件的VNode\n    const vnode = h(Alert, props);\n\n    // 渲染组件到容器\n    render(vnode, container);\n\n    // 保存组件实例引用\n    alertRef.instance = vnode.component?.proxy as Component;\n\n    // 将实例和容器添加到alerts数组中\n    alerts.value.push(alertRef);\n  });\n}\n\nexport function vbenConfirm(options: AlertProps): Promise<void>;\nexport function vbenConfirm(\n  message: string,\n  options?: Partial<AlertProps>,\n): Promise<void>;\nexport function vbenConfirm(\n  message: string,\n  title?: string,\n  options?: Partial<AlertProps>,\n): Promise<void>;\n\nexport function vbenConfirm(\n  arg0: AlertProps | string,\n  arg1?: Partial<AlertProps> | string,\n  arg2?: Partial<AlertProps>,\n): Promise<void> {\n  const defaultProps: Partial<AlertProps> = {\n    showCancel: true,\n  };\n  if (!arg1) {\n    return isString(arg0)\n      ? vbenAlert(arg0, defaultProps)\n      : vbenAlert({ ...defaultProps, ...arg0 });\n  } else if (!arg2) {\n    return isString(arg1)\n      ? vbenAlert(arg0 as string, arg1, defaultProps)\n      : vbenAlert(arg0 as string, { ...defaultProps, ...arg1 });\n  }\n  return vbenAlert(arg0 as string, arg1 as string, {\n    ...defaultProps,\n    ...arg2,\n  });\n}\n\nexport async function vbenPrompt<T = any>(\n  options: PromptProps<T>,\n): Promise<T | undefined> {\n  const {\n    component: _component,\n    componentProps: _componentProps,\n    componentSlots,\n    content,\n    defaultValue,\n    modelPropName: _modelPropName,\n    ...delegated\n  } = options;\n\n  const modelValue = ref<T | undefined>(defaultValue);\n  const inputComponentRef = ref<null | VNode>(null);\n  const staticContents: Component[] = [];\n\n  staticContents.push(h(VbenRenderContent, { content, renderBr: true }));\n\n  const modelPropName = _modelPropName || 'modelValue';\n  const componentProps = { ..._componentProps };\n\n  // 每次渲染时都会重新计算的内容函数\n  const contentRenderer = () => {\n    const currentProps = { ...componentProps };\n\n    // 设置当前值\n    currentProps[modelPropName] = modelValue.value;\n\n    // 设置更新处理函数\n    currentProps[`onUpdate:${modelPropName}`] = (val: T) => {\n      modelValue.value = val;\n    };\n\n    // 创建输入组件\n    inputComponentRef.value = h(\n      _component || Input,\n      currentProps,\n      componentSlots,\n    );\n\n    // 返回包含静态内容和输入组件的数组\n    return h(\n      'div',\n      { class: 'flex flex-col gap-2' },\n      { default: () => [...staticContents, inputComponentRef.value] },\n    );\n  };\n\n  const props: AlertProps & Recordable<any> = {\n    ...delegated,\n    async beforeClose(scope: BeforeCloseScope) {\n      if (delegated.beforeClose) {\n        return await delegated.beforeClose({\n          ...scope,\n          value: modelValue.value,\n        });\n      }\n    },\n    // 使用函数形式，每次渲染都会重新计算内容\n    content: contentRenderer,\n    contentMasking: true,\n    async onOpened() {\n      await nextTick();\n      const componentRef: null | VNode = inputComponentRef.value;\n      if (componentRef) {\n        if (\n          componentRef.component?.exposed &&\n          isFunction(componentRef.component.exposed.focus)\n        ) {\n          componentRef.component.exposed.focus();\n        } else {\n          if (componentRef.el) {\n            if (\n              isFunction(componentRef.el.focus) &&\n              ['BUTTON', 'INPUT', 'SELECT', 'TEXTAREA'].includes(\n                componentRef.el.tagName,\n              )\n            ) {\n              componentRef.el.focus();\n            } else if (isFunction(componentRef.el.querySelector)) {\n              const focusableElement = componentRef.el.querySelector(\n                'input, select, textarea, button',\n              );\n              if (focusableElement && isFunction(focusableElement.focus)) {\n                focusableElement.focus();\n              }\n            } else if (\n              componentRef.el.nextElementSibling &&\n              isFunction(componentRef.el.nextElementSibling.focus)\n            ) {\n              componentRef.el.nextElementSibling.focus();\n            }\n          }\n        }\n      }\n    },\n  };\n\n  await vbenConfirm(props);\n  return modelValue.value;\n}\n\nexport function clearAllAlerts() {\n  alerts.value.forEach((alert) => {\n    // 从DOM中移除容器\n    render(null, alert.container);\n    if (alert.container.parentNode) {\n      alert.container.remove();\n    }\n  });\n  alerts.value = [];\n}\n"
  },
  {
    "path": "packages/@core/ui-kit/popup-ui/src/alert/alert.ts",
    "content": "import type { Component, VNode, VNodeArrayChildren } from 'vue';\n\nimport type { Recordable } from '@vben-core/typings';\n\nimport { createContext } from '@vben-core/shadcn-ui';\n\nexport type IconType = 'error' | 'info' | 'question' | 'success' | 'warning';\n\nexport type BeforeCloseScope = {\n  isConfirm: boolean;\n};\n\nexport type AlertProps = {\n  /** 关闭前的回调，如果返回false，则终止关闭 */\n  beforeClose?: (\n    scope: BeforeCloseScope,\n  ) => boolean | Promise<boolean | undefined> | undefined;\n  /** 边框 */\n  bordered?: boolean;\n  /**\n   * 按钮对齐方式\n   * @default 'end'\n   */\n  buttonAlign?: 'center' | 'end' | 'start';\n  /** 取消按钮的标题 */\n  cancelText?: string;\n  /** 是否居中显示 */\n  centered?: boolean;\n  /** 确认按钮的标题 */\n  confirmText?: string;\n  /** 弹窗容器的额外样式 */\n  containerClass?: string;\n  /** 弹窗提示内容 */\n  content: Component | string;\n  /** 弹窗内容的额外样式 */\n  contentClass?: string;\n  /** 执行beforeClose回调期间，在内容区域显示一个loading遮罩*/\n  contentMasking?: boolean;\n  /** 弹窗底部内容（与按钮在同一个容器中） */\n  footer?: Component | string;\n  /** 弹窗的图标（在标题的前面） */\n  icon?: Component | IconType;\n  /**\n   * 弹窗遮罩模糊效果\n   */\n  overlayBlur?: number;\n  /** 是否显示取消按钮 */\n  showCancel?: boolean;\n  /** 弹窗标题 */\n  title?: string;\n};\n\n/** Prompt属性 */\nexport type PromptProps<T = any> = {\n  /** 关闭前的回调，如果返回false，则终止关闭 */\n  beforeClose?: (scope: {\n    isConfirm: boolean;\n    value: T | undefined;\n  }) => boolean | Promise<boolean | undefined> | undefined;\n  /** 用于接受用户输入的组件 */\n  component?: Component;\n  /** 输入组件的属性 */\n  componentProps?: Recordable<any>;\n  /** 输入组件的插槽 */\n  componentSlots?:\n    | (() => any)\n    | Recordable<unknown>\n    | VNode\n    | VNodeArrayChildren;\n  /** 默认值 */\n  defaultValue?: T;\n  /** 输入组件的值属性名 */\n  modelPropName?: string;\n} & Omit<AlertProps, 'beforeClose'>;\n\n/**\n * Alert上下文\n */\nexport type AlertContext = {\n  /** 执行取消操作 */\n  doCancel: () => void;\n  /** 执行确认操作 */\n  doConfirm: () => void;\n};\n\nexport const [injectAlertContext, provideAlertContext] =\n  createContext<AlertContext>('VbenAlertContext');\n\n/**\n * 获取Alert上下文\n * @returns AlertContext\n */\nexport function useAlertContext() {\n  const context = injectAlertContext();\n  if (!context) {\n    throw new Error('useAlertContext must be used within an AlertProvider');\n  }\n  return context;\n}\n"
  },
  {
    "path": "packages/@core/ui-kit/popup-ui/src/alert/alert.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { Component } from 'vue';\n\nimport type { AlertProps } from './alert';\n\nimport { computed, h, nextTick, ref } from 'vue';\n\nimport { useSimpleLocale } from '@vben-core/composables';\nimport {\n  CircleAlert,\n  CircleCheckBig,\n  CircleHelp,\n  CircleX,\n  Info,\n  X,\n} from '@vben-core/icons';\nimport {\n  AlertDialog,\n  AlertDialogAction,\n  AlertDialogCancel,\n  AlertDialogContent,\n  AlertDialogDescription,\n  AlertDialogTitle,\n  VbenButton,\n  VbenLoading,\n  VbenRenderContent,\n} from '@vben-core/shadcn-ui';\nimport { globalShareState } from '@vben-core/shared/global-state';\nimport { cn } from '@vben-core/shared/utils';\n\nimport { provideAlertContext } from './alert';\n\nconst props = withDefaults(defineProps<AlertProps>(), {\n  bordered: true,\n  buttonAlign: 'end',\n  centered: true,\n});\nconst emits = defineEmits(['closed', 'confirm', 'opened']);\nconst open = defineModel<boolean>('open', { default: false });\nconst { $t } = useSimpleLocale();\nconst components = globalShareState.getComponents();\nconst isConfirm = ref(false);\n\nfunction onAlertClosed() {\n  emits('closed', isConfirm.value);\n  isConfirm.value = false;\n}\n\nfunction onEscapeKeyDown() {\n  isConfirm.value = false;\n}\n\nconst getIconRender = computed(() => {\n  let iconRender: Component | null = null;\n  if (props.icon) {\n    if (typeof props.icon === 'string') {\n      switch (props.icon) {\n        case 'error': {\n          iconRender = h(CircleX, {\n            style: { color: 'hsl(var(--destructive))' },\n          });\n          break;\n        }\n        case 'info': {\n          iconRender = h(Info, { style: { color: 'hsl(var(--info))' } });\n          break;\n        }\n        case 'question': {\n          iconRender = CircleHelp;\n          break;\n        }\n        case 'success': {\n          iconRender = h(CircleCheckBig, {\n            style: { color: 'hsl(var(--success))' },\n          });\n          break;\n        }\n        case 'warning': {\n          iconRender = h(CircleAlert, {\n            style: { color: 'hsl(var(--warning))' },\n          });\n          break;\n        }\n        default: {\n          iconRender = null;\n          break;\n        }\n      }\n    }\n  } else {\n    iconRender = props.icon ?? null;\n  }\n  return iconRender;\n});\n\nfunction doCancel() {\n  handleCancel();\n  handleOpenChange(false);\n}\n\nfunction doConfirm() {\n  handleConfirm();\n  handleOpenChange(false);\n}\n\nprovideAlertContext({\n  doCancel,\n  doConfirm,\n});\n\nfunction handleConfirm() {\n  isConfirm.value = true;\n  emits('confirm');\n}\n\nfunction handleCancel() {\n  isConfirm.value = false;\n}\n\nconst loading = ref(false);\nasync function handleOpenChange(val: boolean) {\n  await nextTick(); // 等待标记isConfirm状态\n  if (!val && props.beforeClose) {\n    loading.value = true;\n    try {\n      const res = await props.beforeClose({ isConfirm: isConfirm.value });\n      if (res !== false) {\n        open.value = false;\n      }\n    } finally {\n      loading.value = false;\n    }\n  } else {\n    open.value = val;\n  }\n}\n</script>\n<template>\n  <AlertDialog :open=\"open\" @update:open=\"handleOpenChange\">\n    <AlertDialogContent\n      :open=\"open\"\n      :centered=\"centered\"\n      :overlay-blur=\"overlayBlur\"\n      @opened=\"emits('opened')\"\n      @closed=\"onAlertClosed\"\n      @escape-key-down=\"onEscapeKeyDown\"\n      :class=\"\n        cn(\n          containerClass,\n          'left-0 right-0 mx-auto flex max-h-[80%] flex-col p-0 duration-300 sm:w-[520px] sm:max-w-[80%] sm:rounded-[var(--radius)]',\n          {\n            'border-border border': bordered,\n            'shadow-3xl': !bordered,\n          },\n        )\n      \"\n    >\n      <div :class=\"cn('relative flex-1 overflow-y-auto p-3', contentClass)\">\n        <AlertDialogTitle v-if=\"title\">\n          <div class=\"flex items-center\">\n            <component :is=\"getIconRender\" class=\"mr-2\" />\n            <span class=\"flex-auto\">{{ $t(title) }}</span>\n            <AlertDialogCancel v-if=\"showCancel\" as-child>\n              <VbenButton\n                variant=\"ghost\"\n                size=\"icon\"\n                class=\"rounded-full\"\n                :disabled=\"loading\"\n                @click=\"handleCancel\"\n              >\n                <X class=\"text-muted-foreground size-4\" />\n              </VbenButton>\n            </AlertDialogCancel>\n          </div>\n        </AlertDialogTitle>\n        <AlertDialogDescription>\n          <div class=\"m-4 min-h-[30px]\">\n            <VbenRenderContent :content=\"content\" render-br />\n          </div>\n          <VbenLoading v-if=\"loading && contentMasking\" :spinning=\"loading\" />\n        </AlertDialogDescription>\n        <div\n          class=\"flex items-center justify-end gap-x-2\"\n          :class=\"`justify-${buttonAlign}`\"\n        >\n          <VbenRenderContent :content=\"footer\" />\n          <AlertDialogCancel v-if=\"showCancel\" as-child>\n            <component\n              :is=\"components.DefaultButton || VbenButton\"\n              :disabled=\"loading\"\n              variant=\"ghost\"\n              @click=\"handleCancel\"\n            >\n              {{ cancelText || $t('cancel') }}\n            </component>\n          </AlertDialogCancel>\n          <AlertDialogAction as-child>\n            <component\n              :is=\"components.PrimaryButton || VbenButton\"\n              :loading=\"loading\"\n              @click=\"handleConfirm\"\n            >\n              {{ confirmText || $t('confirm') }}\n            </component>\n          </AlertDialogAction>\n        </div>\n      </div>\n    </AlertDialogContent>\n  </AlertDialog>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/popup-ui/src/alert/index.ts",
    "content": "export type {\n  AlertProps,\n  BeforeCloseScope,\n  IconType,\n  PromptProps,\n} from './alert';\nexport { useAlertContext } from './alert';\nexport { default as Alert } from './alert.vue';\nexport {\n  vbenAlert as alert,\n  clearAllAlerts,\n  vbenConfirm as confirm,\n  vbenPrompt as prompt,\n} from './AlertBuilder';\n"
  },
  {
    "path": "packages/@core/ui-kit/popup-ui/src/drawer/__tests__/drawer-api.test.ts",
    "content": "import type { DrawerState } from '../drawer';\n\nimport { beforeEach, describe, expect, it, vi } from 'vitest';\n\nimport { DrawerApi } from '../drawer-api';\n\n// 模拟 Store 类\nvi.mock('@vben-core/shared/store', () => {\n  return {\n    isFunction: (fn: any) => typeof fn === 'function',\n    Store: class {\n      get state() {\n        return this._state;\n      }\n      private _state: DrawerState;\n\n      private options: any;\n\n      constructor(initialState: DrawerState, options: any) {\n        this._state = initialState;\n        this.options = options;\n      }\n\n      batch(cb: () => void) {\n        cb();\n      }\n\n      setState(fn: (prev: DrawerState) => DrawerState) {\n        this._state = fn(this._state);\n        this.options.onUpdate();\n      }\n    },\n  };\n});\n\ndescribe('drawerApi', () => {\n  let drawerApi: DrawerApi;\n  let drawerState: DrawerState;\n\n  beforeEach(() => {\n    drawerApi = new DrawerApi();\n    drawerState = drawerApi.store.state;\n  });\n\n  it('should initialize with default state', () => {\n    expect(drawerState.isOpen).toBe(false);\n    expect(drawerState.cancelText).toBe(undefined);\n    expect(drawerState.confirmText).toBe(undefined);\n  });\n\n  it('should open the drawer', () => {\n    drawerApi.open();\n    expect(drawerApi.store.state.isOpen).toBe(true);\n  });\n\n  it('should close the drawer if onBeforeClose allows it', () => {\n    drawerApi.close();\n    expect(drawerApi.store.state.isOpen).toBe(false);\n  });\n\n  it('should not close the drawer if onBeforeClose returns false', () => {\n    const onBeforeClose = vi.fn(() => false);\n    const drawerApiWithHook = new DrawerApi({ onBeforeClose });\n    drawerApiWithHook.open();\n    drawerApiWithHook.close();\n    expect(drawerApiWithHook.store.state.isOpen).toBe(true);\n    expect(onBeforeClose).toHaveBeenCalled();\n  });\n\n  it('should trigger onCancel and keep drawer open if onCancel is provided', () => {\n    const onCancel = vi.fn();\n    const drawerApiWithHook = new DrawerApi({ onCancel });\n    drawerApiWithHook.open();\n    drawerApiWithHook.onCancel();\n    expect(onCancel).toHaveBeenCalled();\n    expect(drawerApiWithHook.store.state.isOpen).toBe(true); // 关闭逻辑不在 onCancel 内\n  });\n\n  it('should update shared data correctly', () => {\n    const testData = { key: 'value' };\n    drawerApi.setData(testData);\n    expect(drawerApi.getData()).toEqual(testData);\n  });\n\n  it('should set state correctly using an object', () => {\n    drawerApi.setState({ title: 'New Title' });\n    expect(drawerApi.store.state.title).toBe('New Title');\n  });\n\n  it('should set state correctly using a function', () => {\n    drawerApi.setState((prev) => ({ ...prev, confirmText: 'Yes' }));\n    expect(drawerApi.store.state.confirmText).toBe('Yes');\n  });\n\n  it('should call onOpenChange when state changes', () => {\n    const onOpenChange = vi.fn();\n    const drawerApiWithHook = new DrawerApi({ onOpenChange });\n    drawerApiWithHook.open();\n    expect(onOpenChange).toHaveBeenCalledWith(true);\n  });\n\n  it('should call onClosed callback when provided', () => {\n    const onClosed = vi.fn();\n    const drawerApiWithHook = new DrawerApi({ onClosed });\n    drawerApiWithHook.onClosed();\n    expect(onClosed).toHaveBeenCalled();\n  });\n\n  it('should call onOpened callback when provided', () => {\n    const onOpened = vi.fn();\n    const drawerApiWithHook = new DrawerApi({ onOpened });\n    drawerApiWithHook.open();\n    drawerApiWithHook.onOpened();\n    expect(onOpened).toHaveBeenCalled();\n  });\n});\n"
  },
  {
    "path": "packages/@core/ui-kit/popup-ui/src/drawer/drawer-api.ts",
    "content": "import type { DrawerApiOptions, DrawerState } from './drawer';\n\nimport { Store } from '@vben-core/shared/store';\nimport { bindMethods, isFunction } from '@vben-core/shared/utils';\n\nexport class DrawerApi {\n  // 共享数据\n  public sharedData: Record<'payload', any> = {\n    payload: {},\n  };\n  public store: Store<DrawerState>;\n\n  private api: Pick<\n    DrawerApiOptions,\n    | 'onBeforeClose'\n    | 'onCancel'\n    | 'onClosed'\n    | 'onConfirm'\n    | 'onOpenChange'\n    | 'onOpened'\n  >;\n\n  // private prevState!: DrawerState;\n  private state!: DrawerState;\n\n  constructor(options: DrawerApiOptions = {}) {\n    const {\n      connectedComponent: _,\n      onBeforeClose,\n      onCancel,\n      onClosed,\n      onConfirm,\n      onOpenChange,\n      onOpened,\n      ...storeState\n    } = options;\n\n    const defaultState: DrawerState = {\n      class: '',\n      closable: true,\n      closeIconPlacement: 'right',\n      closeOnClickModal: true,\n      closeOnPressEscape: true,\n      confirmLoading: false,\n      contentClass: '',\n      footer: true,\n      header: true,\n      isOpen: false,\n      loading: false,\n      modal: true,\n      openAutoFocus: false,\n      placement: 'right',\n      showCancelButton: true,\n      showConfirmButton: true,\n      submitting: false,\n      title: '',\n    };\n\n    this.store = new Store<DrawerState>(\n      {\n        ...defaultState,\n        ...storeState,\n      },\n      {\n        onUpdate: () => {\n          const state = this.store.state;\n          if (state?.isOpen === this.state?.isOpen) {\n            this.state = state;\n          } else {\n            this.state = state;\n            this.api.onOpenChange?.(!!state?.isOpen);\n          }\n        },\n      },\n    );\n    this.state = this.store.state;\n    this.api = {\n      onBeforeClose,\n      onCancel,\n      onClosed,\n      onConfirm,\n      onOpenChange,\n      onOpened,\n    };\n    bindMethods(this);\n  }\n\n  /**\n   * 关闭抽屉\n   * @description 关闭抽屉时会调用 onBeforeClose 钩子函数，如果 onBeforeClose 返回 false，则不关闭弹窗\n   */\n  async close() {\n    // 通过 onBeforeClose 钩子函数来判断是否允许关闭弹窗\n    // 如果 onBeforeClose 返回 false，则不关闭弹窗\n    const allowClose = (await this.api.onBeforeClose?.()) ?? true;\n    if (allowClose) {\n      this.store.setState((prev) => ({\n        ...prev,\n        isOpen: false,\n        submitting: false,\n      }));\n    }\n  }\n\n  /**\n   * loading和lock的区别\n   * loading允许关闭窗口\n   * lock不允许关闭窗口\n   * @param loading 是否loading\n   */\n  drawerLoading(loading: boolean) {\n    this.setState({ confirmLoading: loading, loading });\n  }\n\n  getData<T extends object = Record<string, any>>() {\n    return (this.sharedData?.payload ?? {}) as T;\n  }\n\n  /**\n   * 锁定抽屉状态（用于提交过程中的等待状态）\n   * @description 锁定状态将禁用默认的取消按钮，使用spinner覆盖抽屉内容，隐藏关闭按钮，阻止手动关闭弹窗，将默认的提交按钮标记为loading状态\n   * @param isLocked 是否锁定\n   */\n  lock(isLocked: boolean = true) {\n    return this.setState({ submitting: isLocked });\n  }\n\n  /**\n   * 取消操作\n   */\n  onCancel() {\n    if (this.api.onCancel) {\n      this.api.onCancel?.();\n    } else {\n      this.close();\n    }\n  }\n\n  /**\n   * 弹窗关闭动画播放完毕后的回调\n   */\n  onClosed() {\n    if (!this.state.isOpen) {\n      this.api.onClosed?.();\n    }\n  }\n\n  /**\n   * 确认操作\n   */\n  onConfirm() {\n    this.api.onConfirm?.();\n  }\n\n  /**\n   * 弹窗打开动画播放完毕后的回调\n   */\n  onOpened() {\n    if (this.state.isOpen) {\n      this.api.onOpened?.();\n    }\n  }\n\n  open() {\n    this.store.setState((prev) => ({ ...prev, isOpen: true }));\n  }\n\n  setData<T>(payload: T) {\n    this.sharedData.payload = payload;\n    return this;\n  }\n\n  setState(\n    stateOrFn:\n      | ((prev: DrawerState) => Partial<DrawerState>)\n      | Partial<DrawerState>,\n  ) {\n    if (isFunction(stateOrFn)) {\n      this.store.setState(stateOrFn);\n    } else {\n      this.store.setState((prev) => ({ ...prev, ...stateOrFn }));\n    }\n    return this;\n  }\n\n  /**\n   * 解除抽屉的锁定状态\n   * @description 解除由lock方法设置的锁定状态，是lock(false)的别名\n   */\n  unlock() {\n    return this.lock(false);\n  }\n}\n"
  },
  {
    "path": "packages/@core/ui-kit/popup-ui/src/drawer/drawer.ts",
    "content": "import type { Component, Ref } from 'vue';\n\nimport type { ClassType, MaybePromise } from '@vben-core/typings';\n\nimport type { DrawerApi } from './drawer-api';\n\nexport type DrawerPlacement = 'bottom' | 'left' | 'right' | 'top';\n\nexport type CloseIconPlacement = 'left' | 'right';\n\nexport interface DrawerProps {\n  /**\n   * 是否挂载到内容区域\n   * @default false\n   */\n  appendToMain?: boolean;\n  /**\n   * 取消按钮文字\n   */\n  cancelText?: string;\n  class?: ClassType;\n  /**\n   * 是否显示关闭按钮\n   * @default true\n   */\n  closable?: boolean;\n  /**\n   * 关闭按钮的位置\n   */\n  closeIconPlacement?: CloseIconPlacement;\n  /**\n   * 点击弹窗遮罩是否关闭弹窗\n   * @default true\n   */\n  closeOnClickModal?: boolean;\n  /**\n   * 按下 ESC 键是否关闭弹窗\n   * @default true\n   */\n  closeOnPressEscape?: boolean;\n  /**\n   * 确定按钮 loading\n   * @default false\n   */\n  confirmLoading?: boolean;\n  /**\n   * 确定按钮文字\n   */\n  confirmText?: string;\n  contentClass?: string;\n  /**\n   * 弹窗描述\n   */\n  description?: string;\n  /**\n   * 在关闭时销毁抽屉\n   */\n  destroyOnClose?: boolean;\n  /**\n   * 是否显示底部\n   * @default true\n   */\n  footer?: boolean;\n  /**\n   * 弹窗底部样式\n   */\n  footerClass?: ClassType;\n  /**\n   * 是否显示顶栏\n   * @default true\n   */\n  header?: boolean;\n  /**\n   * 弹窗头部样式\n   */\n  headerClass?: ClassType;\n  /**\n   * 弹窗是否显示\n   * @default false\n   */\n  loading?: boolean;\n  /**\n   * 是否显示遮罩\n   * @default true\n   */\n  modal?: boolean;\n\n  /**\n   * 是否自动聚焦\n   */\n  openAutoFocus?: boolean;\n  /**\n   * 弹窗遮罩模糊效果\n   */\n  overlayBlur?: number;\n  /**\n   * 抽屉位置\n   * @default right\n   */\n  placement?: DrawerPlacement;\n\n  /**\n   * 是否显示取消按钮\n   * @default true\n   */\n  showCancelButton?: boolean;\n  /**\n   * 是否显示确认按钮\n   * @default true\n   */\n  showConfirmButton?: boolean;\n  /**\n   * 提交中（锁定抽屉状态）\n   */\n  submitting?: boolean;\n  /**\n   * 弹窗标题\n   */\n  title?: string;\n  /**\n   * 弹窗标题提示\n   */\n  titleTooltip?: string;\n  /**\n   * 抽屉层级\n   */\n  zIndex?: number;\n}\n\nexport interface DrawerState extends DrawerProps {\n  /** 弹窗打开状态 */\n  isOpen?: boolean;\n  /**\n   * 共享数据\n   */\n  sharedData?: Record<string, any>;\n}\n\nexport type ExtendedDrawerApi = DrawerApi & {\n  useStore: <T = NoInfer<DrawerState>>(\n    selector?: (state: NoInfer<DrawerState>) => T,\n  ) => Readonly<Ref<T>>;\n};\n\nexport interface DrawerApiOptions extends DrawerState {\n  /**\n   * 独立的抽屉组件\n   */\n  connectedComponent?: Component;\n  /**\n   * 关闭前的回调，返回 false 可以阻止关闭\n   * @returns\n   */\n  onBeforeClose?: () => MaybePromise<boolean | undefined>;\n  /**\n   * 点击取消按钮的回调\n   */\n  onCancel?: () => void;\n  /**\n   * 弹窗关闭动画结束的回调\n   * @returns\n   */\n  onClosed?: () => void;\n  /**\n   * 点击确定按钮的回调\n   */\n  onConfirm?: () => void;\n  /**\n   * 弹窗状态变化回调\n   * @param isOpen\n   * @returns\n   */\n  onOpenChange?: (isOpen: boolean) => void;\n  /**\n   * 弹窗打开动画结束的回调\n   * @returns\n   */\n  onOpened?: () => void;\n}\n"
  },
  {
    "path": "packages/@core/ui-kit/popup-ui/src/drawer/drawer.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { DrawerProps, ExtendedDrawerApi } from './drawer';\n\nimport {\n  computed,\n  onDeactivated,\n  provide,\n  ref,\n  unref,\n  useId,\n  watch,\n} from 'vue';\n\nimport {\n  useIsMobile,\n  usePriorityValues,\n  useSimpleLocale,\n} from '@vben-core/composables';\nimport { X } from '@vben-core/icons';\nimport {\n  Separator,\n  Sheet,\n  SheetClose,\n  SheetContent,\n  SheetDescription,\n  SheetFooter,\n  SheetHeader,\n  SheetTitle,\n  VbenButton,\n  VbenHelpTooltip,\n  VbenIconButton,\n  VbenLoading,\n  VisuallyHidden,\n} from '@vben-core/shadcn-ui';\nimport { ELEMENT_ID_MAIN_CONTENT } from '@vben-core/shared/constants';\nimport { globalShareState } from '@vben-core/shared/global-state';\nimport { cn } from '@vben-core/shared/utils';\n\ninterface Props extends DrawerProps {\n  drawerApi?: ExtendedDrawerApi;\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n  appendToMain: false,\n  closeIconPlacement: 'right',\n  destroyOnClose: false,\n  drawerApi: undefined,\n  submitting: false,\n  zIndex: 1000,\n});\n\nconst components = globalShareState.getComponents();\n\nconst id = useId();\nprovide('DISMISSABLE_DRAWER_ID', id);\n\nconst wrapperRef = ref<HTMLElement>();\nconst { $t } = useSimpleLocale();\nconst { isMobile } = useIsMobile();\n\nconst state = props.drawerApi?.useStore?.();\n\nconst {\n  appendToMain,\n  cancelText,\n  class: drawerClass,\n  closable,\n  closeIconPlacement,\n  closeOnClickModal,\n  closeOnPressEscape,\n  confirmLoading,\n  confirmText,\n  contentClass,\n  description,\n  destroyOnClose,\n  footer: showFooter,\n  footerClass,\n  header: showHeader,\n  headerClass,\n  loading: showLoading,\n  modal,\n  openAutoFocus,\n  overlayBlur,\n  placement,\n  showCancelButton,\n  showConfirmButton,\n  submitting,\n  title,\n  titleTooltip,\n  zIndex,\n} = usePriorityValues(props, state);\n\n// watch(\n//   () => showLoading.value,\n//   (v) => {\n//     if (v && wrapperRef.value) {\n//       wrapperRef.value.scrollTo({\n//         // behavior: 'smooth',\n//         top: 0,\n//       });\n//     }\n//   },\n// );\n\n/**\n * 在开启keepAlive情况下 直接通过浏览器按钮/手势等返回 不会关闭弹窗\n */\nonDeactivated(() => {\n  // 如果弹窗没有被挂载到内容区域，则关闭弹窗\n  if (!appendToMain.value) {\n    props.drawerApi?.close();\n  }\n});\n\nfunction interactOutside(e: Event) {\n  if (!closeOnClickModal.value || submitting.value) {\n    e.preventDefault();\n  }\n}\nfunction escapeKeyDown(e: KeyboardEvent) {\n  if (!closeOnPressEscape.value || submitting.value) {\n    e.preventDefault();\n  }\n}\n// pointer-down-outside\nfunction pointerDownOutside(e: Event) {\n  const target = e.target as HTMLElement;\n  const dismissableDrawer = target?.dataset.dismissableDrawer;\n  if (\n    submitting.value ||\n    !closeOnClickModal.value ||\n    dismissableDrawer !== id\n  ) {\n    e.preventDefault();\n  }\n}\n\nfunction handerOpenAutoFocus(e: Event) {\n  if (!openAutoFocus.value) {\n    e?.preventDefault();\n  }\n}\n\nfunction handleFocusOutside(e: Event) {\n  e.preventDefault();\n  e.stopPropagation();\n}\n\nconst getAppendTo = computed(() => {\n  return appendToMain.value\n    ? `#${ELEMENT_ID_MAIN_CONTENT}>div:not(.absolute)>div`\n    : undefined;\n});\n\n/**\n * destroyOnClose功能完善\n */\n// 是否打开过\nconst hasOpened = ref(false);\nconst isClosed = ref(true);\nwatch(\n  () => state?.value?.isOpen,\n  (value) => {\n    isClosed.value = false;\n    if (value && !unref(hasOpened)) {\n      hasOpened.value = true;\n    }\n  },\n);\nfunction handleClosed() {\n  isClosed.value = true;\n  props.drawerApi?.onClosed();\n}\nconst getForceMount = computed(() => {\n  return !unref(destroyOnClose) && unref(hasOpened);\n});\n</script>\n<template>\n  <Sheet\n    :modal=\"false\"\n    :open=\"state?.isOpen\"\n    @update:open=\"() => drawerApi?.close()\"\n  >\n    <SheetContent\n      :append-to=\"getAppendTo\"\n      :class=\"\n        cn('flex w-[520px] flex-col', drawerClass, {\n          '!w-full': isMobile || placement === 'bottom' || placement === 'top',\n          'max-h-[100vh]': placement === 'bottom' || placement === 'top',\n          hidden: isClosed,\n        })\n      \"\n      :modal=\"modal\"\n      :open=\"state?.isOpen\"\n      :side=\"placement\"\n      :z-index=\"zIndex\"\n      :force-mount=\"getForceMount\"\n      :overlay-blur=\"overlayBlur\"\n      @close-auto-focus=\"handleFocusOutside\"\n      @closed=\"handleClosed\"\n      @escape-key-down=\"escapeKeyDown\"\n      @focus-outside=\"handleFocusOutside\"\n      @interact-outside=\"interactOutside\"\n      @open-auto-focus=\"handerOpenAutoFocus\"\n      @opened=\"() => drawerApi?.onOpened()\"\n      @pointer-down-outside=\"pointerDownOutside\"\n    >\n      <SheetHeader\n        v-if=\"showHeader\"\n        :class=\"\n          cn(\n            '!flex flex-row items-center justify-between border-b px-6 py-5',\n            headerClass,\n            {\n              'px-4 py-3': closable,\n              'pl-2': closable && closeIconPlacement === 'left',\n            },\n          )\n        \"\n      >\n        <div class=\"flex items-center\">\n          <SheetClose\n            v-if=\"closable && closeIconPlacement === 'left'\"\n            as-child\n            :disabled=\"submitting\"\n            class=\"data-[state=open]:bg-secondary ml-[2px] cursor-pointer rounded-full opacity-80 transition-opacity hover:opacity-100 focus:outline-none disabled:pointer-events-none\"\n          >\n            <slot name=\"close-icon\">\n              <VbenIconButton>\n                <X class=\"size-4\" />\n              </VbenIconButton>\n            </slot>\n          </SheetClose>\n          <Separator\n            v-if=\"closable && closeIconPlacement === 'left'\"\n            class=\"ml-1 mr-2 h-8\"\n            decorative\n            orientation=\"vertical\"\n          />\n          <SheetTitle v-if=\"title\" class=\"text-left\">\n            <slot name=\"title\">\n              {{ title }}\n\n              <VbenHelpTooltip v-if=\"titleTooltip\" trigger-class=\"pb-1\">\n                {{ titleTooltip }}\n              </VbenHelpTooltip>\n            </slot>\n          </SheetTitle>\n          <SheetDescription v-if=\"description\" class=\"mt-1 text-xs\">\n            <slot name=\"description\">\n              {{ description }}\n            </slot>\n          </SheetDescription>\n        </div>\n\n        <VisuallyHidden v-if=\"!title || !description\">\n          <SheetTitle v-if=\"!title\" />\n          <SheetDescription v-if=\"!description\" />\n        </VisuallyHidden>\n\n        <div class=\"flex-center\">\n          <slot name=\"extra\"></slot>\n          <SheetClose\n            v-if=\"closable && closeIconPlacement === 'right'\"\n            as-child\n            :disabled=\"submitting\"\n            class=\"data-[state=open]:bg-secondary ml-[2px] cursor-pointer rounded-full opacity-80 transition-opacity hover:opacity-100 focus:outline-none disabled:pointer-events-none\"\n          >\n            <slot name=\"close-icon\">\n              <VbenIconButton>\n                <X class=\"size-4\" />\n              </VbenIconButton>\n            </slot>\n          </SheetClose>\n        </div>\n      </SheetHeader>\n      <template v-else>\n        <VisuallyHidden>\n          <SheetTitle />\n          <SheetDescription />\n        </VisuallyHidden>\n      </template>\n      <div\n        ref=\"wrapperRef\"\n        :class=\"\n          cn('relative flex-1 overflow-y-auto p-3', contentClass, {\n            'pointer-events-none': showLoading || submitting,\n          })\n        \"\n      >\n        <slot></slot>\n      </div>\n      <VbenLoading v-if=\"showLoading || submitting\" spinning />\n      <SheetFooter\n        v-if=\"showFooter\"\n        :class=\"\n          cn(\n            'w-full flex-row items-center justify-end border-t p-2 px-3',\n            footerClass,\n          )\n        \"\n      >\n        <slot name=\"prepend-footer\"></slot>\n        <slot name=\"footer\">\n          <component\n            :is=\"components.DefaultButton || VbenButton\"\n            v-if=\"showCancelButton\"\n            variant=\"ghost\"\n            :disabled=\"submitting\"\n            @click=\"() => drawerApi?.onCancel()\"\n          >\n            <slot name=\"cancelText\">\n              {{ cancelText || $t('cancel') }}\n            </slot>\n          </component>\n          <slot name=\"center-footer\"></slot>\n          <component\n            :is=\"components.PrimaryButton || VbenButton\"\n            v-if=\"showConfirmButton\"\n            :loading=\"confirmLoading || submitting\"\n            @click=\"() => drawerApi?.onConfirm()\"\n          >\n            <slot name=\"confirmText\">\n              {{ confirmText || $t('confirm') }}\n            </slot>\n          </component>\n        </slot>\n        <slot name=\"append-footer\"></slot>\n      </SheetFooter>\n    </SheetContent>\n  </Sheet>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/popup-ui/src/drawer/index.ts",
    "content": "export type * from './drawer';\nexport { default as VbenDrawer } from './drawer.vue';\nexport { setDefaultDrawerProps, useVbenDrawer } from './use-drawer';\n"
  },
  {
    "path": "packages/@core/ui-kit/popup-ui/src/drawer/use-drawer.ts",
    "content": "import type {\n  DrawerApiOptions,\n  DrawerProps,\n  ExtendedDrawerApi,\n} from './drawer';\n\nimport {\n  defineComponent,\n  h,\n  inject,\n  nextTick,\n  provide,\n  reactive,\n  ref,\n} from 'vue';\n\nimport { useStore } from '@vben-core/shared/store';\n\nimport { DrawerApi } from './drawer-api';\nimport VbenDrawer from './drawer.vue';\n\nconst USER_DRAWER_INJECT_KEY = Symbol('VBEN_DRAWER_INJECT');\n\nconst DEFAULT_DRAWER_PROPS: Partial<DrawerProps> = {};\n\nexport function setDefaultDrawerProps(props: Partial<DrawerProps>) {\n  Object.assign(DEFAULT_DRAWER_PROPS, props);\n}\n\nexport function useVbenDrawer<\n  TParentDrawerProps extends DrawerProps = DrawerProps,\n>(options: DrawerApiOptions = {}) {\n  // Drawer一般会抽离出来，所以如果有传入 connectedComponent，则表示为外部调用，与内部组件进行连接\n  // 外部的Drawer通过provide/inject传递api\n\n  const { connectedComponent } = options;\n  if (connectedComponent) {\n    const extendedApi = reactive({});\n    const isDrawerReady = ref(true);\n    const Drawer = defineComponent(\n      (props: TParentDrawerProps, { attrs, slots }) => {\n        provide(USER_DRAWER_INJECT_KEY, {\n          extendApi(api: ExtendedDrawerApi) {\n            // 不能直接给 reactive 赋值，会丢失响应\n            // 不能用 Object.assign,会丢失 api 的原型函数\n            Object.setPrototypeOf(extendedApi, api);\n          },\n          options,\n          async reCreateDrawer() {\n            isDrawerReady.value = false;\n            await nextTick();\n            isDrawerReady.value = true;\n          },\n        });\n        checkProps(extendedApi as ExtendedDrawerApi, {\n          ...props,\n          ...attrs,\n          ...slots,\n        });\n        return () =>\n          h(\n            isDrawerReady.value ? connectedComponent : 'div',\n            { ...props, ...attrs },\n            slots,\n          );\n      },\n      // eslint-disable-next-line vue/one-component-per-file\n      {\n        name: 'VbenParentDrawer',\n        inheritAttrs: false,\n      },\n    );\n\n    return [Drawer, extendedApi as ExtendedDrawerApi] as const;\n  }\n\n  const injectData = inject<any>(USER_DRAWER_INJECT_KEY, {});\n\n  const mergedOptions = {\n    ...DEFAULT_DRAWER_PROPS,\n    ...injectData.options,\n    ...options,\n  } as DrawerApiOptions;\n\n  mergedOptions.onOpenChange = (isOpen: boolean) => {\n    options.onOpenChange?.(isOpen);\n    injectData.options?.onOpenChange?.(isOpen);\n  };\n\n  const onClosed = mergedOptions.onClosed;\n  mergedOptions.onClosed = () => {\n    onClosed?.();\n    if (mergedOptions.destroyOnClose) {\n      injectData.reCreateDrawer?.();\n    }\n  };\n  const api = new DrawerApi(mergedOptions);\n\n  const extendedApi: ExtendedDrawerApi = api as never;\n\n  extendedApi.useStore = (selector) => {\n    return useStore(api.store, selector);\n  };\n\n  const Drawer = defineComponent(\n    (props: DrawerProps, { attrs, slots }) => {\n      return () =>\n        h(VbenDrawer, { ...props, ...attrs, drawerApi: extendedApi }, slots);\n    },\n    // eslint-disable-next-line vue/one-component-per-file\n    {\n      name: 'VbenDrawer',\n      inheritAttrs: false,\n    },\n  );\n  injectData.extendApi?.(extendedApi);\n  return [Drawer, extendedApi] as const;\n}\n\nasync function checkProps(api: ExtendedDrawerApi, attrs: Record<string, any>) {\n  if (!attrs || Object.keys(attrs).length === 0) {\n    return;\n  }\n  await nextTick();\n\n  const state = api?.store?.state;\n\n  if (!state) {\n    return;\n  }\n\n  const stateKeys = new Set(Object.keys(state));\n\n  for (const attr of Object.keys(attrs)) {\n    if (stateKeys.has(attr) && !['class'].includes(attr)) {\n      // connectedComponent存在时，不要传入Drawer的props，会造成复杂度提升，如果你需要修改Drawer的props，请使用 useVbenDrawer 或者api\n      console.warn(\n        `[Vben Drawer]: When 'connectedComponent' exists, do not set props or slots '${attr}', which will increase complexity. If you need to modify the props of Drawer, please use useVbenDrawer or api.`,\n      );\n    }\n  }\n}\n"
  },
  {
    "path": "packages/@core/ui-kit/popup-ui/src/index.ts",
    "content": "export * from './alert';\nexport * from './drawer';\nexport * from './modal';\n"
  },
  {
    "path": "packages/@core/ui-kit/popup-ui/src/modal/__tests__/modal-api.test.ts",
    "content": "import type { ModalState } from '../modal';\n\nimport { beforeEach, describe, expect, it, vi } from 'vitest';\n\nimport { ModalApi } from '../modal-api';\n\nvi.mock('@vben-core/shared/store', () => {\n  return {\n    isFunction: (fn: any) => typeof fn === 'function',\n    Store: class {\n      private _state: ModalState;\n      private options: any;\n\n      constructor(initialState: ModalState, options: any) {\n        this._state = initialState;\n        this.options = options;\n      }\n\n      batch(cb: () => void) {\n        cb();\n      }\n\n      setState(fn: (prev: ModalState) => ModalState) {\n        this._state = fn(this._state);\n        this.options.onUpdate();\n      }\n\n      get state() {\n        return this._state;\n      }\n    },\n  };\n});\n\ndescribe('modalApi', () => {\n  let modalApi: ModalApi;\n  // 使用 modalState 而不是 state\n  let modalState: ModalState;\n\n  beforeEach(() => {\n    modalApi = new ModalApi();\n    // 获取 modalApi 内的 state\n    modalState = modalApi.store.state;\n  });\n\n  it('should initialize with default state', () => {\n    expect(modalState.isOpen).toBe(false);\n    expect(modalState.cancelText).toBe(undefined);\n    expect(modalState.confirmText).toBe(undefined);\n  });\n\n  it('should open the modal', () => {\n    modalApi.open();\n    expect(modalApi.store.state.isOpen).toBe(true);\n  });\n\n  it('should close the modal if onBeforeClose allows it', () => {\n    modalApi.close();\n    expect(modalApi.store.state.isOpen).toBe(false);\n  });\n\n  it('should not close the modal if onBeforeClose returns false', () => {\n    const onBeforeClose = vi.fn(() => false);\n    const modalApiWithHook = new ModalApi({ onBeforeClose });\n    modalApiWithHook.open();\n    modalApiWithHook.close();\n    expect(modalApiWithHook.store.state.isOpen).toBe(true);\n    expect(onBeforeClose).toHaveBeenCalled();\n  });\n\n  it('should trigger onCancel and close the modal if no onCancel hook is provided', () => {\n    const onCancel = vi.fn();\n    const modalApiWithHook = new ModalApi({ onCancel });\n    modalApiWithHook.open();\n    modalApiWithHook.onCancel();\n    expect(onCancel).toHaveBeenCalled();\n    expect(modalApiWithHook.store.state.isOpen).toBe(true);\n  });\n\n  it('should update shared data correctly', () => {\n    const testData = { key: 'value' };\n    modalApi.setData(testData);\n    expect(modalApi.getData()).toEqual(testData);\n  });\n\n  it('should set state correctly using an object', () => {\n    modalApi.setState({ title: 'New Title' });\n    expect(modalApi.store.state.title).toBe('New Title');\n  });\n\n  it('should set state correctly using a function', () => {\n    modalApi.setState((prev) => ({ ...prev, confirmText: 'Yes' }));\n    expect(modalApi.store.state.confirmText).toBe('Yes');\n  });\n\n  it('should call onOpenChange when state changes', () => {\n    const onOpenChange = vi.fn();\n    const modalApiWithHook = new ModalApi({ onOpenChange });\n    modalApiWithHook.open();\n    expect(onOpenChange).toHaveBeenCalledWith(true);\n  });\n\n  it('should call onClosed callback when provided', () => {\n    const onClosed = vi.fn();\n    const modalApiWithHook = new ModalApi({ onClosed });\n    modalApiWithHook.onClosed();\n    expect(onClosed).toHaveBeenCalled();\n  });\n\n  it('should call onOpened callback when provided', () => {\n    const onOpened = vi.fn();\n    const modalApiWithHook = new ModalApi({ onOpened });\n    modalApiWithHook.open();\n    modalApiWithHook.onOpened();\n    expect(onOpened).toHaveBeenCalled();\n  });\n});\n"
  },
  {
    "path": "packages/@core/ui-kit/popup-ui/src/modal/index.ts",
    "content": "export type * from './modal';\nexport { default as VbenModal } from './modal.vue';\nexport { setDefaultModalProps, useVbenModal } from './use-modal';\n"
  },
  {
    "path": "packages/@core/ui-kit/popup-ui/src/modal/modal-api.ts",
    "content": "import type { ModalApiOptions, ModalState } from './modal';\n\nimport { Store } from '@vben-core/shared/store';\nimport { bindMethods, isFunction } from '@vben-core/shared/utils';\n\nexport class ModalApi {\n  // 共享数据\n  public sharedData: Record<'payload', any> = {\n    payload: {},\n  };\n  public store: Store<ModalState>;\n\n  private api: Pick<\n    ModalApiOptions,\n    | 'onBeforeClose'\n    | 'onCancel'\n    | 'onClosed'\n    | 'onConfirm'\n    | 'onOpenChange'\n    | 'onOpened'\n  >;\n\n  // private prevState!: ModalState;\n  private state!: ModalState;\n\n  constructor(options: ModalApiOptions = {}) {\n    const {\n      connectedComponent: _,\n      onBeforeClose,\n      onCancel,\n      onClosed,\n      onConfirm,\n      onOpenChange,\n      onOpened,\n      ...storeState\n    } = options;\n\n    const defaultState: ModalState = {\n      bordered: true,\n      centered: false,\n      class: '',\n      closeOnClickModal: true,\n      closeOnPressEscape: true,\n      confirmDisabled: false,\n      confirmLoading: false,\n      contentClass: '',\n      destroyOnClose: true,\n      draggable: false,\n      footer: true,\n      footerClass: '',\n      fullscreen: false,\n      fullscreenButton: true,\n      header: true,\n      headerClass: '',\n      isOpen: false,\n      loading: false,\n      modal: true,\n      openAutoFocus: false,\n      showCancelButton: true,\n      showConfirmButton: true,\n      title: '',\n      animationType: 'slide',\n    };\n\n    this.store = new Store<ModalState>(\n      {\n        ...defaultState,\n        ...storeState,\n      },\n      {\n        onUpdate: () => {\n          const state = this.store.state;\n\n          // 每次更新状态时，都会调用 onOpenChange 回调函数\n          if (state?.isOpen === this.state?.isOpen) {\n            this.state = state;\n          } else {\n            this.state = state;\n            this.api.onOpenChange?.(!!state?.isOpen);\n          }\n        },\n      },\n    );\n\n    this.state = this.store.state;\n\n    this.api = {\n      onBeforeClose,\n      onCancel,\n      onClosed,\n      onConfirm,\n      onOpenChange,\n      onOpened,\n    };\n    bindMethods(this);\n  }\n\n  /**\n   * 关闭弹窗\n   * @description 关闭弹窗时会调用 onBeforeClose 钩子函数，如果 onBeforeClose 返回 false，则不关闭弹窗\n   */\n  async close() {\n    // 通过 onBeforeClose 钩子函数来判断是否允许关闭弹窗\n    // 如果 onBeforeClose 返回 false，则不关闭弹窗\n    const allowClose = (await this.api.onBeforeClose?.()) ?? true;\n    if (allowClose) {\n      this.store.setState((prev) => ({\n        ...prev,\n        isOpen: false,\n      }));\n    }\n  }\n\n  getData<T extends object = Record<string, any>>() {\n    return (this.sharedData?.payload ?? {}) as T;\n  }\n\n  /**\n   * 锁定弹窗状态（用于提交过程中的等待状态）\n   * @description 锁定状态将禁用默认的取消按钮，使用spinner覆盖弹窗内容，隐藏关闭按钮，阻止手动关闭弹窗，将默认的提交按钮标记为loading状态\n   * @param isLocked 是否锁定\n   */\n  lock(isLocked = true) {\n    return this.setState({ submitting: isLocked });\n  }\n\n  /**\n   * loading和lock的区别\n   * loading允许关闭窗口\n   * lock不允许关闭窗口\n   * @param loading 是否loading\n   */\n  modalLoading(loading: boolean) {\n    this.setState({ confirmLoading: loading, loading });\n  }\n\n  /**\n   * 取消操作\n   */\n  onCancel() {\n    if (this.api.onCancel) {\n      this.api.onCancel?.();\n    } else {\n      this.close();\n    }\n  }\n\n  /**\n   * 弹窗关闭动画播放完毕后的回调\n   */\n  onClosed() {\n    if (!this.state.isOpen) {\n      this.api.onClosed?.();\n    }\n  }\n\n  /**\n   * 确认操作\n   */\n  onConfirm() {\n    this.api.onConfirm?.();\n  }\n\n  /**\n   * 弹窗打开动画播放完毕后的回调\n   */\n  onOpened() {\n    if (this.state.isOpen) {\n      this.api.onOpened?.();\n    }\n  }\n\n  open() {\n    this.store.setState((prev) => ({\n      ...prev,\n      isOpen: true,\n      submitting: false,\n    }));\n  }\n\n  setData<T>(payload: T) {\n    this.sharedData.payload = payload;\n    return this;\n  }\n\n  setState(\n    stateOrFn:\n      | ((prev: ModalState) => Partial<ModalState>)\n      | Partial<ModalState>,\n  ) {\n    if (isFunction(stateOrFn)) {\n      this.store.setState(stateOrFn);\n    } else {\n      this.store.setState((prev) => ({ ...prev, ...stateOrFn }));\n    }\n    return this;\n  }\n\n  /**\n   * 解除弹窗的锁定状态\n   * @description 解除由lock方法设置的锁定状态，是lock(false)的别名\n   */\n  unlock() {\n    return this.lock(false);\n  }\n}\n"
  },
  {
    "path": "packages/@core/ui-kit/popup-ui/src/modal/modal.ts",
    "content": "import type { Component, Ref } from 'vue';\n\nimport type { MaybePromise } from '@vben-core/typings';\n\nimport type { ModalApi } from './modal-api';\n\nexport interface ModalProps {\n  /**\n   * 动画类型\n   * @default 'slide'\n   */\n  animationType?: 'scale' | 'slide';\n  /**\n   * 是否要挂载到内容区域\n   * @default false\n   */\n  appendToMain?: boolean;\n  /**\n   * 是否显示边框\n   * @default false\n   */\n  bordered?: boolean;\n  /**\n   * 取消按钮文字\n   */\n  cancelText?: string;\n  /**\n   * 是否居中\n   * @default false\n   */\n  centered?: boolean;\n\n  class?: string;\n\n  /**\n   * 是否显示右上角的关闭按钮\n   * @default true\n   */\n  closable?: boolean;\n  /**\n   * 点击弹窗遮罩是否关闭弹窗\n   * @default true\n   */\n  closeOnClickModal?: boolean;\n  /**\n   * 按下 ESC 键是否关闭弹窗\n   * @default true\n   */\n  closeOnPressEscape?: boolean;\n  /**\n   * 禁用确认按钮\n   */\n  confirmDisabled?: boolean;\n  /**\n   * 确定按钮 loading\n   * @default false\n   */\n  confirmLoading?: boolean;\n  /**\n   * 确定按钮文字\n   */\n  confirmText?: string;\n  contentClass?: string;\n  /**\n   * 弹窗描述\n   */\n  description?: string;\n  /**\n   * 在关闭时销毁弹窗\n   */\n  destroyOnClose?: boolean;\n  /**\n   * 是否可拖拽\n   * @default false\n   */\n  draggable?: boolean;\n  /**\n   * 是否显示底部\n   * @default true\n   */\n  footer?: boolean;\n  footerClass?: string;\n  /**\n   * 是否全屏\n   * @default false\n   */\n  fullscreen?: boolean;\n  /**\n   * 是否显示全屏按钮\n   * @default true\n   */\n  fullscreenButton?: boolean;\n  /**\n   * 是否显示顶栏\n   * @default true\n   */\n  header?: boolean;\n  headerClass?: string;\n  /**\n   * 弹窗是否显示\n   * @default false\n   */\n  loading?: boolean;\n  /**\n   * 是否显示遮罩\n   * @default true\n   */\n  modal?: boolean;\n  /**\n   * 是否自动聚焦\n   */\n  openAutoFocus?: boolean;\n  /**\n   * 弹窗遮罩模糊效果\n   */\n  overlayBlur?: number;\n  /**\n   * 是否显示取消按钮\n   * @default true\n   */\n  showCancelButton?: boolean;\n  /**\n   * 是否显示确认按钮\n   * @default true\n   */\n  showConfirmButton?: boolean;\n  /**\n   * 提交中（锁定弹窗状态）\n   */\n  submitting?: boolean;\n  /**\n   * 弹窗标题\n   */\n  title?: string;\n  /**\n   * 弹窗标题提示\n   */\n  titleTooltip?: string;\n  /**\n   * 弹窗层级\n   */\n  zIndex?: number;\n}\n\nexport interface ModalState extends ModalProps {\n  /** 弹窗打开状态 */\n  isOpen?: boolean;\n  /**\n   * 共享数据\n   */\n  sharedData?: Record<string, any>;\n}\n\nexport type ExtendedModalApi = ModalApi & {\n  useStore: <T = NoInfer<ModalState>>(\n    selector?: (state: NoInfer<ModalState>) => T,\n  ) => Readonly<Ref<T>>;\n};\n\nexport interface ModalApiOptions extends ModalState {\n  /**\n   * 独立的弹窗组件\n   */\n  connectedComponent?: Component;\n  /**\n   * 关闭前的回调，返回 false 可以阻止关闭\n   * @returns\n   */\n  onBeforeClose?: () => MaybePromise<boolean | undefined>;\n  /**\n   * 点击取消按钮的回调\n   */\n  onCancel?: () => void;\n  /**\n   * 弹窗关闭动画结束的回调\n   * @returns\n   */\n  onClosed?: () => void;\n  /**\n   * 点击确定按钮的回调\n   */\n  onConfirm?: () => void;\n  /**\n   * 弹窗状态变化回调\n   * @param isOpen\n   * @returns\n   */\n  onOpenChange?: (isOpen: boolean) => void;\n  /**\n   * 弹窗打开动画结束的回调\n   * @returns\n   */\n  onOpened?: () => void;\n}\n"
  },
  {
    "path": "packages/@core/ui-kit/popup-ui/src/modal/modal.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { ExtendedModalApi, ModalProps } from './modal';\n\nimport {\n  computed,\n  nextTick,\n  onDeactivated,\n  provide,\n  ref,\n  unref,\n  useId,\n  watch,\n} from 'vue';\n\nimport {\n  useIsMobile,\n  usePriorityValues,\n  useSimpleLocale,\n} from '@vben-core/composables';\nimport { Expand, Shrink } from '@vben-core/icons';\nimport {\n  Dialog,\n  DialogContent,\n  DialogDescription,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n  VbenButton,\n  VbenHelpTooltip,\n  VbenIconButton,\n  VbenLoading,\n  VisuallyHidden,\n} from '@vben-core/shadcn-ui';\nimport { ELEMENT_ID_MAIN_CONTENT } from '@vben-core/shared/constants';\nimport { globalShareState } from '@vben-core/shared/global-state';\nimport { cn } from '@vben-core/shared/utils';\n\nimport { useModalDraggable } from './use-modal-draggable';\n\ninterface Props extends ModalProps {\n  modalApi?: ExtendedModalApi;\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n  appendToMain: false,\n  destroyOnClose: false,\n  modalApi: undefined,\n});\n\nconst components = globalShareState.getComponents();\n\nconst contentRef = ref();\nconst wrapperRef = ref<HTMLElement>();\nconst dialogRef = ref();\nconst headerRef = ref();\nconst footerRef = ref();\n\nconst id = useId();\n\nprovide('DISMISSABLE_MODAL_ID', id);\n\nconst { $t } = useSimpleLocale();\nconst { isMobile } = useIsMobile();\nconst state = props.modalApi?.useStore?.();\n\nconst {\n  appendToMain,\n  bordered,\n  cancelText,\n  centered,\n  class: modalClass,\n  closable,\n  closeOnClickModal,\n  closeOnPressEscape,\n  confirmDisabled,\n  confirmLoading,\n  confirmText,\n  contentClass,\n  description,\n  destroyOnClose,\n  draggable,\n  footer: showFooter,\n  footerClass,\n  fullscreen,\n  fullscreenButton,\n  header,\n  headerClass,\n  loading: showLoading,\n  modal,\n  openAutoFocus,\n  overlayBlur,\n  showCancelButton,\n  showConfirmButton,\n  submitting,\n  title,\n  titleTooltip,\n  animationType,\n  zIndex,\n} = usePriorityValues(props, state);\n\nconst shouldFullscreen = computed(() => fullscreen.value || isMobile.value);\n\nconst shouldDraggable = computed(\n  () => draggable.value && !shouldFullscreen.value && header.value,\n);\n\nconst getAppendTo = computed(() => {\n  return appendToMain.value\n    ? `#${ELEMENT_ID_MAIN_CONTENT}>div:not(.absolute)>div`\n    : undefined;\n});\n\nconst { dragging, transform } = useModalDraggable(\n  dialogRef,\n  headerRef,\n  shouldDraggable,\n  getAppendTo,\n);\n\nconst firstOpened = ref(false);\nconst isClosed = ref(true);\n\nwatch(\n  () => state?.value?.isOpen,\n  async (v) => {\n    if (v) {\n      isClosed.value = false;\n      if (!firstOpened.value) firstOpened.value = true;\n      await nextTick();\n      if (!contentRef.value) return;\n      const innerContentRef = contentRef.value.getContentRef();\n      dialogRef.value = innerContentRef.$el;\n      // reopen modal reassign value\n      const { offsetX, offsetY } = transform;\n      dialogRef.value.style.transform = `translate(${offsetX}px, ${offsetY}px)`;\n    }\n  },\n  { immediate: true },\n);\n\n// watch(\n//   () => [showLoading.value, submitting.value],\n//   ([l, s]) => {\n//     if ((s || l) && wrapperRef.value) {\n//       wrapperRef.value.scrollTo({\n//         // behavior: 'smooth',\n//         top: 0,\n//       });\n//     }\n//   },\n// );\n\n/**\n * 在开启keepAlive情况下 直接通过浏览器按钮/手势等返回 不会关闭弹窗\n */\nonDeactivated(() => {\n  // 如果弹窗没有被挂载到内容区域，则关闭弹窗\n  if (!appendToMain.value) {\n    props.modalApi?.close();\n  }\n});\n\nfunction handleFullscreen() {\n  props.modalApi?.setState((prev) => {\n    // if (prev.fullscreen) {\n    //   resetPosition();\n    // }\n    return { ...prev, fullscreen: !fullscreen.value };\n  });\n}\nfunction interactOutside(e: Event) {\n  if (!closeOnClickModal.value || submitting.value) {\n    e.preventDefault();\n    e.stopPropagation();\n  }\n}\nfunction escapeKeyDown(e: KeyboardEvent) {\n  if (!closeOnPressEscape.value || submitting.value) {\n    e.preventDefault();\n  }\n}\n\nfunction handerOpenAutoFocus(e: Event) {\n  if (!openAutoFocus.value) {\n    e?.preventDefault();\n  }\n}\n\n// pointer-down-outside\nfunction pointerDownOutside(e: Event) {\n  const target = e.target as HTMLElement;\n  const isDismissableModal = target?.dataset.dismissableModal;\n  if (\n    !closeOnClickModal.value ||\n    isDismissableModal !== id ||\n    submitting.value\n  ) {\n    e.preventDefault();\n    e.stopPropagation();\n  }\n}\n\nfunction handleFocusOutside(e: Event) {\n  e.preventDefault();\n  e.stopPropagation();\n}\n\nconst getForceMount = computed(() => {\n  return !unref(destroyOnClose) && unref(firstOpened);\n});\n\nfunction handleClosed() {\n  isClosed.value = true;\n  props.modalApi?.onClosed();\n}\n</script>\n<template>\n  <Dialog\n    :modal=\"false\"\n    :open=\"state?.isOpen\"\n    @update:open=\"() => (!submitting ? modalApi?.close() : undefined)\"\n  >\n    <DialogContent\n      ref=\"contentRef\"\n      :append-to=\"getAppendTo\"\n      :class=\"\n        cn(\n          'left-0 right-0 top-[10vh] mx-auto flex max-h-[80%] w-[520px] flex-col p-0',\n          shouldFullscreen ? 'sm:rounded-none' : 'sm:rounded-[var(--radius)]',\n          modalClass,\n          {\n            'border-border border': bordered,\n            'shadow-3xl': !bordered,\n            'left-0 top-0 size-full max-h-full !translate-x-0 !translate-y-0':\n              shouldFullscreen,\n            'top-1/2 !-translate-y-1/2': centered && !shouldFullscreen,\n            'duration-300': !dragging,\n            hidden: isClosed,\n          },\n        )\n      \"\n      :force-mount=\"getForceMount\"\n      :modal=\"modal\"\n      :open=\"state?.isOpen\"\n      :show-close=\"closable\"\n      :animation-type=\"animationType\"\n      :z-index=\"zIndex\"\n      :overlay-blur=\"overlayBlur\"\n      close-class=\"top-3\"\n      @close-auto-focus=\"handleFocusOutside\"\n      @closed=\"handleClosed\"\n      :close-disabled=\"submitting\"\n      @escape-key-down=\"escapeKeyDown\"\n      @focus-outside=\"handleFocusOutside\"\n      @interact-outside=\"interactOutside\"\n      @open-auto-focus=\"handerOpenAutoFocus\"\n      @opened=\"() => modalApi?.onOpened()\"\n      @pointer-down-outside=\"pointerDownOutside\"\n    >\n      <DialogHeader\n        ref=\"headerRef\"\n        :class=\"\n          cn(\n            'px-5 py-4',\n            {\n              'border-b': bordered,\n              hidden: !header,\n              'cursor-move select-none': shouldDraggable,\n            },\n            headerClass,\n          )\n        \"\n      >\n        <DialogTitle v-if=\"title\" class=\"text-left\">\n          <slot name=\"title\">\n            {{ title }}\n\n            <slot v-if=\"titleTooltip\" name=\"titleTooltip\">\n              <VbenHelpTooltip trigger-class=\"pb-1\">\n                {{ titleTooltip }}\n              </VbenHelpTooltip>\n            </slot>\n          </slot>\n        </DialogTitle>\n        <DialogDescription v-if=\"description\">\n          <slot name=\"description\">\n            {{ description }}\n          </slot>\n        </DialogDescription>\n        <VisuallyHidden v-if=\"!title || !description\">\n          <DialogTitle v-if=\"!title\" />\n          <DialogDescription v-if=\"!description\" />\n        </VisuallyHidden>\n      </DialogHeader>\n      <div\n        ref=\"wrapperRef\"\n        :class=\"\n          cn('relative min-h-40 flex-1 overflow-y-auto p-3', contentClass, {\n            'pointer-events-none': showLoading || submitting,\n          })\n        \"\n      >\n        <slot></slot>\n      </div>\n      <VbenLoading v-if=\"showLoading || submitting\" spinning />\n      <VbenIconButton\n        v-if=\"fullscreenButton\"\n        class=\"hover:bg-accent hover:text-accent-foreground text-foreground/80 flex-center absolute right-10 top-3 hidden size-6 rounded-full px-1 text-lg opacity-70 transition-opacity hover:opacity-100 focus:outline-none disabled:pointer-events-none sm:block\"\n        @click=\"handleFullscreen\"\n      >\n        <Shrink v-if=\"fullscreen\" class=\"size-3.5\" />\n        <Expand v-else class=\"size-3.5\" />\n      </VbenIconButton>\n\n      <DialogFooter\n        v-if=\"showFooter\"\n        ref=\"footerRef\"\n        :class=\"\n          cn(\n            'flex-row items-center justify-end p-2',\n            {\n              'border-t': bordered,\n            },\n            footerClass,\n          )\n        \"\n      >\n        <slot name=\"prepend-footer\"></slot>\n        <slot name=\"footer\">\n          <component\n            :is=\"components.DefaultButton || VbenButton\"\n            v-if=\"showCancelButton\"\n            variant=\"ghost\"\n            :disabled=\"submitting\"\n            @click=\"() => modalApi?.onCancel()\"\n          >\n            <slot name=\"cancelText\">\n              {{ cancelText || $t('cancel') }}\n            </slot>\n          </component>\n          <slot name=\"center-footer\"></slot>\n          <component\n            :is=\"components.PrimaryButton || VbenButton\"\n            v-if=\"showConfirmButton\"\n            :disabled=\"confirmDisabled\"\n            :loading=\"confirmLoading || submitting\"\n            @click=\"() => modalApi?.onConfirm()\"\n          >\n            <slot name=\"confirmText\">\n              {{ confirmText || $t('confirm') }}\n            </slot>\n          </component>\n        </slot>\n        <slot name=\"append-footer\"></slot>\n      </DialogFooter>\n    </DialogContent>\n  </Dialog>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/popup-ui/src/modal/use-modal-draggable.ts",
    "content": "/**\n * @copy https://github.com/element-plus/element-plus/blob/dev/packages/hooks/use-draggable/index.ts\n * 调整部分细节\n */\n\nimport type { ComputedRef, Ref } from 'vue';\n\nimport { unrefElement } from '@vueuse/core';\nimport { onBeforeUnmount, onMounted, reactive, ref, watchEffect } from 'vue';\n\nexport function useModalDraggable(\n  targetRef: Ref<HTMLElement | undefined>,\n  dragRef: Ref<HTMLElement | undefined>,\n  draggable: ComputedRef<boolean>,\n  containerSelector?: ComputedRef<string | undefined>,\n) {\n  const transform = reactive({\n    offsetX: 0,\n    offsetY: 0,\n  });\n\n  const dragging = ref(false);\n\n  const onMousedown = (e: MouseEvent) => {\n    const downX = e.clientX;\n    const downY = e.clientY;\n\n    if (!targetRef.value) {\n      return;\n    }\n\n    const targetRect = targetRef.value.getBoundingClientRect();\n    const { offsetX, offsetY } = transform;\n    const targetLeft = targetRect.left;\n    const targetTop = targetRect.top;\n    const targetWidth = targetRect.width;\n    const targetHeight = targetRect.height;\n\n    let containerRect: DOMRect | null = null;\n\n    if (containerSelector?.value) {\n      const container = document.querySelector(containerSelector.value);\n      if (container) {\n        containerRect = container.getBoundingClientRect();\n      }\n    }\n\n    let maxLeft, maxTop, minLeft, minTop;\n    if (containerRect) {\n      minLeft = containerRect.left - targetLeft + offsetX;\n      maxLeft = containerRect.right - targetLeft - targetWidth + offsetX;\n      minTop = containerRect.top - targetTop + offsetY;\n      maxTop = containerRect.bottom - targetTop - targetHeight + offsetY;\n    } else {\n      const docElement = document.documentElement;\n      const clientWidth = docElement.clientWidth;\n      const clientHeight = docElement.clientHeight;\n      minLeft = -targetLeft + offsetX;\n      minTop = -targetTop + offsetY;\n      maxLeft = clientWidth - targetLeft - targetWidth + offsetX;\n      maxTop = clientHeight - targetTop - targetHeight + offsetY;\n    }\n\n    const onMousemove = (e: MouseEvent) => {\n      let moveX = offsetX + e.clientX - downX;\n      let moveY = offsetY + e.clientY - downY;\n\n      moveX = Math.min(Math.max(moveX, minLeft), maxLeft);\n      moveY = Math.min(Math.max(moveY, minTop), maxTop);\n\n      transform.offsetX = moveX;\n      transform.offsetY = moveY;\n\n      if (targetRef.value) {\n        targetRef.value.style.transform = `translate(${moveX}px, ${moveY}px)`;\n        dragging.value = true;\n      }\n    };\n\n    const onMouseup = () => {\n      dragging.value = false;\n      document.removeEventListener('mousemove', onMousemove);\n      document.removeEventListener('mouseup', onMouseup);\n    };\n\n    document.addEventListener('mousemove', onMousemove);\n    document.addEventListener('mouseup', onMouseup);\n  };\n\n  const onDraggable = () => {\n    const dragDom = unrefElement(dragRef);\n    if (dragDom && targetRef.value) {\n      dragDom.addEventListener('mousedown', onMousedown);\n    }\n  };\n\n  const offDraggable = () => {\n    const dragDom = unrefElement(dragRef);\n    if (dragDom && targetRef.value) {\n      dragDom.removeEventListener('mousedown', onMousedown);\n    }\n  };\n\n  const resetPosition = () => {\n    transform.offsetX = 0;\n    transform.offsetY = 0;\n\n    const target = unrefElement(targetRef);\n    if (target) {\n      target.style.transform = 'none';\n    }\n  };\n\n  onMounted(() => {\n    watchEffect(() => {\n      if (draggable.value) {\n        onDraggable();\n      } else {\n        offDraggable();\n      }\n    });\n  });\n\n  onBeforeUnmount(() => {\n    offDraggable();\n  });\n\n  return {\n    dragging,\n    resetPosition,\n    transform,\n  };\n}\n"
  },
  {
    "path": "packages/@core/ui-kit/popup-ui/src/modal/use-modal.ts",
    "content": "import type { ExtendedModalApi, ModalApiOptions, ModalProps } from './modal';\n\nimport {\n  defineComponent,\n  h,\n  inject,\n  nextTick,\n  provide,\n  reactive,\n  ref,\n} from 'vue';\n\nimport { useStore } from '@vben-core/shared/store';\n\nimport { ModalApi } from './modal-api';\nimport VbenModal from './modal.vue';\n\nconst USER_MODAL_INJECT_KEY = Symbol('VBEN_MODAL_INJECT');\n\nconst DEFAULT_MODAL_PROPS: Partial<ModalProps> = {};\n\nexport function setDefaultModalProps(props: Partial<ModalProps>) {\n  Object.assign(DEFAULT_MODAL_PROPS, props);\n}\n\nexport function useVbenModal<TParentModalProps extends ModalProps = ModalProps>(\n  options: ModalApiOptions = {},\n) {\n  // Modal一般会抽离出来，所以如果有传入 connectedComponent，则表示为外部调用，与内部组件进行连接\n  // 外部的Modal通过provide/inject传递api\n\n  const { connectedComponent } = options;\n  if (connectedComponent) {\n    const extendedApi = reactive({});\n    const isModalReady = ref(true);\n    const Modal = defineComponent(\n      (props: TParentModalProps, { attrs, slots }) => {\n        provide(USER_MODAL_INJECT_KEY, {\n          extendApi(api: ExtendedModalApi) {\n            // 不能直接给 reactive 赋值，会丢失响应\n            // 不能用 Object.assign,会丢失 api 的原型函数\n            Object.setPrototypeOf(extendedApi, api);\n          },\n          options,\n          async reCreateModal() {\n            isModalReady.value = false;\n            await nextTick();\n            isModalReady.value = true;\n          },\n        });\n        checkProps(extendedApi as ExtendedModalApi, {\n          ...props,\n          ...attrs,\n          ...slots,\n        });\n        return () =>\n          h(\n            isModalReady.value ? connectedComponent : 'div',\n            {\n              ...props,\n              ...attrs,\n            },\n            slots,\n          );\n      },\n      // eslint-disable-next-line vue/one-component-per-file\n      {\n        name: 'VbenParentModal',\n        inheritAttrs: false,\n      },\n    );\n\n    return [Modal, extendedApi as ExtendedModalApi] as const;\n  }\n\n  const injectData = inject<any>(USER_MODAL_INJECT_KEY, {});\n\n  const mergedOptions = {\n    ...DEFAULT_MODAL_PROPS,\n    ...injectData.options,\n    ...options,\n  } as ModalApiOptions;\n\n  mergedOptions.onOpenChange = (isOpen: boolean) => {\n    options.onOpenChange?.(isOpen);\n    injectData.options?.onOpenChange?.(isOpen);\n  };\n\n  const onClosed = mergedOptions.onClosed;\n  mergedOptions.onClosed = () => {\n    onClosed?.();\n    if (mergedOptions.destroyOnClose) {\n      injectData.reCreateModal?.();\n    }\n  };\n\n  const api = new ModalApi(mergedOptions);\n\n  const extendedApi: ExtendedModalApi = api as never;\n\n  extendedApi.useStore = (selector) => {\n    return useStore(api.store, selector);\n  };\n\n  const Modal = defineComponent(\n    (props: ModalProps, { attrs, slots }) => {\n      return () =>\n        h(\n          VbenModal,\n          {\n            ...props,\n            ...attrs,\n            modalApi: extendedApi,\n          },\n          slots,\n        );\n    },\n    // eslint-disable-next-line vue/one-component-per-file\n    {\n      name: 'VbenModal',\n      inheritAttrs: false,\n    },\n  );\n  injectData.extendApi?.(extendedApi);\n\n  return [Modal, extendedApi] as const;\n}\n\nasync function checkProps(api: ExtendedModalApi, attrs: Record<string, any>) {\n  if (!attrs || Object.keys(attrs).length === 0) {\n    return;\n  }\n  await nextTick();\n\n  const state = api?.store?.state;\n\n  if (!state) {\n    return;\n  }\n\n  const stateKeys = new Set(Object.keys(state));\n\n  for (const attr of Object.keys(attrs)) {\n    if (stateKeys.has(attr) && !['class'].includes(attr)) {\n      // connectedComponent存在时，不要传入Modal的props，会造成复杂度提升，如果你需要修改Modal的props，请使用 useModal 或者api\n      console.warn(\n        `[Vben Modal]: When 'connectedComponent' exists, do not set props or slots '${attr}', which will increase complexity. If you need to modify the props of Modal, please use useVbenModal or api.`,\n      );\n    }\n  }\n}\n"
  },
  {
    "path": "packages/@core/ui-kit/popup-ui/tailwind.config.mjs",
    "content": "export { default } from '@vben/tailwind-config';\n"
  },
  {
    "path": "packages/@core/ui-kit/popup-ui/tsconfig.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/tsconfig\",\n  \"extends\": \"@vben/tsconfig/web.json\",\n  \"include\": [\"src\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/build.config.ts",
    "content": "import { defineBuildConfig } from 'unbuild';\n\nexport default defineBuildConfig({\n  clean: true,\n  declaration: true,\n  entries: [\n    {\n      builder: 'mkdist',\n      input: './src',\n\n      pattern: ['**/*'],\n    },\n    {\n      builder: 'mkdist',\n      input: './src',\n      loaders: ['vue'],\n      pattern: ['**/*.vue'],\n    },\n    {\n      builder: 'mkdist',\n      format: 'esm',\n      input: './src',\n      loaders: ['js'],\n      pattern: ['**/*.ts'],\n    },\n  ],\n});\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/components.json",
    "content": "{\n  \"$schema\": \"https://shadcn-vue.com/schema.json\",\n  \"style\": \"new-york\",\n  \"typescript\": true,\n  \"tailwind\": {\n    \"config\": \"tailwind.config.mjs\",\n    \"css\": \"src/assets/index.css\",\n    \"baseColor\": \"slate\",\n    \"cssVariables\": true\n  },\n  \"framework\": \"vite\",\n  \"aliases\": {\n    \"components\": \"@vben-core/shadcn-ui/components\",\n    \"utils\": \"@vben-core/shared/utils\"\n  }\n}\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/package.json",
    "content": "{\n  \"name\": \"@vben-core/shadcn-ui\",\n  \"version\": \"5.5.9\",\n  \"#main\": \"./dist/index.mjs\",\n  \"#module\": \"./dist/index.mjs\",\n  \"homepage\": \"https://github.com/vbenjs/vue-vben-admin\",\n  \"bugs\": \"https://github.com/vbenjs/vue-vben-admin/issues\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/vbenjs/vue-vben-admin.git\",\n    \"directory\": \"packages/@vben-core/uikit/shadcn-ui\"\n  },\n  \"license\": \"MIT\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"#build\": \"pnpm unbuild\",\n    \"#prepublishOnly\": \"npm run build\"\n  },\n  \"files\": [\n    \"dist\"\n  ],\n  \"sideEffects\": [\n    \"**/*.css\"\n  ],\n  \"main\": \"./src/index.ts\",\n  \"module\": \"./src/index.ts\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./src/index.ts\",\n      \"development\": \"./src/index.ts\",\n      \"default\": \"./src/index.ts\",\n      \"//default\": \"./dist/index.mjs\"\n    }\n  },\n  \"publishConfig\": {\n    \"exports\": {\n      \".\": {\n        \"default\": \"./src/index.ts\"\n      }\n    }\n  },\n  \"dependencies\": {\n    \"@vben-core/composables\": \"workspace:*\",\n    \"@vben-core/icons\": \"workspace:*\",\n    \"@vben-core/shared\": \"workspace:*\",\n    \"@vben-core/typings\": \"workspace:*\",\n    \"@vueuse/core\": \"catalog:\",\n    \"class-variance-authority\": \"catalog:\",\n    \"lucide-vue-next\": \"catalog:\",\n    \"radix-vue\": \"catalog:\",\n    \"vee-validate\": \"catalog:\",\n    \"vue\": \"catalog:\"\n  }\n}\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/postcss.config.mjs",
    "content": "export { default } from '@vben/tailwind-config/postcss';\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/components/avatar/avatar.vue",
    "content": "<script setup lang=\"ts\">\nimport type {\n  AvatarFallbackProps,\n  AvatarImageProps,\n  AvatarRootProps,\n} from 'radix-vue';\n\nimport type { CSSProperties } from 'vue';\n\nimport type { ClassType } from '@vben-core/typings';\n\nimport { computed } from 'vue';\n\nimport { Avatar, AvatarFallback, AvatarImage } from '../../ui';\n\ninterface Props extends AvatarFallbackProps, AvatarImageProps, AvatarRootProps {\n  alt?: string;\n  class?: ClassType;\n  dot?: boolean;\n  dotClass?: ClassType;\n  fit?: 'contain' | 'cover' | 'fill' | 'none' | 'scale-down';\n  size?: number;\n}\n\ndefineOptions({\n  inheritAttrs: false,\n});\n\nconst props = withDefaults(defineProps<Props>(), {\n  alt: 'avatar',\n  as: 'button',\n  dot: false,\n  dotClass: 'bg-green-500',\n  fit: 'cover',\n});\n\nconst imageStyle = computed<CSSProperties>(() => {\n  const { fit } = props;\n  if (fit) {\n    return { objectFit: fit };\n  }\n  return {};\n});\n\nconst text = computed(() => {\n  return props.alt.slice(-2).toUpperCase();\n});\n\nconst rootStyle = computed(() => {\n  return props.size !== undefined && props.size > 0\n    ? {\n        height: `${props.size}px`,\n        width: `${props.size}px`,\n      }\n    : {};\n});\n</script>\n\n<template>\n  <div\n    :class=\"props.class\"\n    :style=\"rootStyle\"\n    class=\"relative flex flex-shrink-0 items-center\"\n  >\n    <Avatar :class=\"props.class\" class=\"size-full\">\n      <AvatarImage :alt=\"alt\" :src=\"src\" :style=\"imageStyle\" />\n      <AvatarFallback>{{ text }}</AvatarFallback>\n    </Avatar>\n    <span\n      v-if=\"dot\"\n      :class=\"dotClass\"\n      class=\"border-background absolute bottom-0 right-0 size-3 rounded-full border-2\"\n    >\n    </span>\n  </div>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/components/avatar/index.ts",
    "content": "export { default as VbenAvatar } from './avatar.vue';\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/components/back-top/back-top.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { BacktopProps } from './backtop';\n\nimport { computed } from 'vue';\n\nimport { ArrowUpToLine } from '@vben-core/icons';\n\nimport { VbenButton } from '../button';\nimport { useBackTop } from './use-backtop';\n\ninterface Props extends BacktopProps {}\n\ndefineOptions({ name: 'BackTop' });\n\nconst props = withDefaults(defineProps<Props>(), {\n  bottom: 20,\n  isGroup: false,\n  right: 24,\n  target: '',\n  visibilityHeight: 200,\n});\n\nconst backTopStyle = computed(() => ({\n  bottom: `${props.bottom}px`,\n  right: `${props.right}px`,\n}));\n\nconst { handleClick, visible } = useBackTop(props);\n</script>\n<template>\n  <transition name=\"fade-down\">\n    <VbenButton\n      v-if=\"visible\"\n      :style=\"backTopStyle\"\n      class=\"dark:bg-accent dark:hover:bg-heavy bg-background hover:bg-heavy data shadow-float z-popup fixed bottom-10 size-10 rounded-full duration-500\"\n      size=\"icon\"\n      variant=\"icon\"\n      @click=\"handleClick\"\n    >\n      <ArrowUpToLine class=\"size-4\" />\n    </VbenButton>\n  </transition>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/components/back-top/backtop.ts",
    "content": "export const backtopProps = {\n  /**\n   * @zh_CN bottom distance.\n   */\n  bottom: {\n    default: 40,\n    type: Number,\n  },\n  /**\n   * @zh_CN right distance.\n   */\n  right: {\n    default: 40,\n    type: Number,\n  },\n  /**\n   * @zh_CN the target to trigger scroll.\n   */\n  target: {\n    default: '',\n    type: String,\n  },\n  /**\n   * @zh_CN the button will not show until the scroll height reaches this value.\n   */\n  visibilityHeight: {\n    default: 200,\n    type: Number,\n  },\n} as const;\n\nexport interface BacktopProps {\n  bottom?: number;\n  isGroup?: boolean;\n  right?: number;\n  target?: string;\n  visibilityHeight?: number;\n}\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/components/back-top/index.ts",
    "content": "export { default as VbenBackTop } from './back-top.vue';\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/components/back-top/use-backtop.ts",
    "content": "import type { BacktopProps } from './backtop';\n\nimport { onMounted, ref, shallowRef } from 'vue';\n\nimport { useEventListener, useThrottleFn } from '@vueuse/core';\n\nexport const useBackTop = (props: BacktopProps) => {\n  const el = shallowRef<HTMLElement>();\n  const container = shallowRef<Document | HTMLElement>();\n  const visible = ref(false);\n\n  const handleScroll = () => {\n    if (el.value) {\n      visible.value = el.value.scrollTop >= (props?.visibilityHeight ?? 0);\n    }\n  };\n\n  const handleClick = () => {\n    el.value?.scrollTo({ behavior: 'smooth', top: 0 });\n  };\n\n  const handleScrollThrottled = useThrottleFn(handleScroll, 300, true);\n\n  useEventListener(container, 'scroll', handleScrollThrottled);\n  onMounted(() => {\n    container.value = document;\n    el.value = document.documentElement;\n\n    if (props.target) {\n      el.value = document.querySelector<HTMLElement>(props.target) ?? undefined;\n\n      if (!el.value) {\n        throw new Error(`target does not exist: ${props.target}`);\n      }\n      container.value = el.value;\n    }\n    // Give visible an initial value, fix #13066\n    handleScroll();\n  });\n\n  return {\n    handleClick,\n    visible,\n  };\n};\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/components/breadcrumb/breadcrumb-background.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { BreadcrumbProps } from './types';\n\nimport { VbenIcon } from '../icon';\n\ninterface Props extends BreadcrumbProps {}\n\ndefineOptions({ name: 'Breadcrumb' });\nconst { breadcrumbs, showIcon } = defineProps<Props>();\n\nconst emit = defineEmits<{ select: [string] }>();\n\nfunction handleClick(index: number, path?: string) {\n  if (!path || index === breadcrumbs.length - 1) {\n    return;\n  }\n  emit('select', path);\n}\n</script>\n<template>\n  <ul class=\"flex\">\n    <TransitionGroup name=\"breadcrumb-transition\">\n      <template\n        v-for=\"(item, index) in breadcrumbs\"\n        :key=\"`${item.path}-${item.title}-${index}`\"\n      >\n        <li>\n          <a\n            href=\"javascript:void 0\"\n            @click.stop=\"handleClick(index, item.path)\"\n          >\n            <span class=\"flex-center z-10 h-full\">\n              <VbenIcon\n                v-if=\"showIcon\"\n                :icon=\"item.icon\"\n                class=\"mr-1 size-4 flex-shrink-0\"\n              />\n              <span\n                :class=\"{\n                  'text-foreground font-normal':\n                    index === breadcrumbs.length - 1,\n                }\"\n                >{{ item.title }}\n              </span>\n            </span>\n          </a>\n        </li>\n      </template>\n    </TransitionGroup>\n  </ul>\n</template>\n<style scoped>\nli {\n  @apply h-7;\n}\n\nli a {\n  @apply text-muted-foreground bg-accent relative mr-9 flex h-7 items-center py-0 pl-[5px] pr-2 text-[13px];\n}\n\nli a > span {\n  @apply -ml-3;\n}\n\nli:first-child a > span {\n  @apply -ml-1;\n}\n\nli:first-child a {\n  @apply rounded-[4px_0_0_4px] pl-[15px];\n}\n\nli:first-child a::before {\n  @apply border-none;\n}\n\nli:last-child a {\n  @apply rounded-[0_4px_4px_0] pr-[15px];\n}\n\nli:last-child a::after {\n  @apply border-none;\n}\n\nli a::before,\nli a::after {\n  @apply border-accent absolute top-0 h-0 w-0 border-[.875rem] border-solid content-[''];\n}\n\nli a::before {\n  @apply -left-7 z-10 border-l-transparent;\n}\n\nli a::after {\n  @apply border-l-accent left-full border-transparent;\n}\n\nli:not(:last-child) a:hover {\n  @apply bg-accent-hover;\n}\n\nli:not(:last-child) a:hover::before {\n  @apply border-accent-hover border-l-transparent;\n}\n\nli:not(:last-child) a:hover::after {\n  @apply border-l-accent-hover;\n}\n</style>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/components/breadcrumb/breadcrumb-view.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { BreadcrumbProps } from './types';\n\nimport { useForwardPropsEmits } from 'radix-vue';\n\nimport BreadcrumbBackground from './breadcrumb-background.vue';\nimport Breadcrumb from './breadcrumb.vue';\n\ninterface Props extends BreadcrumbProps {\n  class?: any;\n}\n\nconst props = withDefaults(defineProps<Props>(), {});\n\nconst emit = defineEmits<{ select: [string] }>();\n\nconst forward = useForwardPropsEmits(props, emit);\n</script>\n<template>\n  <Breadcrumb\n    v-if=\"styleType === 'normal'\"\n    v-bind=\"forward\"\n    class=\"vben-breadcrumb\"\n  />\n  <BreadcrumbBackground\n    v-if=\"styleType === 'background'\"\n    v-bind=\"forward\"\n    class=\"vben-breadcrumb\"\n  />\n</template>\n<style lang=\"scss\" scoped>\n/** 修复全局引入Antd时，ol和ul的默认样式会被修改的问题 */\n.vben-breadcrumb {\n  :deep(ol),\n  :deep(ul) {\n    margin-bottom: 0;\n  }\n}\n</style>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/components/breadcrumb/breadcrumb.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { BreadcrumbProps } from './types';\n\nimport { ChevronDown } from '@vben-core/icons';\n\nimport {\n  Breadcrumb,\n  BreadcrumbItem,\n  BreadcrumbLink,\n  BreadcrumbList,\n  BreadcrumbPage,\n  BreadcrumbSeparator,\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuTrigger,\n} from '../../ui';\nimport { VbenIcon } from '../icon';\n\ninterface Props extends BreadcrumbProps {}\n\ndefineOptions({ name: 'Breadcrumb' });\nwithDefaults(defineProps<Props>(), {\n  showIcon: false,\n});\n\nconst emit = defineEmits<{ select: [string] }>();\n\nfunction handleClick(path?: string) {\n  if (!path) {\n    return;\n  }\n  emit('select', path);\n}\n</script>\n<template>\n  <Breadcrumb>\n    <BreadcrumbList>\n      <TransitionGroup name=\"breadcrumb-transition\">\n        <template\n          v-for=\"(item, index) in breadcrumbs\"\n          :key=\"`${item.path}-${item.title}-${index}`\"\n        >\n          <BreadcrumbItem>\n            <div v-if=\"item.items?.length ?? 0 > 0\">\n              <DropdownMenu>\n                <DropdownMenuTrigger class=\"flex items-center gap-1\">\n                  <VbenIcon v-if=\"showIcon\" :icon=\"item.icon\" class=\"size-5\" />\n                  {{ item.title }}\n                  <ChevronDown class=\"size-4\" />\n                </DropdownMenuTrigger>\n                <DropdownMenuContent align=\"start\">\n                  <template\n                    v-for=\"menuItem in item.items\"\n                    :key=\"`sub-${menuItem.path}`\"\n                  >\n                    <DropdownMenuItem @click.stop=\"handleClick(menuItem.path)\">\n                      {{ menuItem.title }}\n                    </DropdownMenuItem>\n                  </template>\n                </DropdownMenuContent>\n              </DropdownMenu>\n            </div>\n            <BreadcrumbLink\n              v-else-if=\"index !== breadcrumbs.length - 1\"\n              href=\"javascript:void 0\"\n              @click.stop=\"handleClick(item.path)\"\n            >\n              <div class=\"flex-center\">\n                <VbenIcon\n                  v-if=\"showIcon\"\n                  :class=\"{ 'size-5': item.isHome }\"\n                  :icon=\"item.icon\"\n                  class=\"mr-1 size-4\"\n                />\n                {{ item.title }}\n              </div>\n            </BreadcrumbLink>\n            <BreadcrumbPage v-else>\n              <div class=\"flex-center\">\n                <VbenIcon\n                  v-if=\"showIcon\"\n                  :class=\"{ 'size-5': item.isHome }\"\n                  :icon=\"item.icon\"\n                  class=\"mr-1 size-4\"\n                />\n                {{ item.title }}\n              </div>\n            </BreadcrumbPage>\n            <BreadcrumbSeparator\n              v-if=\"index < breadcrumbs.length - 1 && !item.isHome\"\n            />\n          </BreadcrumbItem>\n        </template>\n      </TransitionGroup>\n    </BreadcrumbList>\n  </Breadcrumb>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/components/breadcrumb/index.ts",
    "content": "export { default as VbenBreadcrumbView } from './breadcrumb-view.vue';\n\nexport type * from './types';\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/components/breadcrumb/types.ts",
    "content": "import type { BreadcrumbStyleType } from '@vben-core/typings';\nimport type { Component } from 'vue';\n\nexport interface IBreadcrumb {\n  icon?: Component | string;\n  isHome?: boolean;\n  items?: IBreadcrumb[];\n  path?: string;\n  title?: string;\n}\n\nexport interface BreadcrumbProps {\n  breadcrumbs: IBreadcrumb[];\n  showIcon?: boolean;\n  styleType?: BreadcrumbStyleType;\n}\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/components/button/button-group.vue",
    "content": "<script lang=\"ts\" setup>\nimport { cn } from '@vben-core/shared/utils';\n\ndefineOptions({ name: 'VbenButtonGroup' });\n\nwithDefaults(\n  defineProps<{\n    border?: boolean;\n    gap?: number;\n    size?: 'large' | 'middle' | 'small';\n  }>(),\n  { border: false, gap: 0, size: 'middle' },\n);\n</script>\n<template>\n  <div\n    :class=\"\n      cn(\n        'vben-button-group rounded-md',\n        `size-${size}`,\n        gap ? 'with-gap' : 'no-gap',\n        $attrs.class as string,\n      )\n    \"\n    :style=\"{ gap: gap ? `${gap}px` : '0px' }\"\n  >\n    <slot></slot>\n  </div>\n</template>\n\n<style lang=\"scss\" scoped>\n.vben-button-group {\n  display: inline-flex;\n\n  &.size-large :deep(button) {\n    height: 2.25rem;\n    padding: 0.5rem 0.75rem;\n    font-size: 0.875rem;\n    line-height: 1.25rem;\n\n    .icon-wrapper {\n      margin-right: 0.4rem;\n\n      svg {\n        width: 1rem;\n        height: 1rem;\n      }\n    }\n  }\n\n  &.size-middle :deep(button) {\n    height: 2rem;\n    padding: 0.25rem 0.5rem;\n    font-size: 0.75rem;\n    line-height: 1rem;\n\n    .icon-wrapper {\n      margin-right: 0.2rem;\n\n      svg {\n        width: 0.75rem;\n        height: 0.75rem;\n      }\n    }\n  }\n\n  &.size-small :deep(button) {\n    height: 1.75rem;\n    padding: 0.2rem 0.4rem;\n    font-size: 0.65rem;\n    line-height: 0.75rem;\n\n    .icon-wrapper {\n      margin-right: 0.1rem;\n\n      svg {\n        width: 0.65rem;\n        height: 0.65rem;\n      }\n    }\n  }\n\n  &.no-gap > :deep(button):nth-of-type(1) {\n    border-radius: calc(var(--radius) - 2px) 0 0 calc(var(--radius) - 2px);\n  }\n\n  &.no-gap > :deep(button):last-of-type {\n    border-radius: 0 calc(var(--radius) - 2px) calc(var(--radius) - 2px) 0;\n  }\n\n  &.no-gap {\n    :deep(button + button) {\n      border-left-width: 0;\n      border-radius: 0;\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/components/button/button.ts",
    "content": "import type { AsTag } from 'radix-vue';\n\nimport type { Component } from 'vue';\n\nimport type { ButtonVariants, ButtonVariantSize } from '../../ui';\n\nexport interface VbenButtonProps {\n  /**\n   * The element or component this component should render as. Can be overwrite by `asChild`\n   * @defaultValue \"div\"\n   */\n  as?: AsTag | Component;\n  /**\n   * Change the default rendered element for the one passed as a child, merging their props and behavior.\n   *\n   * Read our [Composition](https://www.radix-vue.com/guides/composition.html) guide for more details.\n   */\n  asChild?: boolean;\n  class?: any;\n  disabled?: boolean;\n  loading?: boolean;\n  size?: ButtonVariantSize;\n  variant?: ButtonVariants;\n}\n\nexport type CustomRenderType = (() => Component | string) | string;\n\nexport type ValueType = boolean | number | string;\n\nexport interface VbenButtonGroupProps\n  extends Pick<VbenButtonProps, 'disabled'> {\n  /** 单选模式下允许清除选中 */\n  allowClear?: boolean;\n  /** 值改变前的回调 */\n  beforeChange?: (\n    value: ValueType,\n    isChecked: boolean,\n  ) => boolean | PromiseLike<boolean | undefined> | undefined;\n  /** 按钮样式 */\n  btnClass?: any;\n  /** 按钮间隔距离 */\n  gap?: number;\n  /** 多选模式下限制最多选择的数量。0表示不限制 */\n  maxCount?: number;\n  /** 是否允许多选 */\n  multiple?: boolean;\n  /** 选项 */\n  options?: { [key: string]: any; label: CustomRenderType; value: ValueType }[];\n  /** 显示图标 */\n  showIcon?: boolean;\n  /** 尺寸 */\n  size?: 'large' | 'middle' | 'small';\n}\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/components/button/button.vue",
    "content": "<script setup lang=\"ts\">\nimport type { VbenButtonProps } from './button';\n\nimport { computed } from 'vue';\n\nimport { LoaderCircle } from '@vben-core/icons';\nimport { cn } from '@vben-core/shared/utils';\n\nimport { Primitive } from 'radix-vue';\n\nimport { buttonVariants } from '../../ui';\n\ninterface Props extends VbenButtonProps {}\n\nconst props = withDefaults(defineProps<Props>(), {\n  as: 'button',\n  class: '',\n  disabled: false,\n  loading: false,\n  size: 'default',\n  variant: 'default',\n});\n\nconst isDisabled = computed(() => {\n  return props.disabled || props.loading;\n});\n</script>\n\n<template>\n  <Primitive\n    :as=\"as\"\n    :as-child=\"asChild\"\n    :class=\"cn(buttonVariants({ variant, size }), props.class)\"\n    :disabled=\"isDisabled\"\n  >\n    <LoaderCircle\n      v-if=\"loading\"\n      class=\"text-md mr-2 size-4 flex-shrink-0 animate-spin\"\n    />\n    <slot></slot>\n  </Primitive>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/components/button/check-button-group.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { Arrayable } from '@vueuse/core';\n\nimport type { ValueType, VbenButtonGroupProps } from './button';\n\nimport { computed, ref, watch } from 'vue';\n\nimport { Circle, CircleCheckBig, LoaderCircle } from '@vben-core/icons';\nimport { cn, isFunction } from '@vben-core/shared/utils';\n\nimport { objectOmit } from '@vueuse/core';\n\nimport { VbenRenderContent } from '../render-content';\nimport VbenButtonGroup from './button-group.vue';\nimport Button from './button.vue';\n\nconst props = withDefaults(defineProps<VbenButtonGroupProps>(), {\n  gap: 0,\n  multiple: false,\n  showIcon: true,\n  size: 'middle',\n  allowClear: false,\n  maxCount: 0,\n});\nconst emit = defineEmits(['btnClick']);\nconst btnDefaultProps = computed(() => {\n  return {\n    ...objectOmit(props, ['options', 'btnClass', 'size', 'disabled']),\n    class: cn(props.btnClass),\n  };\n});\nconst modelValue = defineModel<Arrayable<ValueType> | undefined>();\n\nconst innerValue = ref<Array<ValueType>>([]);\nconst loadingValues = ref<Array<ValueType>>([]);\nwatch(\n  () => props.multiple,\n  (val) => {\n    if (val) {\n      modelValue.value = innerValue.value;\n    } else {\n      modelValue.value =\n        innerValue.value.length > 0 ? innerValue.value[0] : undefined;\n    }\n  },\n);\n\nwatch(\n  () => modelValue.value,\n  (val) => {\n    if (Array.isArray(val)) {\n      const arrVal = val.filter((v) => v !== undefined);\n      if (arrVal.length > 0) {\n        innerValue.value = props.multiple\n          ? [...arrVal]\n          : [arrVal[0] as ValueType];\n      } else {\n        innerValue.value = [];\n      }\n    } else {\n      innerValue.value = val === undefined ? [] : [val as ValueType];\n    }\n  },\n  { deep: true, immediate: true },\n);\n\nasync function onBtnClick(value: ValueType) {\n  if (props.beforeChange && isFunction(props.beforeChange)) {\n    try {\n      loadingValues.value.push(value);\n      const canChange = await props.beforeChange(\n        value,\n        !innerValue.value.includes(value),\n      );\n      if (canChange === false) {\n        return;\n      }\n    } finally {\n      loadingValues.value.splice(loadingValues.value.indexOf(value), 1);\n    }\n  }\n\n  if (props.multiple) {\n    if (innerValue.value.includes(value)) {\n      innerValue.value = innerValue.value.filter((item) => item !== value);\n    } else {\n      if (props.maxCount > 0 && innerValue.value.length >= props.maxCount) {\n        innerValue.value = innerValue.value.slice(0, props.maxCount - 1);\n      }\n      innerValue.value.push(value);\n    }\n    modelValue.value = innerValue.value;\n  } else {\n    if (props.allowClear && innerValue.value.includes(value)) {\n      innerValue.value = [];\n      modelValue.value = undefined;\n      emit('btnClick', undefined);\n      return;\n    } else {\n      innerValue.value = [value];\n      modelValue.value = value;\n    }\n  }\n  emit('btnClick', value);\n}\n</script>\n<template>\n  <VbenButtonGroup\n    :size=\"props.size\"\n    :gap=\"props.gap\"\n    class=\"vben-check-button-group\"\n  >\n    <Button\n      v-for=\"(btn, index) in props.options\"\n      :key=\"index\"\n      :class=\"cn('border', props.btnClass)\"\n      :disabled=\"\n        props.disabled ||\n        loadingValues.includes(btn.value) ||\n        (!props.multiple && loadingValues.length > 0)\n      \"\n      v-bind=\"btnDefaultProps\"\n      :variant=\"innerValue.includes(btn.value) ? 'default' : 'outline'\"\n      @click=\"onBtnClick(btn.value)\"\n      type=\"button\"\n    >\n      <div class=\"icon-wrapper\" v-if=\"props.showIcon\">\n        <slot\n          name=\"icon\"\n          :loading=\"loadingValues.includes(btn.value)\"\n          :checked=\"innerValue.includes(btn.value)\"\n        >\n          <LoaderCircle\n            class=\"animate-spin\"\n            v-if=\"loadingValues.includes(btn.value)\"\n          />\n          <CircleCheckBig v-else-if=\"innerValue.includes(btn.value)\" />\n          <Circle v-else />\n        </slot>\n      </div>\n      <slot name=\"option\" :label=\"btn.label\" :value=\"btn.value\" :data=\"btn\">\n        <VbenRenderContent :content=\"btn.label\" />\n      </slot>\n    </Button>\n  </VbenButtonGroup>\n</template>\n<style lang=\"scss\" scoped>\n.vben-check-button-group {\n  display: flex;\n  flex-wrap: wrap;\n\n  &:deep(.size-large) button {\n    .icon-wrapper {\n      margin-right: 0.3rem;\n\n      svg {\n        width: 1rem;\n        height: 1rem;\n      }\n    }\n  }\n\n  &:deep(.size-middle) button {\n    .icon-wrapper {\n      margin-right: 0.2rem;\n\n      svg {\n        width: 0.75rem;\n        height: 0.75rem;\n      }\n    }\n  }\n\n  &:deep(.size-small) button {\n    .icon-wrapper {\n      margin-right: 0.1rem;\n\n      svg {\n        width: 0.65rem;\n        height: 0.65rem;\n      }\n    }\n  }\n\n  &.no-gap > :deep(button):nth-of-type(1) {\n    border-right-width: 0;\n  }\n\n  &.no-gap {\n    :deep(button + button) {\n      margin-right: -1px;\n      border-left-width: 1px;\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/components/button/icon-button.vue",
    "content": "<script setup lang=\"ts\">\nimport type { ButtonVariants } from '../../ui';\nimport type { VbenButtonProps } from './button';\n\nimport { computed, useSlots } from 'vue';\n\nimport { cn } from '@vben-core/shared/utils';\n\nimport { VbenTooltip } from '../tooltip';\nimport VbenButton from './button.vue';\n\ninterface Props extends VbenButtonProps {\n  class?: any;\n  disabled?: boolean;\n  onClick?: () => void;\n  tooltip?: string;\n  tooltipDelayDuration?: number;\n  tooltipSide?: 'bottom' | 'left' | 'right' | 'top';\n  variant?: ButtonVariants;\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n  disabled: false,\n  onClick: () => {},\n  tooltipDelayDuration: 200,\n  tooltipSide: 'bottom',\n  variant: 'icon',\n});\n\nconst slots = useSlots();\n\nconst showTooltip = computed(() => !!slots.tooltip || !!props.tooltip);\n</script>\n\n<template>\n  <VbenButton\n    v-if=\"!showTooltip\"\n    :class=\"cn('rounded-full', props.class)\"\n    :disabled=\"disabled\"\n    :variant=\"variant\"\n    size=\"icon\"\n    @click=\"onClick\"\n  >\n    <slot></slot>\n  </VbenButton>\n\n  <VbenTooltip\n    v-else\n    :delay-duration=\"tooltipDelayDuration\"\n    :side=\"tooltipSide\"\n  >\n    <template #trigger>\n      <VbenButton\n        :class=\"cn('rounded-full', props.class)\"\n        :disabled=\"disabled\"\n        :variant=\"variant\"\n        size=\"icon\"\n        @click=\"onClick\"\n      >\n        <slot></slot>\n      </VbenButton>\n    </template>\n    <slot v-if=\"slots.tooltip\" name=\"tooltip\"> </slot>\n    <template v-else>\n      {{ tooltip }}\n    </template>\n  </VbenTooltip>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/components/button/index.ts",
    "content": "export type * from './button';\nexport { default as VbenButtonGroup } from './button-group.vue';\nexport { default as VbenButton } from './button.vue';\nexport { default as VbenCheckButtonGroup } from './check-button-group.vue';\nexport { default as VbenIconButton } from './icon-button.vue';\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/components/checkbox/checkbox.vue",
    "content": "<script setup lang=\"ts\">\nimport type { CheckboxRootEmits, CheckboxRootProps } from 'radix-vue';\n\nimport { useId } from 'vue';\n\nimport { useForwardPropsEmits } from 'radix-vue';\n\nimport { Checkbox } from '../../ui/checkbox';\n\nconst props = defineProps<CheckboxRootProps & { indeterminate?: boolean }>();\n\nconst emits = defineEmits<CheckboxRootEmits>();\n\nconst checked = defineModel<boolean>('checked');\n\nconst forwarded = useForwardPropsEmits(props, emits);\n\nconst id = useId();\n</script>\n\n<template>\n  <div class=\"flex items-center\">\n    <Checkbox v-bind=\"forwarded\" :id=\"id\" v-model:checked=\"checked\" />\n    <label :for=\"id\" class=\"ml-2 cursor-pointer text-sm\"> <slot></slot> </label>\n  </div>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/components/checkbox/index.ts",
    "content": "export { default as VbenCheckbox } from './checkbox.vue';\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/components/context-menu/context-menu.vue",
    "content": "<script setup lang=\"ts\">\nimport type {\n  ContextMenuContentProps,\n  ContextMenuRootEmits,\n  ContextMenuRootProps,\n} from 'radix-vue';\n\nimport type { ClassType } from '@vben-core/typings';\n\nimport type { IContextMenuItem } from './interface';\n\nimport { computed } from 'vue';\n\nimport { useForwardPropsEmits } from 'radix-vue';\n\nimport {\n  ContextMenu,\n  ContextMenuContent,\n  ContextMenuItem,\n  ContextMenuSeparator,\n  ContextMenuShortcut,\n  ContextMenuTrigger,\n} from '../../ui/context-menu';\n\nconst props = defineProps<\n  ContextMenuRootProps & {\n    class?: ClassType;\n    contentClass?: ClassType;\n    contentProps?: ContextMenuContentProps;\n    handlerData?: Record<string, any>;\n    itemClass?: ClassType;\n    menus: (data: any) => IContextMenuItem[];\n  }\n>();\n\nconst emits = defineEmits<ContextMenuRootEmits>();\n\nconst delegatedProps = computed(() => {\n  const {\n    class: _cls,\n    contentClass: _,\n    contentProps: _cProps,\n    itemClass: _iCls,\n    ...delegated\n  } = props;\n\n  return delegated;\n});\n\nconst forwarded = useForwardPropsEmits(delegatedProps, emits);\n\nconst menusView = computed(() => {\n  return props.menus?.(props.handlerData);\n});\n\nfunction handleClick(menu: IContextMenuItem) {\n  if (menu.disabled) {\n    return;\n  }\n  menu?.handler?.(props.handlerData);\n}\n</script>\n\n<template>\n  <ContextMenu v-bind=\"forwarded\">\n    <ContextMenuTrigger as-child>\n      <slot></slot>\n    </ContextMenuTrigger>\n    <ContextMenuContent\n      :class=\"contentClass\"\n      v-bind=\"contentProps\"\n      class=\"side-content z-popup\"\n    >\n      <template v-for=\"menu in menusView\" :key=\"menu.key\">\n        <ContextMenuItem\n          :class=\"itemClass\"\n          :disabled=\"menu.disabled\"\n          :inset=\"menu.inset || !menu.icon\"\n          class=\"cursor-pointer\"\n          @click=\"handleClick(menu)\"\n        >\n          <component\n            :is=\"menu.icon\"\n            v-if=\"menu.icon\"\n            class=\"mr-2 size-4 text-lg\"\n          />\n\n          {{ menu.text }}\n          <ContextMenuShortcut v-if=\"menu.shortcut\">\n            {{ menu.shortcut }}\n          </ContextMenuShortcut>\n        </ContextMenuItem>\n        <ContextMenuSeparator v-if=\"menu.separator\" />\n      </template>\n    </ContextMenuContent>\n  </ContextMenu>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/components/context-menu/index.ts",
    "content": "export { default as VbenContextMenu } from './context-menu.vue';\n\nexport type * from './interface';\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/components/context-menu/interface.ts",
    "content": "import type { Component } from 'vue';\n\ninterface IContextMenuItem {\n  /**\n   * @zh_CN 是否禁用\n   */\n  disabled?: boolean;\n  /**\n   * @zh_CN 点击事件处理\n   * @param data\n   */\n  handler?: (data: any) => void;\n  /**\n   * @zh_CN 图标\n   */\n  icon?: Component;\n  /**\n   * @zh_CN 是否显示图标\n   */\n  inset?: boolean;\n  /**\n   * @zh_CN 唯一标识\n   */\n  key: string;\n  /**\n   * @zh_CN 是否是分割线\n   */\n  separator?: boolean;\n  /**\n   * @zh_CN 快捷键\n   */\n  shortcut?: string;\n  /**\n   * @zh_CN 标题\n   */\n  text: string;\n}\nexport type { IContextMenuItem };\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/components/count-to-animator/count-to-animator.vue",
    "content": "<script lang=\"ts\" setup>\nimport { computed, onMounted, ref, unref, watch, watchEffect } from 'vue';\n\nimport { isNumber } from '@vben-core/shared/utils';\n\nimport { TransitionPresets, useTransition } from '@vueuse/core';\n\ninterface Props {\n  autoplay?: boolean;\n  color?: string;\n  decimal?: string;\n  decimals?: number;\n  duration?: number;\n  endVal?: number;\n  prefix?: string;\n  separator?: string;\n  startVal?: number;\n  suffix?: string;\n  transition?: keyof typeof TransitionPresets;\n  useEasing?: boolean;\n}\n\ndefineOptions({ name: 'CountToAnimator' });\n\nconst props = withDefaults(defineProps<Props>(), {\n  autoplay: true,\n  color: '',\n  decimal: '.',\n  decimals: 0,\n  duration: 1500,\n  endVal: 2021,\n  prefix: '',\n  separator: ',',\n  startVal: 0,\n  suffix: '',\n  transition: 'linear',\n  useEasing: true,\n});\n\nconst emit = defineEmits<{\n  finished: [];\n  /**\n   * @deprecated 请使用{@link finished}事件\n   */\n  onFinished: [];\n  /**\n   * @deprecated 请使用{@link started}事件\n   */\n  onStarted: [];\n  started: [];\n}>();\n\nconst source = ref(props.startVal);\nconst disabled = ref(false);\nlet outputValue = useTransition(source);\n\nconst value = computed(() => formatNumber(unref(outputValue)));\n\nwatchEffect(() => {\n  source.value = props.startVal;\n});\n\nwatch([() => props.startVal, () => props.endVal], () => {\n  if (props.autoplay) {\n    start();\n  }\n});\n\nonMounted(() => {\n  props.autoplay && start();\n});\n\nfunction start() {\n  run();\n  source.value = props.endVal;\n}\n\nfunction reset() {\n  source.value = props.startVal;\n  run();\n}\n\nfunction run() {\n  outputValue = useTransition(source, {\n    disabled,\n    duration: props.duration,\n    onFinished: () => {\n      emit('finished');\n      emit('onFinished');\n    },\n    onStarted: () => {\n      emit('started');\n      emit('onStarted');\n    },\n    ...(props.useEasing\n      ? { transition: TransitionPresets[props.transition] }\n      : {}),\n  });\n}\n\nfunction formatNumber(num: number | string) {\n  if (!num && num !== 0) {\n    return '';\n  }\n  const { decimal, decimals, prefix, separator, suffix } = props;\n  num = Number(num).toFixed(decimals);\n  num += '';\n\n  const x = num.split('.');\n  let x1 = x[0];\n  const x2 = x.length > 1 ? decimal + x[1] : '';\n\n  const rgx = /(\\d+)(\\d{3})/;\n  if (separator && !isNumber(separator) && x1) {\n    while (rgx.test(x1)) {\n      x1 = x1.replace(rgx, `$1${separator}$2`);\n    }\n  }\n  return prefix + x1 + x2 + suffix;\n}\n\ndefineExpose({ reset });\n</script>\n<template>\n  <span :style=\"{ color }\">\n    {{ value }}\n  </span>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/components/count-to-animator/index.ts",
    "content": "export { default as VbenCountToAnimator } from './count-to-animator.vue';\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/components/dropdown-menu/dropdown-menu.vue",
    "content": "<script lang=\"ts\" setup>\nimport type {\n  DropdownMenuProps,\n  VbenDropdownMenuItem as IDropdownMenuItem,\n} from './interface';\n\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuGroup,\n  DropdownMenuItem,\n  DropdownMenuSeparator,\n  DropdownMenuTrigger,\n} from '../../ui';\n\ninterface Props extends DropdownMenuProps {}\n\ndefineOptions({ name: 'DropdownMenu' });\nconst props = withDefaults(defineProps<Props>(), {});\n\nfunction handleItemClick(menu: IDropdownMenuItem) {\n  if (menu.disabled) {\n    return;\n  }\n  menu?.handler?.(props);\n}\n</script>\n<template>\n  <DropdownMenu>\n    <DropdownMenuTrigger class=\"flex h-full items-center gap-1\">\n      <slot></slot>\n    </DropdownMenuTrigger>\n    <DropdownMenuContent align=\"start\">\n      <DropdownMenuGroup>\n        <template v-for=\"menu in menus\" :key=\"menu.value\">\n          <DropdownMenuItem\n            :disabled=\"menu.disabled\"\n            class=\"data-[state=checked]:bg-accent data-[state=checked]:text-accent-foreground text-foreground/80 mb-1 cursor-pointer\"\n            @click=\"handleItemClick(menu)\"\n          >\n            <component :is=\"menu.icon\" v-if=\"menu.icon\" class=\"mr-2 size-4\" />\n            {{ menu.label }}\n          </DropdownMenuItem>\n          <DropdownMenuSeparator v-if=\"menu.separator\" class=\"bg-border\" />\n        </template>\n      </DropdownMenuGroup>\n    </DropdownMenuContent>\n  </DropdownMenu>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/components/dropdown-menu/dropdown-radio-menu.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { DropdownMenuProps } from './interface';\n\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuGroup,\n  DropdownMenuItem,\n  DropdownMenuTrigger,\n} from '../../ui';\n\ninterface Props extends DropdownMenuProps {}\n\ndefineOptions({ name: 'DropdownRadioMenu' });\nwithDefaults(defineProps<Props>(), {});\n\nconst modelValue = defineModel<string>();\n\nfunction handleItemClick(value: string) {\n  modelValue.value = value;\n}\n</script>\n<template>\n  <DropdownMenu>\n    <DropdownMenuTrigger as-child class=\"flex items-center gap-1\">\n      <slot></slot>\n    </DropdownMenuTrigger>\n    <DropdownMenuContent align=\"start\">\n      <DropdownMenuGroup>\n        <template v-for=\"menu in menus\" :key=\"menu.key\">\n          <DropdownMenuItem\n            :class=\"\n              menu.value === modelValue\n                ? 'bg-accent text-accent-foreground'\n                : ''\n            \"\n            class=\"data-[state=checked]:bg-accent data-[state=checked]:text-accent-foreground text-foreground/80 mb-1 cursor-pointer\"\n            @click=\"handleItemClick(menu.value)\"\n          >\n            <component :is=\"menu.icon\" v-if=\"menu.icon\" class=\"mr-2 size-4\" />\n            <span\n              v-if=\"!menu.icon\"\n              :class=\"menu.value === modelValue ? 'bg-foreground' : ''\"\n              class=\"mr-2 size-1.5 rounded-full\"\n            ></span>\n            {{ menu.label }}\n          </DropdownMenuItem>\n        </template>\n      </DropdownMenuGroup>\n    </DropdownMenuContent>\n  </DropdownMenu>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/components/dropdown-menu/index.ts",
    "content": "export { default as VbenDropdownMenu } from './dropdown-menu.vue';\nexport { default as VbenDropdownRadioMenu } from './dropdown-radio-menu.vue';\n\nexport type * from './interface';\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/components/dropdown-menu/interface.ts",
    "content": "import type { Component } from 'vue';\n\ninterface VbenDropdownMenuItem {\n  disabled?: boolean;\n  /**\n   * @zh_CN 点击事件处理\n   * @param data\n   */\n  handler?: (data: any) => void;\n  /**\n   * @zh_CN 图标\n   */\n  icon?: Component;\n  /**\n   * @zh_CN 标题\n   */\n  label: string;\n  /**\n   * @zh_CN 是否是分割线\n   */\n  separator?: boolean;\n  /**\n   * @zh_CN 唯一标识\n   */\n  value: string;\n}\n\ninterface DropdownMenuProps {\n  menus: VbenDropdownMenuItem[];\n}\n\nexport type { DropdownMenuProps, VbenDropdownMenuItem };\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/components/expandable-arrow/expandable-arrow.vue",
    "content": "<script lang=\"ts\" setup>\nimport { ChevronDown } from '@vben-core/icons';\nimport { cn } from '@vben-core/shared/utils';\n\nconst props = defineProps<{\n  class?: string;\n}>();\n\n// 控制箭头展开/收起状态\nconst collapsed = defineModel({ default: false });\n</script>\n\n<template>\n  <div\n    :class=\"cn('vben-link inline-flex items-center', props.class)\"\n    @click=\"collapsed = !collapsed\"\n  >\n    <slot :is-expanded=\"collapsed\">\n      {{ collapsed }}\n      <!-- <span>{{ isExpanded ? '收起' : '展开' }}</span> -->\n    </slot>\n    <div\n      :class=\"{ 'rotate-180': !collapsed }\"\n      class=\"transition-transform duration-300\"\n    >\n      <slot name=\"icon\">\n        <ChevronDown class=\"size-4\" />\n      </slot>\n    </div>\n  </div>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/components/expandable-arrow/index.ts",
    "content": "export { default as VbenExpandableArrow } from './expandable-arrow.vue';\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/components/full-screen/full-screen.vue",
    "content": "<script lang=\"ts\" setup>\nimport { Maximize, Minimize } from '@vben-core/icons';\n\nimport { useFullscreen } from '@vueuse/core';\n\nimport { VbenIconButton } from '../button';\n\ndefineOptions({ name: 'FullScreen' });\n\nconst { isFullscreen, toggle } = useFullscreen();\n\n// 重新检查全屏状态\nisFullscreen.value = !!(\n  document.fullscreenElement ||\n  // @ts-ignore\n  document.webkitFullscreenElement ||\n  // @ts-ignore\n  document.mozFullScreenElement ||\n  // @ts-ignore\n  document.msFullscreenElement\n);\n</script>\n<template>\n  <VbenIconButton\n    class=\"hover:animate-[shrink_0.3s_ease-in-out]\"\n    @click=\"toggle\"\n  >\n    <Minimize v-if=\"isFullscreen\" class=\"text-foreground size-4\" />\n    <Maximize v-else class=\"text-foreground size-4\" />\n  </VbenIconButton>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/components/full-screen/index.ts",
    "content": "export { default as VbenFullScreen } from './full-screen.vue';\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/components/hover-card/hover-card.vue",
    "content": "<script setup lang=\"ts\">\nimport type {\n  HoverCardContentProps,\n  HoverCardRootEmits,\n  HoverCardRootProps,\n} from 'radix-vue';\n\nimport type { ClassType } from '@vben-core/typings';\n\nimport { computed } from 'vue';\n\nimport { useForwardPropsEmits } from 'radix-vue';\n\nimport { HoverCard, HoverCardContent, HoverCardTrigger } from '../../ui';\n\ninterface Props extends HoverCardRootProps {\n  class?: ClassType;\n  contentClass?: ClassType;\n  contentProps?: HoverCardContentProps;\n}\n\nconst props = defineProps<Props>();\n\nconst emits = defineEmits<HoverCardRootEmits>();\n\nconst delegatedProps = computed(() => {\n  const {\n    class: _cls,\n    contentClass: _,\n    contentProps: _cProps,\n    ...delegated\n  } = props;\n\n  return delegated;\n});\n\nconst forwarded = useForwardPropsEmits(delegatedProps, emits);\n</script>\n\n<template>\n  <HoverCard v-bind=\"forwarded\">\n    <HoverCardTrigger as-child class=\"h-full\">\n      <div class=\"h-full cursor-pointer\">\n        <slot name=\"trigger\"></slot>\n      </div>\n    </HoverCardTrigger>\n    <HoverCardContent\n      :class=\"contentClass\"\n      v-bind=\"contentProps\"\n      class=\"side-content z-popup\"\n    >\n      <slot></slot>\n    </HoverCardContent>\n  </HoverCard>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/components/hover-card/index.ts",
    "content": "export { default as VbenHoverCard } from './hover-card.vue';\nexport type { HoverCardContentProps } from 'radix-vue';\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/components/icon/icon.vue",
    "content": "<script setup lang=\"ts\">\nimport type { Component } from 'vue';\n\nimport { IconDefault, IconifyIcon } from '@vben-core/icons';\nimport {\n  isFunction,\n  isHttpUrl,\n  isObject,\n  isString,\n} from '@vben-core/shared/utils';\nimport { computed } from 'vue';\n\nconst props = defineProps<{\n  // 没有是否显示默认图标\n  fallback?: boolean;\n  icon?: Component | Function | string;\n}>();\n\nconst isRemoteIcon = computed(() => {\n  return isString(props.icon) && isHttpUrl(props.icon);\n});\n\nconst isComponent = computed(() => {\n  const { icon } = props;\n  return !isString(icon) && (isObject(icon) || isFunction(icon));\n});\n</script>\n\n<template>\n  <component :is=\"icon as Component\" v-if=\"isComponent\" v-bind=\"$attrs\" />\n  <img v-else-if=\"isRemoteIcon\" :src=\"icon as string\" v-bind=\"$attrs\" />\n  <IconifyIcon v-else-if=\"icon\" v-bind=\"$attrs\" :icon=\"icon as string\" />\n  <IconDefault v-else-if=\"fallback\" v-bind=\"$attrs\" />\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/components/icon/index.ts",
    "content": "export { default as VbenIcon } from './icon.vue';\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/components/index.ts",
    "content": "export * from './avatar';\nexport * from './back-top';\nexport * from './breadcrumb';\nexport * from './button';\nexport * from './checkbox';\nexport * from './context-menu';\nexport * from './count-to-animator';\nexport * from './dropdown-menu';\nexport * from './expandable-arrow';\nexport * from './full-screen';\nexport * from './hover-card';\nexport * from './icon';\nexport * from './input-captcha';\nexport * from './input-password';\nexport * from './logo';\nexport * from './pin-input';\nexport * from './popover';\nexport * from './render-content';\nexport * from './scrollbar';\nexport * from './segmented';\nexport * from './select';\nexport * from './spine-text';\nexport * from './spinner';\nexport * from './tooltip';\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/components/input-captcha/index.ts",
    "content": "export { default as VbenInputCaptcha } from './input-captcha.vue';\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/components/input-captcha/input-captcha.vue",
    "content": "<script setup lang=\"ts\">\nimport { Input as VbenInput } from '../../ui/input';\n\ninterface Props {\n  captcha?: string;\n  label?: string;\n  loading?: boolean;\n  placeholder?: string;\n}\n\nwithDefaults(defineProps<Props>(), {\n  captcha: '',\n  label: '验证码',\n  loading: false,\n  placeholder: '请输入验证码',\n});\n\ndefineEmits<{ captchaClick: [] }>();\n\nconst modelValue = defineModel<string>({ default: '' });\n</script>\n\n<!-- 图片验证码 -->\n<template>\n  <div class=\"flex w-full\">\n    <div class=\"flex-1\">\n      <VbenInput\n        id=\"code\"\n        name=\"code\"\n        type=\"text\"\n        autocomplete=\"off\"\n        required\n        v-model=\"modelValue\"\n        :class=\"$attrs?.class ?? {}\"\n        :label=\"label\"\n        :placeholder=\"placeholder\"\n      />\n    </div>\n    <div class=\"captcha-image--container relative\">\n      <img\n        :src=\"captcha\"\n        class=\"h-[40px] w-[115px] cursor-pointer rounded-r-md\"\n        :class=\"{ 'pointer-events-none': loading }\"\n        @click=\"$emit('captchaClick')\"\n      />\n      <div\n        v-if=\"loading\"\n        class=\"absolute inset-0 flex cursor-not-allowed items-center justify-center rounded-r-md bg-black/30\"\n      >\n        <span class=\"captcha-loading\"></span>\n      </div>\n    </div>\n  </div>\n</template>\n\n<style lang=\"scss\">\n@keyframes loading-rotation {\n  0% {\n    transform: rotate(0deg);\n  }\n\n  100% {\n    transform: rotate(360deg);\n  }\n}\n\n.captcha-loading {\n  box-sizing: border-box;\n  display: inline-block;\n  width: 18px;\n  height: 18px;\n  border: 2px solid #fff;\n  border-bottom-color: transparent;\n  border-radius: 50%;\n  animation: loading-rotation 1s linear infinite;\n}\n\n/**\n  验证码输入框样式\n  去除右边的圆角\n*/\ninput[id='code'] {\n  border-top-right-radius: 0;\n  border-bottom-right-radius: 0;\n}\n</style>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/components/input-password/index.ts",
    "content": "export { default as VbenInputPassword } from './input-password.vue';\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/components/input-password/input-password.vue",
    "content": "<script setup lang=\"ts\">\nimport { ref, useSlots } from 'vue';\n\nimport { Eye, EyeOff } from '@vben-core/icons';\nimport { cn } from '@vben-core/shared/utils';\n\nimport { Input } from '../../ui';\nimport PasswordStrength from './password-strength.vue';\n\ninterface Props {\n  class?: any;\n  /**\n   * 是否显示密码强度\n   */\n  passwordStrength?: boolean;\n}\n\ndefineOptions({\n  inheritAttrs: false,\n});\n\nconst props = defineProps<Props>();\n\nconst modelValue = defineModel<string>();\n\nconst slots = useSlots();\n\nconst show = ref(false);\n</script>\n\n<template>\n  <div class=\"relative w-full\">\n    <Input\n      v-bind=\"$attrs\"\n      v-model=\"modelValue\"\n      :class=\"cn(props.class)\"\n      :type=\"show ? 'text' : 'password'\"\n    />\n    <template v-if=\"passwordStrength\">\n      <PasswordStrength :password=\"modelValue\" />\n      <p v-if=\"slots.strengthText\" class=\"text-muted-foreground mt-1.5 text-xs\">\n        <slot name=\"strengthText\"> </slot>\n      </p>\n    </template>\n    <div\n      :class=\"{\n        'top-3': !!passwordStrength,\n        'top-1/2 -translate-y-1/2 items-center': !passwordStrength,\n      }\"\n      class=\"hover:text-foreground text-foreground/60 absolute inset-y-0 right-0 flex cursor-pointer pr-3 text-lg leading-5\"\n      @click=\"show = !show\"\n    >\n      <Eye v-if=\"show\" class=\"size-4\" />\n      <EyeOff v-else class=\"size-4\" />\n    </div>\n  </div>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/components/input-password/password-strength.vue",
    "content": "<script setup lang=\"ts\">\nimport { computed } from 'vue';\n\nconst props = withDefaults(defineProps<{ password?: string }>(), {\n  password: '',\n});\n\nconst strengthList: string[] = [\n  '',\n  '#e74242',\n  '#ED6F6F',\n  '#EFBD47',\n  '#55D18780',\n  '#55D187',\n];\n\nconst currentStrength = computed(() => {\n  return checkPasswordStrength(props.password);\n});\n\nconst currentColor = computed(() => {\n  return strengthList[currentStrength.value];\n});\n\n/**\n * Check the strength of a password\n */\nfunction checkPasswordStrength(password: string) {\n  let strength = 0;\n\n  // Check length\n  if (password.length >= 8) strength++;\n\n  // Check for lowercase letters\n  if (/[a-z]/.test(password)) strength++;\n\n  // Check for uppercase letters\n  if (/[A-Z]/.test(password)) strength++;\n\n  // Check for numbers\n  if (/\\d/.test(password)) strength++;\n\n  // Check for special characters\n  if (/[^\\da-z]/i.test(password)) strength++;\n\n  return strength;\n}\n</script>\n\n<template>\n  <div class=\"relative mt-2 flex items-center justify-between\">\n    <template v-for=\"index in 5\" :key=\"index\">\n      <div\n        class=\"dark:bg-input-background bg-heavy relative mr-1 h-1.5 w-1/5 rounded-sm last:mr-0\"\n      >\n        <span\n          :style=\"{\n            backgroundColor: currentColor,\n            width: currentStrength >= index ? '100%' : '',\n          }\"\n          class=\"absolute left-0 h-full w-0 rounded-sm transition-all duration-500\"\n        ></span>\n      </div>\n    </template>\n  </div>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/components/logo/index.ts",
    "content": "export { default as VbenLogo } from './logo.vue';\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/components/logo/logo.vue",
    "content": "<script setup lang=\"ts\">\nimport { VbenAvatar } from '../avatar';\n\ninterface Props {\n  /**\n   * @zh_CN 是否收起文本\n   */\n  collapsed?: boolean;\n  /**\n   * @zh_CN Logo 图片适应方式\n   */\n  fit?: 'contain' | 'cover' | 'fill' | 'none' | 'scale-down';\n  /**\n   * @zh_CN Logo 跳转地址\n   */\n  href?: string;\n  /**\n   * @zh_CN Logo 图片大小\n   */\n  logoSize?: number;\n  /**\n   * @zh_CN Logo 图标\n   */\n  src?: string;\n  /**\n   * @zh_CN Logo 文本\n   */\n  text: string;\n  /**\n   * @zh_CN Logo 主题\n   */\n  theme?: string;\n}\n\ndefineOptions({\n  name: 'VbenLogo',\n});\n\nwithDefaults(defineProps<Props>(), {\n  collapsed: false,\n  href: 'javascript:void 0',\n  logoSize: 32,\n  src: '',\n  theme: 'light',\n  fit: 'cover',\n});\n</script>\n\n<template>\n  <div :class=\"theme\" class=\"flex h-full items-center text-lg\">\n    <a\n      :class=\"$attrs.class\"\n      :href=\"href\"\n      class=\"flex h-full items-center gap-2 overflow-hidden px-3 text-lg leading-normal transition-all duration-500\"\n    >\n      <VbenAvatar\n        v-if=\"src\"\n        :alt=\"text\"\n        :src=\"src\"\n        :size=\"logoSize\"\n        :fit=\"fit\"\n        class=\"relative rounded-none bg-transparent\"\n      />\n      <template v-if=\"!collapsed\">\n        <slot name=\"text\">\n          <span class=\"text-foreground truncate text-nowrap font-semibold\">\n            {{ text }}\n          </span>\n        </slot>\n      </template>\n    </a>\n  </div>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/components/pin-input/index.ts",
    "content": "export { default as VbenPinInput } from './input.vue';\n\nexport type * from './types';\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/components/pin-input/input.vue",
    "content": "<script setup lang=\"ts\">\nimport type { PinInputProps } from './types';\n\nimport { computed, onBeforeUnmount, ref, useId, watch } from 'vue';\n\nimport { PinInput, PinInputGroup, PinInputInput } from '../../ui';\nimport { VbenButton } from '../button';\n\ndefineOptions({\n  inheritAttrs: false,\n});\n\nconst {\n  codeLength = 6,\n  createText = async () => {},\n  disabled = false,\n  handleSendCode = async () => {},\n  loading = false,\n  maxTime = 60,\n} = defineProps<PinInputProps>();\n\nconst emit = defineEmits<{\n  complete: [];\n  sendError: [error: any];\n}>();\n\nconst timer = ref<ReturnType<typeof setTimeout>>();\n\nconst modelValue = defineModel<string>();\n\nconst inputValue = ref<string[]>([]);\nconst countdown = ref(0);\n\nconst btnText = computed(() => {\n  const countdownValue = countdown.value;\n  return createText?.(countdownValue);\n});\n\nconst btnLoading = computed(() => {\n  return loading || countdown.value > 0;\n});\n\nwatch(\n  () => modelValue.value,\n  () => {\n    inputValue.value = modelValue.value?.split('') ?? [];\n  },\n);\n\nwatch(inputValue, (val) => {\n  modelValue.value = val.join('');\n});\n\nfunction handleComplete(e: string[]) {\n  modelValue.value = e.join('');\n  emit('complete');\n}\n\nasync function handleSend(e: Event) {\n  try {\n    e?.preventDefault();\n    countdown.value = maxTime;\n    startCountdown();\n    await handleSendCode();\n  } catch (error) {\n    console.error('Failed to send code:', error);\n    // Consider emitting an error event or showing a notification\n    emit('sendError', error);\n  }\n}\n\nfunction startCountdown() {\n  if (countdown.value > 0) {\n    timer.value = setTimeout(() => {\n      countdown.value--;\n      startCountdown();\n    }, 1000);\n  }\n}\n\nonBeforeUnmount(() => {\n  countdown.value = 0;\n  clearTimeout(timer.value);\n});\n\nconst id = useId();\n</script>\n\n<template>\n  <PinInput\n    :id=\"id\"\n    v-model=\"inputValue\"\n    :disabled=\"disabled\"\n    class=\"flex w-full justify-between\"\n    otp\n    placeholder=\"○\"\n    type=\"number\"\n    @complete=\"handleComplete\"\n  >\n    <div class=\"relative flex w-full\">\n      <PinInputGroup class=\"mr-2\">\n        <PinInputInput\n          v-for=\"(item, index) in codeLength\"\n          :key=\"item\"\n          :index=\"index\"\n        />\n      </PinInputGroup>\n      <VbenButton\n        :disabled=\"disabled\"\n        :loading=\"btnLoading\"\n        class=\"flex-grow\"\n        size=\"lg\"\n        variant=\"outline\"\n        @click=\"handleSend\"\n      >\n        {{ btnText }}\n      </VbenButton>\n    </div>\n  </PinInput>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/components/pin-input/types.ts",
    "content": "interface PinInputProps {\n  class?: any;\n  /**\n   * 验证码长度\n   */\n  codeLength?: number;\n  /**\n   * 发送验证码按钮文本\n   */\n  createText?: (countdown: number) => string;\n  /**\n   * 是否禁用\n   */\n  disabled?: boolean;\n  /**\n   * 自定义验证码发送逻辑\n   * @returns\n   */\n  handleSendCode?: () => Promise<void>;\n  /**\n   * 发送验证码按钮loading\n   */\n  loading?: boolean;\n  /**\n   * 最大重试时间\n   */\n  maxTime?: number;\n}\n\nexport type { PinInputProps };\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/components/popover/index.ts",
    "content": "export { default as VbenPopover } from './popover.vue';\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/components/popover/popover.vue",
    "content": "<script setup lang=\"ts\">\nimport type {\n  PopoverContentProps,\n  PopoverRootEmits,\n  PopoverRootProps,\n} from 'radix-vue';\n\nimport type { ClassType } from '@vben-core/typings';\n\nimport { computed } from 'vue';\n\nimport { useForwardPropsEmits } from 'radix-vue';\n\nimport {\n  PopoverContent,\n  Popover as PopoverRoot,\n  PopoverTrigger,\n} from '../../ui';\n\ninterface Props extends PopoverRootProps {\n  class?: ClassType;\n  contentClass?: ClassType;\n  contentProps?: PopoverContentProps;\n  triggerClass?: ClassType;\n}\n\nconst props = withDefaults(defineProps<Props>(), {});\n\nconst emits = defineEmits<PopoverRootEmits>();\n\nconst delegatedProps = computed(() => {\n  const {\n    class: _cls,\n    contentClass: _,\n    contentProps: _cProps,\n    triggerClass: _tClass,\n    ...delegated\n  } = props;\n\n  return delegated;\n});\n\nconst forwarded = useForwardPropsEmits(delegatedProps, emits);\n</script>\n\n<template>\n  <PopoverRoot v-bind=\"forwarded\">\n    <PopoverTrigger :class=\"triggerClass\">\n      <slot name=\"trigger\"></slot>\n\n      <PopoverContent\n        :class=\"contentClass\"\n        class=\"side-content z-popup\"\n        v-bind=\"contentProps\"\n      >\n        <slot></slot>\n      </PopoverContent>\n    </PopoverTrigger>\n  </PopoverRoot>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/components/render-content/index.ts",
    "content": "export { default as VbenRenderContent } from './render-content.vue';\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/components/render-content/render-content.vue",
    "content": "<script lang=\"ts\">\nimport type { Component, PropType } from 'vue';\n\nimport { defineComponent, h } from 'vue';\n\nimport { isFunction, isObject, isString } from '@vben-core/shared/utils';\n\nexport default defineComponent({\n  name: 'RenderContent',\n  props: {\n    content: {\n      default: undefined as\n        | PropType<(() => any) | Component | string>\n        | undefined,\n      type: [Object, String, Function],\n    },\n    renderBr: {\n      default: false,\n      type: Boolean,\n    },\n  },\n  setup(props, { attrs, slots }) {\n    return () => {\n      if (!props.content) {\n        return null;\n      }\n      const isComponent =\n        (isObject(props.content) || isFunction(props.content)) &&\n        props.content !== null;\n      if (!isComponent) {\n        if (props.renderBr && isString(props.content)) {\n          const lines = props.content.split('\\n');\n          const result = [];\n          for (const [i, line] of lines.entries()) {\n            result.push(h('p', { key: i }, line));\n            // if (i < lines.length - 1) {\n            //   result.push(h('br'));\n            // }\n          }\n          return result;\n        } else {\n          return props.content;\n        }\n      }\n      return h(props.content as never, {\n        ...attrs,\n        props: {\n          ...props,\n          ...attrs,\n        },\n        slots,\n      });\n    };\n  },\n});\n</script>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/components/scrollbar/index.ts",
    "content": "export { default as VbenScrollbar } from './scrollbar.vue';\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/components/scrollbar/scrollbar.vue",
    "content": "<script setup lang=\"ts\">\nimport type { ClassType } from '@vben-core/typings';\n\nimport { computed, ref } from 'vue';\n\nimport { cn } from '@vben-core/shared/utils';\n\nimport { ScrollArea, ScrollBar } from '../../ui';\n\ninterface Props {\n  class?: ClassType;\n  horizontal?: boolean;\n  scrollBarClass?: ClassType;\n  shadow?: boolean;\n  shadowBorder?: boolean;\n  shadowBottom?: boolean;\n  shadowLeft?: boolean;\n  shadowRight?: boolean;\n  shadowTop?: boolean;\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n  class: '',\n  horizontal: false,\n  shadow: false,\n  shadowBorder: false,\n  shadowBottom: true,\n  shadowLeft: false,\n  shadowRight: false,\n  shadowTop: true,\n});\n\nconst emit = defineEmits<{\n  scrollAt: [{ bottom: boolean; left: boolean; right: boolean; top: boolean }];\n}>();\n\nconst isAtTop = ref(true);\nconst isAtRight = ref(false);\nconst isAtBottom = ref(false);\nconst isAtLeft = ref(true);\n\n/**\n * We have to check if the scroll amount is close enough to some threshold in order to\n * more accurately calculate arrivedState. This is because scrollTop/scrollLeft are non-rounded\n * numbers, while scrollHeight/scrollWidth and clientHeight/clientWidth are rounded.\n * https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollHeight#determine_if_an_element_has_been_totally_scrolled\n */\nconst ARRIVED_STATE_THRESHOLD_PIXELS = 1;\n\nconst showShadowTop = computed(() => props.shadow && props.shadowTop);\nconst showShadowBottom = computed(() => props.shadow && props.shadowBottom);\nconst showShadowLeft = computed(() => props.shadow && props.shadowLeft);\nconst showShadowRight = computed(() => props.shadow && props.shadowRight);\n\nconst computedShadowClasses = computed(() => {\n  return {\n    'both-shadow':\n      !isAtLeft.value &&\n      !isAtRight.value &&\n      showShadowLeft.value &&\n      showShadowRight.value,\n    'left-shadow': !isAtLeft.value && showShadowLeft.value,\n    'right-shadow': !isAtRight.value && showShadowRight.value,\n  };\n});\n\nfunction handleScroll(event: Event) {\n  const target = event.target as HTMLElement;\n  const scrollTop = target?.scrollTop ?? 0;\n  const scrollLeft = target?.scrollLeft ?? 0;\n  const clientHeight = target?.clientHeight ?? 0;\n  const clientWidth = target?.clientWidth ?? 0;\n  const scrollHeight = target?.scrollHeight ?? 0;\n  const scrollWidth = target?.scrollWidth ?? 0;\n  isAtTop.value = scrollTop <= 0;\n  isAtLeft.value = scrollLeft <= 0;\n  isAtBottom.value =\n    Math.abs(scrollTop) + clientHeight >=\n    scrollHeight - ARRIVED_STATE_THRESHOLD_PIXELS;\n  isAtRight.value =\n    Math.abs(scrollLeft) + clientWidth >=\n    scrollWidth - ARRIVED_STATE_THRESHOLD_PIXELS;\n\n  emit('scrollAt', {\n    bottom: isAtBottom.value,\n    left: isAtLeft.value,\n    right: isAtRight.value,\n    top: isAtTop.value,\n  });\n}\n</script>\n\n<template>\n  <ScrollArea\n    :class=\"[cn(props.class), computedShadowClasses]\"\n    :on-scroll=\"handleScroll\"\n    class=\"vben-scrollbar relative\"\n  >\n    <div\n      v-if=\"showShadowTop\"\n      :class=\"{\n        'opacity-100': !isAtTop,\n        'border-border border-t': shadowBorder && !isAtTop,\n      }\"\n      class=\"scrollbar-top-shadow pointer-events-none absolute top-0 z-10 h-12 w-full opacity-0 transition-opacity duration-300 ease-in-out will-change-[opacity]\"\n    ></div>\n    <slot></slot>\n    <div\n      v-if=\"showShadowBottom\"\n      :class=\"{\n        'opacity-100': !isAtTop && !isAtBottom,\n        'border-border border-b': shadowBorder && !isAtTop && !isAtBottom,\n      }\"\n      class=\"scrollbar-bottom-shadow pointer-events-none absolute bottom-0 z-10 h-12 w-full opacity-0 transition-opacity duration-300 ease-in-out will-change-[opacity]\"\n    ></div>\n    <ScrollBar\n      v-if=\"horizontal\"\n      :class=\"scrollBarClass\"\n      orientation=\"horizontal\"\n    />\n  </ScrollArea>\n</template>\n\n<style scoped>\n.vben-scrollbar {\n  &:not(.both-shadow).left-shadow {\n    mask-image: linear-gradient(90deg, transparent, #000 16px);\n  }\n\n  &:not(.both-shadow).right-shadow {\n    mask-image: linear-gradient(\n      90deg,\n      #000 0%,\n      #000 calc(100% - 16px),\n      transparent\n    );\n  }\n\n  &.both-shadow {\n    mask-image: linear-gradient(\n      90deg,\n      transparent,\n      #000 16px,\n      #000 calc(100% - 16px),\n      transparent 100%\n    );\n  }\n}\n\n.scrollbar-top-shadow {\n  background: linear-gradient(\n    to bottom,\n    hsl(var(--scroll-shadow, var(--background))),\n    transparent\n  );\n}\n\n.scrollbar-bottom-shadow {\n  background: linear-gradient(\n    to top,\n    hsl(var(--scroll-shadow, var(--background))),\n    transparent\n  );\n}\n</style>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/components/segmented/index.ts",
    "content": "export { default as VbenSegmented } from './segmented.vue';\n\nexport type * from './types';\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/components/segmented/segmented.vue",
    "content": "<script setup lang=\"ts\">\nimport type { SegmentedItem } from './types';\n\nimport { computed } from 'vue';\n\nimport { TabsTrigger } from 'radix-vue';\n\nimport { Tabs, TabsContent, TabsList } from '../../ui';\nimport TabsIndicator from './tabs-indicator.vue';\n\ninterface Props {\n  defaultValue?: string;\n  tabs?: SegmentedItem[];\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n  defaultValue: '',\n  tabs: () => [],\n});\n\nconst activeTab = defineModel<string>();\n\nconst getDefaultValue = computed(() => {\n  return props.defaultValue || props.tabs[0]?.value;\n});\n\nconst tabsStyle = computed(() => {\n  return {\n    'grid-template-columns': `repeat(${props.tabs.length}, minmax(0, 1fr))`,\n  };\n});\n\nconst tabsIndicatorStyle = computed(() => {\n  return {\n    width: `${(100 / props.tabs.length).toFixed(0)}%`,\n  };\n});\n\nfunction activeClass(tab: string): string[] {\n  return tab === activeTab.value ? ['!font-bold', 'text-primary'] : [];\n}\n</script>\n\n<template>\n  <Tabs v-model=\"activeTab\" :default-value=\"getDefaultValue\">\n    <TabsList\n      :style=\"tabsStyle\"\n      class=\"bg-accent !outline-heavy relative grid w-full !outline !outline-2\"\n    >\n      <TabsIndicator :style=\"tabsIndicatorStyle\" />\n      <template v-for=\"tab in tabs\" :key=\"tab.value\">\n        <TabsTrigger\n          :value=\"tab.value\"\n          :class=\"activeClass(tab.value)\"\n          class=\"hover:text-primary z-20 inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium disabled:pointer-events-none disabled:opacity-50\"\n        >\n          {{ tab.label }}\n        </TabsTrigger>\n      </template>\n    </TabsList>\n    <template v-for=\"tab in tabs\" :key=\"tab.value\">\n      <TabsContent :value=\"tab.value\">\n        <slot :name=\"tab.value\"></slot>\n      </TabsContent>\n    </template>\n  </Tabs>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/components/segmented/tabs-indicator.vue",
    "content": "<script setup lang=\"ts\">\nimport type { TabsIndicatorProps } from 'radix-vue';\n\nimport { cn } from '@vben-core/shared/utils';\nimport { TabsIndicator, useForwardProps } from 'radix-vue';\nimport { computed } from 'vue';\n\nconst props = defineProps<{ class?: any } & TabsIndicatorProps>();\n\nconst delegatedProps = computed(() => {\n  const { class: _, ...delegated } = props;\n\n  return delegated;\n});\n\nconst forwardedProps = useForwardProps(delegatedProps);\n</script>\n\n<template>\n  <TabsIndicator\n    v-bind=\"forwardedProps\"\n    :class=\"\n      cn(\n        'absolute bottom-0 left-0 z-10 h-full w-1/2 translate-x-[--radix-tabs-indicator-position] rounded-full px-0 py-1 pr-0.5 transition-[width,transform] duration-300',\n        props.class,\n      )\n    \"\n  >\n    <div\n      class=\"bg-background text-foreground inline-flex h-full w-full items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50\"\n    >\n      <slot></slot>\n    </div>\n  </TabsIndicator>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/components/segmented/types.ts",
    "content": "interface SegmentedItem {\n  label: string;\n  value: string;\n}\n\nexport type { SegmentedItem };\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/components/select/index.ts",
    "content": "export { default as VbenSelect } from './select.vue';\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/components/select/select.vue",
    "content": "<script lang=\"ts\" setup>\nimport { CircleX } from '@vben-core/icons';\n\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from '../../ui';\n\ninterface Props {\n  allowClear?: boolean;\n  class?: any;\n  // 弹出层的类名\n  contentClass?: any;\n  options?: Array<{ label: string; value: string }>;\n  placeholder?: string;\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n  allowClear: false,\n});\n\nconst modelValue = defineModel<string>();\n\nfunction handleClear() {\n  modelValue.value = undefined;\n}\n</script>\n<template>\n  <Select v-model=\"modelValue\">\n    <SelectTrigger :class=\"props.class\" class=\"flex w-full items-center\">\n      <SelectValue class=\"flex-auto text-left\" :placeholder=\"placeholder\" />\n      <CircleX\n        @pointerdown.stop\n        @click.stop.prevent=\"handleClear\"\n        v-if=\"allowClear && modelValue\"\n        data-clear-button\n        class=\"mr-1 size-4 cursor-pointer opacity-50 hover:opacity-100\"\n      />\n    </SelectTrigger>\n    <SelectContent :class=\"props.contentClass\">\n      <template v-for=\"item in options\" :key=\"item.value\">\n        <SelectItem :value=\"item.value\"> {{ item.label }} </SelectItem>\n      </template>\n    </SelectContent>\n  </Select>\n</template>\n\n<style lang=\"scss\" scoped>\nbutton[role='combobox'][data-placeholder] {\n  color: hsl(var(--muted-foreground));\n}\n\nbutton {\n  --ring: var(--primary);\n}\n</style>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/components/spine-text/index.ts",
    "content": "export { default as VbenSpineText } from './spine-text.vue';\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/components/spine-text/spine-text.vue",
    "content": "<script lang=\"ts\" setup>\nimport { computed } from 'vue';\n\nconst { animationDuration = 2, animationIterationCount = 'infinite' } =\n  defineProps<{\n    // 动画持续时间，单位秒\n    animationDuration?: number;\n    // 动画是否只执行一次\n    animationIterationCount?: 'infinite' | number;\n  }>();\n\nconst style = computed(() => {\n  return {\n    animation: `shine ${animationDuration}s linear ${animationIterationCount}`,\n  };\n});\n</script>\n<template>\n  <div :style=\"style\" class=\"vben-spine-text !bg-clip-text text-transparent\">\n    <slot></slot>\n  </div>\n</template>\n<style>\n.vben-spine-text {\n  background:\n    radial-gradient(circle at center, rgb(255 255 255 / 80%), #f000) -200% 50% /\n      200% 100% no-repeat,\n    #000;\n\n  /* animation: shine 3s linear infinite; */\n}\n\n.dark .vben-spine-text {\n  background:\n    radial-gradient(circle at center, rgb(24 24 26 / 80%), transparent) -200% 50% /\n      200% 100% no-repeat,\n    #f4f4f4;\n}\n\n@keyframes shine {\n  0% {\n    background-position: 200% 0%;\n  }\n\n  100% {\n    background-position: -200% 0%;\n  }\n}\n</style>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/components/spinner/index.ts",
    "content": "export { default as VbenLoading } from './loading.vue';\nexport { default as VbenSpinner } from './spinner.vue';\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/components/spinner/loading.vue",
    "content": "<script lang=\"ts\" setup>\nimport { ref, watch } from 'vue';\n\nimport { cn } from '@vben-core/shared/utils';\n\ninterface Props {\n  class?: string;\n  /**\n   * @zh_CN 最小加载时间\n   * @en_US Minimum loading time\n   */\n  minLoadingTime?: number;\n\n  /**\n   * @zh_CN loading状态开启\n   */\n  spinning?: boolean;\n  /**\n   * @zh_CN 文字\n   */\n  text?: string;\n}\n\ndefineOptions({\n  name: 'VbenLoading',\n});\n\nconst props = withDefaults(defineProps<Props>(), {\n  minLoadingTime: 50,\n  text: '',\n});\n// const startTime = ref(0);\nconst showSpinner = ref(false);\nconst renderSpinner = ref(false);\nconst timer = ref<ReturnType<typeof setTimeout>>();\n\nwatch(\n  () => props.spinning,\n  (show) => {\n    if (!show) {\n      showSpinner.value = false;\n      clearTimeout(timer.value);\n      return;\n    }\n\n    // startTime.value = performance.now();\n    timer.value = setTimeout(() => {\n      // const loadingTime = performance.now() - startTime.value;\n\n      showSpinner.value = true;\n      if (showSpinner.value) {\n        renderSpinner.value = true;\n      }\n    }, props.minLoadingTime);\n  },\n  {\n    immediate: true,\n  },\n);\n\nfunction onTransitionEnd() {\n  if (!showSpinner.value) {\n    renderSpinner.value = false;\n  }\n}\n</script>\n\n<template>\n  <div\n    :class=\"\n      cn(\n        'z-100 dark:bg-overlay bg-overlay-content absolute left-0 top-0 flex size-full flex-col items-center justify-center transition-all duration-500',\n        {\n          'invisible opacity-0': !showSpinner,\n        },\n        props.class,\n      )\n    \"\n    @transitionend=\"onTransitionEnd\"\n  >\n    <slot name=\"icon\" v-if=\"renderSpinner\">\n      <span class=\"dot relative inline-block size-9 text-3xl\">\n        <i\n          v-for=\"index in 4\"\n          :key=\"index\"\n          class=\"bg-primary absolute block size-4 origin-[50%_50%] scale-75 rounded-full opacity-30\"\n        ></i>\n      </span>\n    </slot>\n\n    <div v-if=\"text\" class=\"text-primary mt-4 text-xs\">{{ text }}</div>\n    <slot></slot>\n  </div>\n</template>\n\n<style scoped>\n.dot {\n  transform: rotate(45deg);\n  animation: rotate-ani 1.2s infinite linear;\n}\n\n.dot i {\n  animation: spin-move-ani 1s infinite linear alternate;\n}\n\n.dot i:nth-child(1) {\n  top: 0;\n  left: 0;\n}\n\n.dot i:nth-child(2) {\n  top: 0;\n  right: 0;\n  animation-delay: 0.4s;\n}\n\n.dot i:nth-child(3) {\n  right: 0;\n  bottom: 0;\n  animation-delay: 0.8s;\n}\n\n.dot i:nth-child(4) {\n  bottom: 0;\n  left: 0;\n  animation-delay: 1.2s;\n}\n\n@keyframes rotate-ani {\n  to {\n    transform: rotate(405deg);\n  }\n}\n\n@keyframes spin-move-ani {\n  to {\n    opacity: 1;\n  }\n}\n</style>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/components/spinner/spinner.vue",
    "content": "<script lang=\"ts\" setup>\nimport { ref, watch } from 'vue';\n\nimport { cn } from '@vben-core/shared/utils';\n\ninterface Props {\n  class?: string;\n  /**\n   * @zh_CN 最小加载时间\n   * @en_US Minimum loading time\n   */\n  minLoadingTime?: number;\n  /**\n   * @zh_CN loading状态开启\n   */\n  spinning?: boolean;\n}\n\ndefineOptions({\n  name: 'VbenSpinner',\n});\n\nconst props = withDefaults(defineProps<Props>(), {\n  minLoadingTime: 50,\n});\n// const startTime = ref(0);\nconst showSpinner = ref(false);\nconst renderSpinner = ref(false);\nconst timer = ref<ReturnType<typeof setTimeout>>();\n\nwatch(\n  () => props.spinning,\n  (show) => {\n    if (!show) {\n      showSpinner.value = false;\n      clearTimeout(timer.value);\n      return;\n    }\n\n    // startTime.value = performance.now();\n    timer.value = setTimeout(() => {\n      // const loadingTime = performance.now() - startTime.value;\n\n      showSpinner.value = true;\n      if (showSpinner.value) {\n        renderSpinner.value = true;\n      }\n    }, props.minLoadingTime);\n  },\n  {\n    immediate: true,\n  },\n);\n\nfunction onTransitionEnd() {\n  if (!showSpinner.value) {\n    renderSpinner.value = false;\n  }\n}\n</script>\n\n<template>\n  <div\n    :class=\"\n      cn(\n        'flex-center z-100 bg-overlay-content absolute left-0 top-0 size-full backdrop-blur-sm transition-all duration-500',\n        {\n          'invisible opacity-0': !showSpinner,\n        },\n        props.class,\n      )\n    \"\n    @transitionend=\"onTransitionEnd\"\n  >\n    <div\n      :class=\"{ paused: !renderSpinner }\"\n      v-if=\"renderSpinner\"\n      class=\"loader before:bg-primary/50 after:bg-primary relative size-12 before:absolute before:left-0 before:top-[60px] before:h-[5px] before:w-12 before:rounded-[50%] before:content-[''] after:absolute after:left-0 after:top-0 after:h-full after:w-full after:rounded after:content-['']\"\n    ></div>\n  </div>\n</template>\n\n<style scoped>\n.paused {\n  &::before {\n    animation-play-state: paused !important;\n  }\n\n  &::after {\n    animation-play-state: paused !important;\n  }\n}\n\n.loader {\n  &::before {\n    animation: loader-shadow-ani 0.5s linear infinite;\n  }\n\n  &::after {\n    animation: loader-jump-ani 0.5s linear infinite;\n  }\n}\n\n@keyframes loader-jump-ani {\n  15% {\n    border-bottom-right-radius: 3px;\n  }\n\n  25% {\n    transform: translateY(9px) rotate(22.5deg);\n  }\n\n  50% {\n    border-bottom-right-radius: 40px;\n    transform: translateY(18px) scale(1, 0.9) rotate(45deg);\n  }\n\n  75% {\n    transform: translateY(9px) rotate(67.5deg);\n  }\n\n  100% {\n    transform: translateY(0) rotate(90deg);\n  }\n}\n\n@keyframes loader-shadow-ani {\n  0%,\n  100% {\n    transform: scale(1, 1);\n  }\n\n  50% {\n    transform: scale(1.2, 1);\n  }\n}\n</style>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/components/tooltip/help-tooltip.vue",
    "content": "<script setup lang=\"ts\">\nimport { cn } from '@vben-core/shared/utils';\n\nimport { CircleHelp } from 'lucide-vue-next';\n\nimport Tooltip from './tooltip.vue';\n\ndefineOptions({\n  inheritAttrs: false,\n});\n\ndefineProps<{ triggerClass?: string }>();\n</script>\n\n<template>\n  <Tooltip :delay-duration=\"300\" side=\"top\">\n    <template #trigger>\n      <slot name=\"trigger\">\n        <CircleHelp\n          :class=\"\n            cn(\n              'text-foreground/80 hover:text-foreground inline-flex size-5 cursor-pointer',\n              triggerClass,\n            )\n          \"\n        />\n      </slot>\n    </template>\n    <slot></slot>\n  </Tooltip>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/components/tooltip/index.ts",
    "content": "export { default as VbenHelpTooltip } from './help-tooltip.vue';\nexport { default as VbenTooltip } from './tooltip.vue';\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/components/tooltip/tooltip.vue",
    "content": "<script setup lang=\"ts\">\nimport type { ClassType } from '@vben-core/typings';\nimport type { TooltipContentProps } from 'radix-vue';\nimport type { StyleValue } from 'vue';\n\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipProvider,\n  TooltipTrigger,\n} from '../../ui';\n\ninterface Props {\n  contentClass?: ClassType;\n  contentStyle?: StyleValue;\n  delayDuration?: number;\n  side?: TooltipContentProps['side'];\n}\n\nwithDefaults(defineProps<Props>(), {\n  delayDuration: 0,\n  side: 'right',\n});\n</script>\n\n<template>\n  <TooltipProvider :delay-duration=\"delayDuration\">\n    <Tooltip>\n      <TooltipTrigger as-child tabindex=\"-1\">\n        <slot name=\"trigger\"></slot>\n      </TooltipTrigger>\n      <TooltipContent\n        :class=\"contentClass\"\n        :side=\"side\"\n        :style=\"contentStyle\"\n        class=\"side-content text-popover-foreground bg-accent rounded-md\"\n      >\n        <slot></slot>\n      </TooltipContent>\n    </Tooltip>\n  </TooltipProvider>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/index.ts",
    "content": "export * from './components';\nexport * from './ui';\nexport { createContext, Slot, VisuallyHidden } from 'radix-vue';\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/accordion/Accordion.vue",
    "content": "<script setup lang=\"ts\">\nimport type { AccordionRootEmits, AccordionRootProps } from 'radix-vue';\n\nimport { AccordionRoot, useForwardPropsEmits } from 'radix-vue';\n\nconst props = defineProps<AccordionRootProps>();\nconst emits = defineEmits<AccordionRootEmits>();\n\nconst forwarded = useForwardPropsEmits(props, emits);\n</script>\n\n<template>\n  <AccordionRoot v-bind=\"forwarded\">\n    <slot></slot>\n  </AccordionRoot>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/accordion/AccordionContent.vue",
    "content": "<script setup lang=\"ts\">\nimport type { AccordionContentProps } from 'radix-vue';\n\nimport { cn } from '@vben-core/shared/utils';\nimport { AccordionContent } from 'radix-vue';\nimport { computed } from 'vue';\n\nconst props = defineProps<{ class?: any } & AccordionContentProps>();\n\nconst delegatedProps = computed(() => {\n  const { class: _, ...delegated } = props;\n\n  return delegated;\n});\n</script>\n\n<template>\n  <AccordionContent\n    v-bind=\"delegatedProps\"\n    class=\"data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm\"\n  >\n    <div :class=\"cn('pb-4 pt-0', props.class)\">\n      <slot></slot>\n    </div>\n  </AccordionContent>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/accordion/AccordionItem.vue",
    "content": "<script setup lang=\"ts\">\nimport type { AccordionItemProps } from 'radix-vue';\n\nimport { cn } from '@vben-core/shared/utils';\nimport { AccordionItem, useForwardProps } from 'radix-vue';\nimport { computed } from 'vue';\n\nconst props = defineProps<{ class?: any } & AccordionItemProps>();\n\nconst delegatedProps = computed(() => {\n  const { class: _, ...delegated } = props;\n\n  return delegated;\n});\n\nconst forwardedProps = useForwardProps(delegatedProps);\n</script>\n\n<template>\n  <AccordionItem v-bind=\"forwardedProps\" :class=\"cn('border-b', props.class)\">\n    <slot></slot>\n  </AccordionItem>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/accordion/AccordionTrigger.vue",
    "content": "<script setup lang=\"ts\">\nimport type { AccordionTriggerProps } from 'radix-vue';\n\nimport { cn } from '@vben-core/shared/utils';\nimport { ChevronDown } from 'lucide-vue-next';\nimport { AccordionHeader, AccordionTrigger } from 'radix-vue';\nimport { computed } from 'vue';\n\nconst props = defineProps<{ class?: any } & AccordionTriggerProps>();\n\nconst delegatedProps = computed(() => {\n  const { class: _, ...delegated } = props;\n\n  return delegated;\n});\n</script>\n\n<template>\n  <AccordionHeader class=\"flex\">\n    <AccordionTrigger\n      v-bind=\"delegatedProps\"\n      :class=\"\n        cn(\n          'flex flex-1 items-center justify-between py-4 text-sm font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180',\n          props.class,\n        )\n      \"\n    >\n      <slot></slot>\n      <slot name=\"icon\">\n        <ChevronDown\n          class=\"text-muted-foreground h-4 w-4 shrink-0 transition-transform duration-200\"\n        />\n      </slot>\n    </AccordionTrigger>\n  </AccordionHeader>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/accordion/index.ts",
    "content": "export { default as Accordion } from './Accordion.vue';\nexport { default as AccordionContent } from './AccordionContent.vue';\nexport { default as AccordionItem } from './AccordionItem.vue';\nexport { default as AccordionTrigger } from './AccordionTrigger.vue';\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/alert-dialog/AlertDialog.vue",
    "content": "<script setup lang=\"ts\">\nimport type { AlertDialogEmits, AlertDialogProps } from 'radix-vue';\n\nimport { AlertDialogRoot, useForwardPropsEmits } from 'radix-vue';\n\nconst props = defineProps<AlertDialogProps>();\nconst emits = defineEmits<AlertDialogEmits>();\n\nconst forwarded = useForwardPropsEmits(props, emits);\n</script>\n\n<template>\n  <AlertDialogRoot v-bind=\"forwarded\">\n    <slot></slot>\n  </AlertDialogRoot>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/alert-dialog/AlertDialogAction.vue",
    "content": "<script setup lang=\"ts\">\nimport type { AlertDialogActionProps } from 'radix-vue';\n\nimport { AlertDialogAction } from 'radix-vue';\n\nconst props = defineProps<AlertDialogActionProps>();\n</script>\n\n<template>\n  <AlertDialogAction v-bind=\"props\">\n    <slot></slot>\n  </AlertDialogAction>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/alert-dialog/AlertDialogCancel.vue",
    "content": "<script setup lang=\"ts\">\nimport type { AlertDialogCancelProps } from 'radix-vue';\n\nimport { AlertDialogCancel } from 'radix-vue';\n\nconst props = defineProps<AlertDialogCancelProps>();\n</script>\n\n<template>\n  <AlertDialogCancel v-bind=\"props\">\n    <slot></slot>\n  </AlertDialogCancel>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/alert-dialog/AlertDialogContent.vue",
    "content": "<script setup lang=\"ts\">\nimport type {\n  AlertDialogContentEmits,\n  AlertDialogContentProps,\n} from 'radix-vue';\n\nimport type { ClassType } from '@vben-core/typings';\n\nimport { computed, ref } from 'vue';\n\nimport { cn } from '@vben-core/shared/utils';\n\nimport {\n  AlertDialogContent,\n  AlertDialogPortal,\n  useForwardPropsEmits,\n} from 'radix-vue';\n\nimport AlertDialogOverlay from './AlertDialogOverlay.vue';\n\nconst props = withDefaults(\n  defineProps<\n    AlertDialogContentProps & {\n      centered?: boolean;\n      class?: ClassType;\n      modal?: boolean;\n      open?: boolean;\n      overlayBlur?: number;\n      zIndex?: number;\n    }\n  >(),\n  { modal: true },\n);\nconst emits = defineEmits<\n  AlertDialogContentEmits & { close: []; closed: []; opened: [] }\n>();\n\nconst delegatedProps = computed(() => {\n  const { class: _, modal: _modal, open: _open, ...delegated } = props;\n\n  return delegated;\n});\n\nconst forwarded = useForwardPropsEmits(delegatedProps, emits);\n\nconst contentRef = ref<InstanceType<typeof AlertDialogContent> | null>(null);\nfunction onAnimationEnd(event: AnimationEvent) {\n  // 只有在 contentRef 的动画结束时才触发 opened/closed 事件\n  if (event.target === contentRef.value?.$el) {\n    if (props.open) {\n      emits('opened');\n    } else {\n      emits('closed');\n    }\n  }\n}\ndefineExpose({\n  getContentRef: () => contentRef.value,\n});\n</script>\n\n<template>\n  <AlertDialogPortal>\n    <Transition name=\"fade\" appear>\n      <AlertDialogOverlay\n        v-if=\"open && modal\"\n        :style=\"{\n          ...(zIndex ? { zIndex } : {}),\n          position: 'fixed',\n          backdropFilter:\n            overlayBlur && overlayBlur > 0 ? `blur(${overlayBlur}px)` : 'none',\n        }\"\n        @click=\"() => emits('close')\"\n      />\n    </Transition>\n    <AlertDialogContent\n      ref=\"contentRef\"\n      :style=\"{ ...(zIndex ? { zIndex } : {}), position: 'fixed' }\"\n      @animationend=\"onAnimationEnd\"\n      v-bind=\"forwarded\"\n      :class=\"\n        cn(\n          'z-popup bg-background p-6 shadow-lg outline-none sm:rounded-xl',\n          'data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95',\n          'data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95',\n          {\n            'data-[state=open]:slide-in-from-top-[48%] data-[state=closed]:slide-out-to-top-[48%]':\n              !centered,\n            'data-[state=open]:slide-in-from-top-[98%] data-[state=closed]:slide-out-to-top-[148%]':\n              centered,\n            'top-[10vh]': !centered,\n            'top-1/2 -translate-y-1/2': centered,\n          },\n          props.class,\n        )\n      \"\n    >\n      <slot></slot>\n    </AlertDialogContent>\n  </AlertDialogPortal>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/alert-dialog/AlertDialogDescription.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { AlertDialogDescriptionProps } from 'radix-vue';\n\nimport { computed } from 'vue';\n\nimport { cn } from '@vben-core/shared/utils';\n\nimport { AlertDialogDescription, useForwardProps } from 'radix-vue';\n\nconst props = defineProps<AlertDialogDescriptionProps & { class?: any }>();\n\nconst delegatedProps = computed(() => {\n  const { class: _, ...delegated } = props;\n\n  return delegated;\n});\n\nconst forwardedProps = useForwardProps(delegatedProps);\n</script>\n\n<template>\n  <AlertDialogDescription\n    v-bind=\"forwardedProps\"\n    :class=\"cn('text-muted-foreground text-sm', props.class)\"\n  >\n    <slot></slot>\n  </AlertDialogDescription>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/alert-dialog/AlertDialogOverlay.vue",
    "content": "<script setup lang=\"ts\">\nimport { useScrollLock } from '@vben-core/composables';\n\nuseScrollLock();\n</script>\n<template>\n  <div class=\"bg-overlay z-popup inset-0\"></div>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/alert-dialog/AlertDialogTitle.vue",
    "content": "<script setup lang=\"ts\">\nimport type { AlertDialogTitleProps } from 'radix-vue';\n\nimport { computed } from 'vue';\n\nimport { cn } from '@vben-core/shared/utils';\n\nimport { AlertDialogTitle, useForwardProps } from 'radix-vue';\n\nconst props = defineProps<AlertDialogTitleProps & { class?: any }>();\n\nconst delegatedProps = computed(() => {\n  const { class: _, ...delegated } = props;\n\n  return delegated;\n});\n\nconst forwardedProps = useForwardProps(delegatedProps);\n</script>\n\n<template>\n  <AlertDialogTitle\n    v-bind=\"forwardedProps\"\n    :class=\"\n      cn('text-lg font-semibold leading-none tracking-tight', props.class)\n    \"\n  >\n    <slot></slot>\n  </AlertDialogTitle>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/alert-dialog/index.ts",
    "content": "export { default as AlertDialog } from './AlertDialog.vue';\nexport { default as AlertDialogAction } from './AlertDialogAction.vue';\nexport { default as AlertDialogCancel } from './AlertDialogCancel.vue';\nexport { default as AlertDialogContent } from './AlertDialogContent.vue';\nexport { default as AlertDialogDescription } from './AlertDialogDescription.vue';\nexport { default as AlertDialogTitle } from './AlertDialogTitle.vue';\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/avatar/Avatar.vue",
    "content": "<script setup lang=\"ts\">\nimport type { AvatarVariants } from './avatar';\n\nimport { cn } from '@vben-core/shared/utils';\nimport { AvatarRoot } from 'radix-vue';\n\nimport { avatarVariant } from './avatar';\n\nconst props = withDefaults(\n  defineProps<{\n    class?: any;\n    shape?: AvatarVariants['shape'];\n    size?: AvatarVariants['size'];\n  }>(),\n  {\n    shape: 'circle',\n    size: 'sm',\n  },\n);\n</script>\n\n<template>\n  <AvatarRoot :class=\"cn(avatarVariant({ size, shape }), props.class)\">\n    <slot></slot>\n  </AvatarRoot>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/avatar/AvatarFallback.vue",
    "content": "<script setup lang=\"ts\">\nimport type { AvatarFallbackProps } from 'radix-vue';\n\nimport { AvatarFallback } from 'radix-vue';\n\nconst props = defineProps<AvatarFallbackProps>();\n</script>\n\n<template>\n  <AvatarFallback v-bind=\"props\">\n    <slot></slot>\n  </AvatarFallback>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/avatar/AvatarImage.vue",
    "content": "<script setup lang=\"ts\">\nimport type { AvatarImageProps } from 'radix-vue';\n\nimport { AvatarImage } from 'radix-vue';\n\nconst props = defineProps<AvatarImageProps>();\n</script>\n\n<template>\n  <AvatarImage v-bind=\"props\" class=\"h-full w-full object-cover\" />\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/avatar/avatar.ts",
    "content": "import type { VariantProps } from 'class-variance-authority';\n\nimport { cva } from 'class-variance-authority';\n\nexport const avatarVariant = cva(\n  'inline-flex items-center justify-center font-normal text-foreground select-none shrink-0 bg-secondary overflow-hidden',\n  {\n    variants: {\n      shape: {\n        circle: 'rounded-full',\n        square: 'rounded-md',\n      },\n      size: {\n        base: 'h-16 w-16 text-2xl',\n        lg: 'h-32 w-32 text-5xl',\n        sm: 'h-10 w-10 text-xs',\n      },\n    },\n  },\n);\n\nexport type AvatarVariants = VariantProps<typeof avatarVariant>;\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/avatar/index.ts",
    "content": "export * from './avatar';\nexport { default as Avatar } from './Avatar.vue';\nexport { default as AvatarFallback } from './AvatarFallback.vue';\nexport { default as AvatarImage } from './AvatarImage.vue';\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/badge/Badge.vue",
    "content": "<script setup lang=\"ts\">\nimport type { BadgeVariants } from './badge';\n\nimport { cn } from '@vben-core/shared/utils';\n\nimport { badgeVariants } from './badge';\n\nconst props = defineProps<{\n  class?: any;\n  variant?: BadgeVariants['variant'];\n}>();\n</script>\n\n<template>\n  <div :class=\"cn(badgeVariants({ variant }), props.class)\">\n    <slot></slot>\n  </div>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/badge/badge.ts",
    "content": "import type { VariantProps } from 'class-variance-authority';\n\nimport { cva } from 'class-variance-authority';\n\nexport const badgeVariants = cva(\n  'inline-flex items-center rounded-md border border-border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',\n  {\n    defaultVariants: {\n      variant: 'default',\n    },\n    variants: {\n      variant: {\n        default:\n          'border-transparent bg-accent hover:bg-accent text-primary-foreground shadow',\n        destructive:\n          'border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive-hover',\n        outline: 'text-foreground',\n        secondary:\n          'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',\n      },\n    },\n  },\n);\n\nexport type BadgeVariants = VariantProps<typeof badgeVariants>;\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/badge/index.ts",
    "content": "export * from './badge';\n\nexport { default as Badge } from './Badge.vue';\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/breadcrumb/Breadcrumb.vue",
    "content": "<script lang=\"ts\" setup>\nconst props = defineProps<{\n  class?: any;\n}>();\n</script>\n\n<template>\n  <nav :class=\"props.class\" aria-label=\"breadcrumb\" role=\"navigation\">\n    <slot></slot>\n  </nav>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/breadcrumb/BreadcrumbEllipsis.vue",
    "content": "<script lang=\"ts\" setup>\nimport { cn } from '@vben-core/shared/utils';\n\nimport { MoreHorizontal } from 'lucide-vue-next';\n\nconst props = defineProps<{\n  class?: any;\n}>();\n</script>\n\n<template>\n  <span\n    :class=\"cn('flex h-9 w-9 items-center justify-center', props.class)\"\n    aria-hidden=\"true\"\n    role=\"presentation\"\n  >\n    <slot>\n      <MoreHorizontal class=\"h-4 w-4\" />\n    </slot>\n    <span class=\"sr-only\">More</span>\n  </span>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/breadcrumb/BreadcrumbItem.vue",
    "content": "<script lang=\"ts\" setup>\nimport { cn } from '@vben-core/shared/utils';\n\nconst props = defineProps<{\n  class?: any;\n}>();\n</script>\n\n<template>\n  <li\n    :class=\"\n      cn('hover:text-foreground inline-flex items-center gap-1.5', props.class)\n    \"\n  >\n    <slot></slot>\n  </li>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/breadcrumb/BreadcrumbLink.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { PrimitiveProps } from 'radix-vue';\n\nimport { cn } from '@vben-core/shared/utils';\nimport { Primitive } from 'radix-vue';\n\nconst props = withDefaults(defineProps<{ class?: any } & PrimitiveProps>(), {\n  as: 'a',\n});\n</script>\n\n<template>\n  <Primitive\n    :as=\"as\"\n    :as-child=\"asChild\"\n    :class=\"cn('hover:text-foreground transition-colors', props.class)\"\n  >\n    <slot></slot>\n  </Primitive>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/breadcrumb/BreadcrumbList.vue",
    "content": "<script lang=\"ts\" setup>\nimport { cn } from '@vben-core/shared/utils';\n\nconst props = defineProps<{\n  class?: any;\n}>();\n</script>\n\n<template>\n  <ol\n    :class=\"\n      cn(\n        'text-muted-foreground flex flex-wrap items-center gap-1.5 break-words text-sm sm:gap-2.5',\n        props.class,\n      )\n    \"\n  >\n    <slot></slot>\n  </ol>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/breadcrumb/BreadcrumbPage.vue",
    "content": "<script lang=\"ts\" setup>\nimport { cn } from '@vben-core/shared/utils';\n\nconst props = defineProps<{\n  class?: any;\n}>();\n</script>\n\n<template>\n  <span\n    :class=\"cn('text-foreground font-normal', props.class)\"\n    aria-current=\"page\"\n    aria-disabled=\"true\"\n    role=\"link\"\n  >\n    <slot></slot>\n  </span>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/breadcrumb/BreadcrumbSeparator.vue",
    "content": "<script lang=\"ts\" setup>\nimport { cn } from '@vben-core/shared/utils';\n\nimport { ChevronRight } from 'lucide-vue-next';\n\nconst props = defineProps<{\n  class?: any;\n}>();\n</script>\n\n<template>\n  <li\n    :class=\"cn('[&>svg]:size-3.5', props.class)\"\n    aria-hidden=\"true\"\n    role=\"presentation\"\n  >\n    <slot>\n      <ChevronRight />\n    </slot>\n  </li>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/breadcrumb/index.ts",
    "content": "export { default as Breadcrumb } from './Breadcrumb.vue';\nexport { default as BreadcrumbEllipsis } from './BreadcrumbEllipsis.vue';\nexport { default as BreadcrumbItem } from './BreadcrumbItem.vue';\nexport { default as BreadcrumbLink } from './BreadcrumbLink.vue';\nexport { default as BreadcrumbList } from './BreadcrumbList.vue';\nexport { default as BreadcrumbPage } from './BreadcrumbPage.vue';\nexport { default as BreadcrumbSeparator } from './BreadcrumbSeparator.vue';\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/button/Button.vue",
    "content": "<script setup lang=\"ts\">\nimport type { PrimitiveProps } from 'radix-vue';\n\nimport type { ButtonVariants, ButtonVariantSize } from './types';\n\nimport { cn } from '@vben-core/shared/utils';\nimport { Primitive } from 'radix-vue';\n\nimport { buttonVariants } from './button';\n\ninterface Props extends PrimitiveProps {\n  class?: any;\n  size?: ButtonVariantSize;\n  variant?: ButtonVariants;\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n  as: 'button',\n  class: '',\n});\n</script>\n\n<template>\n  <Primitive\n    :as=\"as\"\n    :as-child=\"asChild\"\n    :class=\"cn(buttonVariants({ variant, size }), props.class)\"\n  >\n    <slot></slot>\n  </Primitive>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/button/button.ts",
    "content": "import { cva } from 'class-variance-authority';\n\nexport const buttonVariants = cva(\n  'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:cursor-not-allowed  disabled:opacity-50',\n  {\n    defaultVariants: {\n      size: 'default',\n      variant: 'default',\n    },\n    variants: {\n      size: {\n        default: 'h-9 px-4 py-2',\n        icon: 'h-8 w-8 rounded-sm px-1 text-lg',\n        lg: 'h-10 rounded-md px-4',\n        sm: 'h-8 rounded-md px-2 text-xs',\n        xs: 'h-8 w-8 rounded-sm px-1 text-xs',\n      },\n      variant: {\n        default:\n          'bg-primary text-primary-foreground shadow hover:bg-primary/90',\n        destructive:\n          'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive-hover',\n        ghost: 'hover:bg-accent hover:text-accent-foreground',\n        heavy: 'hover:bg-heavy hover:text-heavy-foreground',\n        icon: 'hover:bg-accent hover:text-accent-foreground text-foreground/80',\n        link: 'text-primary underline-offset-4 hover:underline',\n        outline:\n          'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground',\n        secondary:\n          'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',\n      },\n    },\n  },\n);\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/button/index.ts",
    "content": "export * from './button';\n\nexport { default as Button } from './Button.vue';\n\nexport type * from './types';\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/button/types.ts",
    "content": "export type ButtonVariantSize =\n  | 'default'\n  | 'icon'\n  | 'lg'\n  | 'sm'\n  | 'xs'\n  | null\n  | undefined;\n\nexport type ButtonVariants =\n  | 'default'\n  | 'destructive'\n  | 'ghost'\n  | 'heavy'\n  | 'icon'\n  | 'link'\n  | 'outline'\n  | 'secondary'\n  | null\n  | undefined;\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/card/Card.vue",
    "content": "<script setup lang=\"ts\">\nimport { cn } from '@vben-core/shared/utils';\n\nconst props = defineProps<{\n  class?: any;\n}>();\n</script>\n\n<template>\n  <div\n    :class=\"\n      cn(\n        'bg-card text-card-foreground border-border rounded-xl border',\n        props.class,\n      )\n    \"\n  >\n    <slot></slot>\n  </div>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/card/CardContent.vue",
    "content": "<script setup lang=\"ts\">\nimport { cn } from '@vben-core/shared/utils';\n\nconst props = defineProps<{\n  class?: any;\n}>();\n</script>\n\n<template>\n  <div :class=\"cn('p-6 pt-0', props.class)\">\n    <slot></slot>\n  </div>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/card/CardDescription.vue",
    "content": "<script setup lang=\"ts\">\nimport { cn } from '@vben-core/shared/utils';\n\nconst props = defineProps<{\n  class?: any;\n}>();\n</script>\n\n<template>\n  <p :class=\"cn('text-muted-foreground text-sm', props.class)\">\n    <slot></slot>\n  </p>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/card/CardFooter.vue",
    "content": "<script setup lang=\"ts\">\nimport { cn } from '@vben-core/shared/utils';\n\nconst props = defineProps<{\n  class?: any;\n}>();\n</script>\n\n<template>\n  <div :class=\"cn('flex items-center p-6 pt-0', props.class)\">\n    <slot></slot>\n  </div>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/card/CardHeader.vue",
    "content": "<script setup lang=\"ts\">\nimport { cn } from '@vben-core/shared/utils';\n\nconst props = defineProps<{\n  class?: any;\n}>();\n</script>\n\n<template>\n  <div :class=\"cn('flex flex-col gap-y-1.5 p-5', props.class)\">\n    <slot></slot>\n  </div>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/card/CardTitle.vue",
    "content": "<script setup lang=\"ts\">\nimport { cn } from '@vben-core/shared/utils';\n\nconst props = defineProps<{\n  class?: any;\n}>();\n</script>\n\n<template>\n  <h3 :class=\"cn('font-semibold leading-none tracking-tight', props.class)\">\n    <slot></slot>\n  </h3>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/card/index.ts",
    "content": "export { default as Card } from './Card.vue';\nexport { default as CardContent } from './CardContent.vue';\nexport { default as CardDescription } from './CardDescription.vue';\nexport { default as CardFooter } from './CardFooter.vue';\nexport { default as CardHeader } from './CardHeader.vue';\nexport { default as CardTitle } from './CardTitle.vue';\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/checkbox/Checkbox.vue",
    "content": "<script setup lang=\"ts\">\nimport type { CheckboxRootEmits, CheckboxRootProps } from 'radix-vue';\n\nimport { computed } from 'vue';\n\nimport { cn } from '@vben-core/shared/utils';\n\nimport { Check, Minus } from 'lucide-vue-next';\nimport {\n  CheckboxIndicator,\n  CheckboxRoot,\n  useForwardPropsEmits,\n} from 'radix-vue';\n\nconst props = defineProps<\n  CheckboxRootProps & { class?: any; indeterminate?: boolean }\n>();\nconst emits = defineEmits<CheckboxRootEmits>();\n\nconst delegatedProps = computed(() => {\n  const { class: _, ...delegated } = props;\n\n  return delegated;\n});\n\nconst forwarded = useForwardPropsEmits(delegatedProps, emits);\n</script>\n\n<template>\n  <CheckboxRoot\n    v-bind=\"forwarded\"\n    :class=\"\n      cn(\n        'focus-visible:ring-ring data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground border-border peer h-4 w-4 shrink-0 rounded-sm border transition focus-visible:outline-none focus-visible:ring-1 disabled:cursor-not-allowed disabled:opacity-50',\n        props.class,\n      )\n    \"\n  >\n    <CheckboxIndicator\n      class=\"flex h-full w-full items-center justify-center text-current\"\n    >\n      <slot>\n        <component :is=\"indeterminate ? Minus : Check\" class=\"h-4 w-4\" />\n      </slot>\n    </CheckboxIndicator>\n  </CheckboxRoot>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/checkbox/index.ts",
    "content": "export { default as Checkbox } from './Checkbox.vue';\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/context-menu/ContextMenu.vue",
    "content": "<script setup lang=\"ts\">\nimport type { ContextMenuRootEmits, ContextMenuRootProps } from 'radix-vue';\n\nimport { ContextMenuRoot, useForwardPropsEmits } from 'radix-vue';\n\nconst props = withDefaults(defineProps<ContextMenuRootProps>(), {\n  modal: false,\n});\nconst emits = defineEmits<ContextMenuRootEmits>();\n\nconst forwarded = useForwardPropsEmits(props, emits);\n</script>\n\n<template>\n  <ContextMenuRoot v-bind=\"forwarded\">\n    <slot></slot>\n  </ContextMenuRoot>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/context-menu/ContextMenuCheckboxItem.vue",
    "content": "<script setup lang=\"ts\">\nimport type {\n  ContextMenuCheckboxItemEmits,\n  ContextMenuCheckboxItemProps,\n} from 'radix-vue';\n\nimport { cn } from '@vben-core/shared/utils';\nimport { Check } from 'lucide-vue-next';\nimport {\n  ContextMenuCheckboxItem,\n  ContextMenuItemIndicator,\n  useForwardPropsEmits,\n} from 'radix-vue';\nimport { computed } from 'vue';\n\nconst props = defineProps<{ class?: any } & ContextMenuCheckboxItemProps>();\nconst emits = defineEmits<ContextMenuCheckboxItemEmits>();\n\nconst delegatedProps = computed(() => {\n  const { class: _, ...delegated } = props;\n\n  return delegated;\n});\n\nconst forwarded = useForwardPropsEmits(delegatedProps, emits);\n</script>\n\n<template>\n  <ContextMenuCheckboxItem\n    v-bind=\"forwarded\"\n    :class=\"\n      cn(\n        'focus:bg-accent focus:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50',\n        props.class,\n      )\n    \"\n  >\n    <span class=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\">\n      <ContextMenuItemIndicator>\n        <Check class=\"h-4 w-4\" />\n      </ContextMenuItemIndicator>\n    </span>\n    <slot></slot>\n  </ContextMenuCheckboxItem>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/context-menu/ContextMenuContent.vue",
    "content": "<script setup lang=\"ts\">\nimport type {\n  ContextMenuContentEmits,\n  ContextMenuContentProps,\n} from 'radix-vue';\n\nimport { computed } from 'vue';\n\nimport { cn } from '@vben-core/shared/utils';\n\nimport {\n  ContextMenuContent,\n  ContextMenuPortal,\n  useForwardPropsEmits,\n} from 'radix-vue';\n\nconst props = defineProps<ContextMenuContentProps & { class?: any }>();\nconst emits = defineEmits<ContextMenuContentEmits>();\n\nconst delegatedProps = computed(() => {\n  const { class: _, ...delegated } = props;\n\n  return delegated;\n});\n\nconst forwarded = useForwardPropsEmits(delegatedProps, emits);\n</script>\n\n<template>\n  <ContextMenuPortal>\n    <ContextMenuContent\n      v-bind=\"forwarded\"\n      :class=\"\n        cn(\n          'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 border-border z-popup min-w-32 overflow-hidden rounded-md border p-1 shadow-md',\n          props.class,\n        )\n      \"\n    >\n      <slot></slot>\n    </ContextMenuContent>\n  </ContextMenuPortal>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/context-menu/ContextMenuGroup.vue",
    "content": "<script setup lang=\"ts\">\nimport type { ContextMenuGroupProps } from 'radix-vue';\n\nimport { ContextMenuGroup } from 'radix-vue';\n\nconst props = defineProps<ContextMenuGroupProps>();\n</script>\n\n<template>\n  <ContextMenuGroup v-bind=\"props\">\n    <slot></slot>\n  </ContextMenuGroup>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/context-menu/ContextMenuItem.vue",
    "content": "<script setup lang=\"ts\">\nimport type { ContextMenuItemEmits, ContextMenuItemProps } from 'radix-vue';\n\nimport { cn } from '@vben-core/shared/utils';\nimport { ContextMenuItem, useForwardPropsEmits } from 'radix-vue';\nimport { computed } from 'vue';\n\nconst props = defineProps<\n  { class?: any; inset?: boolean } & ContextMenuItemProps\n>();\nconst emits = defineEmits<ContextMenuItemEmits>();\n\nconst delegatedProps = computed(() => {\n  const { class: _, ...delegated } = props;\n\n  return delegated;\n});\n\nconst forwarded = useForwardPropsEmits(delegatedProps, emits);\n</script>\n\n<template>\n  <ContextMenuItem\n    v-bind=\"forwarded\"\n    :class=\"\n      cn(\n        'focus:bg-accent focus:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50',\n        inset && 'pl-8',\n        props.class,\n      )\n    \"\n  >\n    <slot></slot>\n  </ContextMenuItem>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/context-menu/ContextMenuLabel.vue",
    "content": "<script setup lang=\"ts\">\nimport type { ContextMenuLabelProps } from 'radix-vue';\n\nimport { cn } from '@vben-core/shared/utils';\nimport { ContextMenuLabel } from 'radix-vue';\nimport { computed } from 'vue';\n\nconst props = defineProps<\n  { class?: any; inset?: boolean } & ContextMenuLabelProps\n>();\n\nconst delegatedProps = computed(() => {\n  const { class: _, ...delegated } = props;\n\n  return delegated;\n});\n</script>\n\n<template>\n  <ContextMenuLabel\n    v-bind=\"delegatedProps\"\n    :class=\"\n      cn(\n        'text-foreground px-2 py-1.5 text-sm font-semibold',\n        inset && 'pl-8',\n        props.class,\n      )\n    \"\n  >\n    <slot></slot>\n  </ContextMenuLabel>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/context-menu/ContextMenuPortal.vue",
    "content": "<script setup lang=\"ts\">\nimport type { ContextMenuPortalProps } from 'radix-vue';\n\nimport { ContextMenuPortal } from 'radix-vue';\n\nconst props = defineProps<ContextMenuPortalProps>();\n</script>\n\n<template>\n  <ContextMenuPortal v-bind=\"props\">\n    <slot></slot>\n  </ContextMenuPortal>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/context-menu/ContextMenuRadioGroup.vue",
    "content": "<script setup lang=\"ts\">\nimport type {\n  ContextMenuRadioGroupEmits,\n  ContextMenuRadioGroupProps,\n} from 'radix-vue';\n\nimport { ContextMenuRadioGroup, useForwardPropsEmits } from 'radix-vue';\n\nconst props = defineProps<ContextMenuRadioGroupProps>();\nconst emits = defineEmits<ContextMenuRadioGroupEmits>();\n\nconst forwarded = useForwardPropsEmits(props, emits);\n</script>\n\n<template>\n  <ContextMenuRadioGroup v-bind=\"forwarded\">\n    <slot></slot>\n  </ContextMenuRadioGroup>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/context-menu/ContextMenuRadioItem.vue",
    "content": "<script setup lang=\"ts\">\nimport type {\n  ContextMenuRadioItemEmits,\n  ContextMenuRadioItemProps,\n} from 'radix-vue';\n\nimport { cn } from '@vben-core/shared/utils';\nimport { Circle } from 'lucide-vue-next';\nimport {\n  ContextMenuItemIndicator,\n  ContextMenuRadioItem,\n  useForwardPropsEmits,\n} from 'radix-vue';\nimport { computed } from 'vue';\n\nconst props = defineProps<{ class?: any } & ContextMenuRadioItemProps>();\nconst emits = defineEmits<ContextMenuRadioItemEmits>();\n\nconst delegatedProps = computed(() => {\n  const { class: _, ...delegated } = props;\n\n  return delegated;\n});\n\nconst forwarded = useForwardPropsEmits(delegatedProps, emits);\n</script>\n\n<template>\n  <ContextMenuRadioItem\n    v-bind=\"forwarded\"\n    :class=\"\n      cn(\n        'focus:bg-accent focus:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50',\n        props.class,\n      )\n    \"\n  >\n    <span class=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\">\n      <ContextMenuItemIndicator>\n        <Circle class=\"h-2 w-2 fill-current\" />\n      </ContextMenuItemIndicator>\n    </span>\n    <slot></slot>\n  </ContextMenuRadioItem>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/context-menu/ContextMenuSeparator.vue",
    "content": "<script setup lang=\"ts\">\nimport type { ContextMenuSeparatorProps } from 'radix-vue';\n\nimport { cn } from '@vben-core/shared/utils';\nimport { ContextMenuSeparator } from 'radix-vue';\nimport { computed } from 'vue';\n\nconst props = defineProps<{ class?: any } & ContextMenuSeparatorProps>();\n\nconst delegatedProps = computed(() => {\n  const { class: _, ...delegated } = props;\n\n  return delegated;\n});\n</script>\n\n<template>\n  <ContextMenuSeparator\n    v-bind=\"delegatedProps\"\n    :class=\"cn('bg-border -mx-1 my-1 h-px', props.class)\"\n  />\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/context-menu/ContextMenuShortcut.vue",
    "content": "<script setup lang=\"ts\">\nimport { cn } from '@vben-core/shared/utils';\n\nconst props = defineProps<{\n  class?: any;\n}>();\n</script>\n\n<template>\n  <span\n    :class=\"\n      cn('text-muted-foreground ml-auto text-xs tracking-widest', props.class)\n    \"\n  >\n    <slot></slot>\n  </span>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/context-menu/ContextMenuSub.vue",
    "content": "<script setup lang=\"ts\">\nimport type { ContextMenuSubEmits, ContextMenuSubProps } from 'radix-vue';\n\nimport { ContextMenuSub, useForwardPropsEmits } from 'radix-vue';\n\nconst props = defineProps<ContextMenuSubProps>();\nconst emits = defineEmits<ContextMenuSubEmits>();\n\nconst forwarded = useForwardPropsEmits(props, emits);\n</script>\n\n<template>\n  <ContextMenuSub v-bind=\"forwarded\">\n    <slot></slot>\n  </ContextMenuSub>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/context-menu/ContextMenuSubContent.vue",
    "content": "<script setup lang=\"ts\">\nimport type {\n  DropdownMenuSubContentEmits,\n  DropdownMenuSubContentProps,\n} from 'radix-vue';\n\nimport { cn } from '@vben-core/shared/utils';\nimport { ContextMenuSubContent, useForwardPropsEmits } from 'radix-vue';\nimport { computed } from 'vue';\n\nconst props = defineProps<{ class?: any } & DropdownMenuSubContentProps>();\nconst emits = defineEmits<DropdownMenuSubContentEmits>();\n\nconst delegatedProps = computed(() => {\n  const { class: _, ...delegated } = props;\n\n  return delegated;\n});\n\nconst forwarded = useForwardPropsEmits(delegatedProps, emits);\n</script>\n\n<template>\n  <ContextMenuSubContent\n    v-bind=\"forwarded\"\n    :class=\"\n      cn(\n        'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 border-border z-50 min-w-32 overflow-hidden rounded-md border p-1 shadow-lg',\n        props.class,\n      )\n    \"\n  >\n    <slot></slot>\n  </ContextMenuSubContent>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/context-menu/ContextMenuSubTrigger.vue",
    "content": "<script setup lang=\"ts\">\nimport type { ContextMenuSubTriggerProps } from 'radix-vue';\n\nimport { cn } from '@vben-core/shared/utils';\nimport { ChevronRight } from 'lucide-vue-next';\nimport { ContextMenuSubTrigger, useForwardProps } from 'radix-vue';\nimport { computed } from 'vue';\n\nconst props = defineProps<\n  {\n    class?: any;\n    inset?: boolean;\n  } & ContextMenuSubTriggerProps\n>();\n\nconst delegatedProps = computed(() => {\n  const { class: _, ...delegated } = props;\n\n  return delegated;\n});\n\nconst forwardedProps = useForwardProps(delegatedProps);\n</script>\n\n<template>\n  <ContextMenuSubTrigger\n    v-bind=\"forwardedProps\"\n    :class=\"\n      cn(\n        'focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none',\n        inset && 'pl-8',\n        props.class,\n      )\n    \"\n  >\n    <slot></slot>\n    <ChevronRight class=\"ml-auto h-4 w-4\" />\n  </ContextMenuSubTrigger>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/context-menu/ContextMenuTrigger.vue",
    "content": "<script setup lang=\"ts\">\nimport type { ContextMenuTriggerProps } from 'radix-vue';\n\nimport { ContextMenuTrigger, useForwardProps } from 'radix-vue';\n\nconst props = defineProps<ContextMenuTriggerProps>();\n\nconst forwardedProps = useForwardProps(props);\n</script>\n\n<template>\n  <ContextMenuTrigger v-bind=\"forwardedProps\">\n    <slot></slot>\n  </ContextMenuTrigger>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/context-menu/index.ts",
    "content": "export { default as ContextMenu } from './ContextMenu.vue';\nexport { default as ContextMenuCheckboxItem } from './ContextMenuCheckboxItem.vue';\nexport { default as ContextMenuContent } from './ContextMenuContent.vue';\nexport { default as ContextMenuGroup } from './ContextMenuGroup.vue';\nexport { default as ContextMenuItem } from './ContextMenuItem.vue';\nexport { default as ContextMenuLabel } from './ContextMenuLabel.vue';\nexport { default as ContextMenuRadioGroup } from './ContextMenuRadioGroup.vue';\nexport { default as ContextMenuRadioItem } from './ContextMenuRadioItem.vue';\nexport { default as ContextMenuSeparator } from './ContextMenuSeparator.vue';\nexport { default as ContextMenuShortcut } from './ContextMenuShortcut.vue';\nexport { default as ContextMenuSub } from './ContextMenuSub.vue';\nexport { default as ContextMenuSubContent } from './ContextMenuSubContent.vue';\nexport { default as ContextMenuSubTrigger } from './ContextMenuSubTrigger.vue';\nexport { default as ContextMenuTrigger } from './ContextMenuTrigger.vue';\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/dialog/Dialog.vue",
    "content": "<script setup lang=\"ts\">\nimport type { DialogRootEmits, DialogRootProps } from 'radix-vue';\n\nimport { DialogRoot, useForwardPropsEmits } from 'radix-vue';\n\nconst props = defineProps<DialogRootProps>();\nconst emits = defineEmits<DialogRootEmits>();\n\nconst forwarded = useForwardPropsEmits(props, emits);\n</script>\n\n<template>\n  <DialogRoot v-bind=\"forwarded\">\n    <slot></slot>\n  </DialogRoot>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/dialog/DialogClose.vue",
    "content": "<script setup lang=\"ts\">\nimport type { DialogCloseProps } from 'radix-vue';\n\nimport { DialogClose } from 'radix-vue';\n\nconst props = defineProps<DialogCloseProps>();\n</script>\n\n<template>\n  <DialogClose v-bind=\"props\">\n    <slot></slot>\n  </DialogClose>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/dialog/DialogContent.vue",
    "content": "<script setup lang=\"ts\">\nimport type { DialogContentEmits, DialogContentProps } from 'radix-vue';\n\nimport type { ClassType } from '@vben-core/typings';\n\nimport { computed, ref } from 'vue';\n\nimport { cn } from '@vben-core/shared/utils';\n\nimport { X } from 'lucide-vue-next';\nimport { DialogClose, DialogContent, useForwardPropsEmits } from 'radix-vue';\n\nimport DialogOverlay from './DialogOverlay.vue';\n\nconst props = withDefaults(\n  defineProps<\n    DialogContentProps & {\n      animationType?: 'scale' | 'slide';\n      appendTo?: HTMLElement | string;\n      class?: ClassType;\n      closeClass?: ClassType;\n      closeDisabled?: boolean;\n      modal?: boolean;\n      open?: boolean;\n      overlayBlur?: number;\n      showClose?: boolean;\n      zIndex?: number;\n    }\n  >(),\n  {\n    appendTo: 'body',\n    animationType: 'slide',\n    closeDisabled: false,\n    showClose: true,\n  },\n);\nconst emits = defineEmits<\n  DialogContentEmits & { close: []; closed: []; opened: [] }\n>();\n\nconst delegatedProps = computed(() => {\n  const {\n    class: _,\n    modal: _modal,\n    open: _open,\n    showClose: __,\n    animationType: ___,\n    ...delegated\n  } = props;\n\n  return delegated;\n});\n\nfunction isAppendToBody() {\n  return (\n    props.appendTo === 'body' ||\n    props.appendTo === document.body ||\n    !props.appendTo\n  );\n}\n\nconst position = computed(() => {\n  return isAppendToBody() ? 'fixed' : 'absolute';\n});\n\nconst forwarded = useForwardPropsEmits(delegatedProps, emits);\n\nconst contentRef = ref<InstanceType<typeof DialogContent> | null>(null);\nfunction onAnimationEnd(event: AnimationEvent) {\n  // 只有在 contentRef 的动画结束时才触发 opened/closed 事件\n  if (event.target === contentRef.value?.$el) {\n    if (props.open) {\n      emits('opened');\n    } else {\n      emits('closed');\n    }\n  }\n}\ndefineExpose({\n  getContentRef: () => contentRef.value,\n});\n</script>\n\n<template>\n  <Teleport defer :to=\"appendTo\">\n    <Transition name=\"fade\">\n      <DialogOverlay\n        v-if=\"open && modal\"\n        :style=\"{\n          ...(zIndex ? { zIndex } : {}),\n          position,\n          backdropFilter:\n            overlayBlur && overlayBlur > 0 ? `blur(${overlayBlur}px)` : 'none',\n        }\"\n        @click=\"() => emits('close')\"\n      />\n    </Transition>\n    <DialogContent\n      ref=\"contentRef\"\n      :style=\"{ ...(zIndex ? { zIndex } : {}), position }\"\n      @animationend=\"onAnimationEnd\"\n      v-bind=\"forwarded\"\n      :class=\"\n        cn(\n          'z-popup bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 w-full p-6 shadow-lg outline-none sm:rounded-xl',\n          {\n            'data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-top-[48%]':\n              animationType === 'slide',\n          },\n          props.class,\n        )\n      \"\n    >\n      <slot></slot>\n\n      <DialogClose\n        v-if=\"showClose\"\n        :disabled=\"closeDisabled\"\n        :class=\"\n          cn(\n            'data-[state=open]:bg-accent data-[state=open]:text-muted-foreground hover:bg-accent hover:text-accent-foreground text-foreground/80 flex-center absolute right-3 top-3 h-6 w-6 rounded-full px-1 text-lg opacity-70 transition-opacity hover:opacity-100 focus:outline-none disabled:pointer-events-none',\n            props.closeClass,\n          )\n        \"\n        @click=\"() => emits('close')\"\n      >\n        <X class=\"h-4 w-4\" />\n      </DialogClose>\n    </DialogContent>\n  </Teleport>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/dialog/DialogDescription.vue",
    "content": "<script setup lang=\"ts\">\nimport type { DialogDescriptionProps } from 'radix-vue';\n\nimport { cn } from '@vben-core/shared/utils';\nimport { DialogDescription, useForwardProps } from 'radix-vue';\nimport { computed } from 'vue';\n\nconst props = defineProps<{ class?: any } & DialogDescriptionProps>();\n\nconst delegatedProps = computed(() => {\n  const { class: _, ...delegated } = props;\n\n  return delegated;\n});\n\nconst forwardedProps = useForwardProps(delegatedProps);\n</script>\n\n<template>\n  <DialogDescription\n    v-bind=\"forwardedProps\"\n    :class=\"cn('text-muted-foreground text-sm', props.class)\"\n  >\n    <slot></slot>\n  </DialogDescription>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/dialog/DialogFooter.vue",
    "content": "<script setup lang=\"ts\">\nimport { cn } from '@vben-core/shared/utils';\n\nconst props = defineProps<{ class?: any }>();\n</script>\n\n<template>\n  <div\n    :class=\"\n      cn('flex flex-row flex-col-reverse justify-end gap-x-2', props.class)\n    \"\n  >\n    <slot></slot>\n  </div>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/dialog/DialogHeader.vue",
    "content": "<script setup lang=\"ts\">\nimport { cn } from '@vben-core/shared/utils';\n\nconst props = defineProps<{\n  class?: any;\n}>();\n</script>\n\n<template>\n  <div\n    :class=\"cn('flex flex-col gap-y-1.5 text-center sm:text-left', props.class)\"\n  >\n    <slot></slot>\n  </div>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/dialog/DialogOverlay.vue",
    "content": "<script setup lang=\"ts\">\nimport { inject } from 'vue';\n\nimport { useScrollLock } from '@vben-core/composables';\n\nuseScrollLock();\nconst id = inject('DISMISSABLE_MODAL_ID');\n</script>\n<template>\n  <div :data-dismissable-modal=\"id\" class=\"bg-overlay z-popup inset-0\"></div>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/dialog/DialogScrollContent.vue",
    "content": "<script setup lang=\"ts\">\nimport type { DialogContentEmits, DialogContentProps } from 'radix-vue';\n\nimport { cn } from '@vben-core/shared/utils';\nimport { X } from 'lucide-vue-next';\nimport {\n  DialogClose,\n  DialogContent,\n  DialogOverlay,\n  DialogPortal,\n  useForwardPropsEmits,\n} from 'radix-vue';\nimport { computed } from 'vue';\n\nconst props = withDefaults(\n  defineProps<{ class?: any; zIndex?: number } & DialogContentProps>(),\n  { zIndex: 1000 },\n);\nconst emits = defineEmits<DialogContentEmits>();\n\nconst delegatedProps = computed(() => {\n  const { class: _, ...delegated } = props;\n\n  return delegated;\n});\n\nconst forwarded = useForwardPropsEmits(delegatedProps, emits);\n</script>\n\n<template>\n  <DialogPortal>\n    <DialogOverlay\n      :style=\"{ zIndex }\"\n      class=\"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 border-border absolute inset-0 grid place-items-center overflow-y-auto border bg-black/80\"\n    >\n      <DialogContent\n        :class=\"\n          cn(\n            'border-border bg-background relative z-50 my-8 grid w-full max-w-lg gap-4 border p-6 shadow-lg duration-200 sm:rounded-lg md:w-full',\n            props.class,\n          )\n        \"\n        :style=\"{ zIndex }\"\n        v-bind=\"forwarded\"\n        @pointer-down-outside=\"\n          (event) => {\n            const originalEvent = event.detail.originalEvent;\n            const target = originalEvent.target as HTMLElement;\n            if (\n              originalEvent.offsetX > target.clientWidth ||\n              originalEvent.offsetY > target.clientHeight\n            ) {\n              event.preventDefault();\n            }\n          }\n        \"\n      >\n        <slot></slot>\n\n        <DialogClose\n          class=\"hover:bg-secondary absolute right-4 top-4 rounded-md p-0.5 transition-colors\"\n        >\n          <X class=\"h-4 w-4\" />\n          <span class=\"sr-only\">Close</span>\n        </DialogClose>\n      </DialogContent>\n    </DialogOverlay>\n  </DialogPortal>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/dialog/DialogTitle.vue",
    "content": "<script setup lang=\"ts\">\nimport type { DialogTitleProps } from 'radix-vue';\n\nimport { cn } from '@vben-core/shared/utils';\nimport { DialogTitle, useForwardProps } from 'radix-vue';\nimport { computed } from 'vue';\n\nconst props = defineProps<{ class?: any } & DialogTitleProps>();\n\nconst delegatedProps = computed(() => {\n  const { class: _, ...delegated } = props;\n\n  return delegated;\n});\n\nconst forwardedProps = useForwardProps(delegatedProps);\n</script>\n\n<template>\n  <DialogTitle\n    v-bind=\"forwardedProps\"\n    :class=\"\n      cn('text-lg font-semibold leading-none tracking-tight', props.class)\n    \"\n  >\n    <slot></slot>\n  </DialogTitle>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/dialog/DialogTrigger.vue",
    "content": "<script setup lang=\"ts\">\nimport type { DialogTriggerProps } from 'radix-vue';\n\nimport { DialogTrigger } from 'radix-vue';\n\nconst props = defineProps<DialogTriggerProps>();\n</script>\n\n<template>\n  <DialogTrigger v-bind=\"props\">\n    <slot></slot>\n  </DialogTrigger>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/dialog/index.ts",
    "content": "export { default as Dialog } from './Dialog.vue';\nexport { default as DialogClose } from './DialogClose.vue';\nexport { default as DialogContent } from './DialogContent.vue';\nexport { default as DialogDescription } from './DialogDescription.vue';\nexport { default as DialogFooter } from './DialogFooter.vue';\nexport { default as DialogHeader } from './DialogHeader.vue';\nexport { default as DialogScrollContent } from './DialogScrollContent.vue';\nexport { default as DialogTitle } from './DialogTitle.vue';\nexport { default as DialogTrigger } from './DialogTrigger.vue';\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/dropdown-menu/DropdownMenu.vue",
    "content": "<script setup lang=\"ts\">\nimport type { DropdownMenuRootEmits, DropdownMenuRootProps } from 'radix-vue';\n\nimport { DropdownMenuRoot, useForwardPropsEmits } from 'radix-vue';\n\nconst props = withDefaults(defineProps<DropdownMenuRootProps>(), {\n  modal: false,\n});\nconst emits = defineEmits<DropdownMenuRootEmits>();\n\nconst forwarded = useForwardPropsEmits(props, emits);\n</script>\n\n<template>\n  <DropdownMenuRoot v-bind=\"forwarded\">\n    <slot></slot>\n  </DropdownMenuRoot>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/dropdown-menu/DropdownMenuCheckboxItem.vue",
    "content": "<script setup lang=\"ts\">\nimport type {\n  DropdownMenuCheckboxItemEmits,\n  DropdownMenuCheckboxItemProps,\n} from 'radix-vue';\n\nimport { cn } from '@vben-core/shared/utils';\nimport { Check } from 'lucide-vue-next';\nimport {\n  DropdownMenuCheckboxItem,\n  DropdownMenuItemIndicator,\n  useForwardPropsEmits,\n} from 'radix-vue';\nimport { computed } from 'vue';\n\nconst props = defineProps<{ class?: any } & DropdownMenuCheckboxItemProps>();\nconst emits = defineEmits<DropdownMenuCheckboxItemEmits>();\n\nconst delegatedProps = computed(() => {\n  const { class: _, ...delegated } = props;\n\n  return delegated;\n});\n\nconst forwarded = useForwardPropsEmits(delegatedProps, emits);\n</script>\n\n<template>\n  <DropdownMenuCheckboxItem\n    v-bind=\"forwarded\"\n    :class=\"\n      cn(\n        'focus:bg-accent focus:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors data-[disabled]:pointer-events-none data-[disabled]:opacity-50',\n        props.class,\n      )\n    \"\n  >\n    <span class=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\">\n      <DropdownMenuItemIndicator>\n        <Check class=\"h-4 w-4\" />\n      </DropdownMenuItemIndicator>\n    </span>\n    <slot></slot>\n  </DropdownMenuCheckboxItem>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/dropdown-menu/DropdownMenuContent.vue",
    "content": "<script setup lang=\"ts\">\nimport type {\n  DropdownMenuContentEmits,\n  DropdownMenuContentProps,\n} from 'radix-vue';\n\nimport { computed } from 'vue';\n\nimport { cn } from '@vben-core/shared/utils';\n\nimport {\n  DropdownMenuContent,\n  DropdownMenuPortal,\n  useForwardPropsEmits,\n} from 'radix-vue';\n\nconst props = withDefaults(\n  defineProps<DropdownMenuContentProps & { class?: any }>(),\n  {\n    sideOffset: 4,\n  },\n);\nconst emits = defineEmits<DropdownMenuContentEmits>();\n\nconst delegatedProps = computed(() => {\n  const { class: _, ...delegated } = props;\n\n  return delegated;\n});\n\nconst forwarded = useForwardPropsEmits(delegatedProps, emits);\n</script>\n\n<template>\n  <DropdownMenuPortal>\n    <DropdownMenuContent\n      v-bind=\"forwarded\"\n      :class=\"\n        cn(\n          'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 border-border z-popup min-w-32 overflow-hidden rounded-md border p-1 shadow-md',\n          props.class,\n        )\n      \"\n    >\n      <slot></slot>\n    </DropdownMenuContent>\n  </DropdownMenuPortal>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/dropdown-menu/DropdownMenuGroup.vue",
    "content": "<script setup lang=\"ts\">\nimport type { DropdownMenuGroupProps } from 'radix-vue';\n\nimport { DropdownMenuGroup } from 'radix-vue';\n\nconst props = defineProps<DropdownMenuGroupProps>();\n</script>\n\n<template>\n  <DropdownMenuGroup v-bind=\"props\">\n    <slot></slot>\n  </DropdownMenuGroup>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/dropdown-menu/DropdownMenuItem.vue",
    "content": "<script setup lang=\"ts\">\nimport type { DropdownMenuItemProps } from 'radix-vue';\n\nimport { cn } from '@vben-core/shared/utils';\nimport { DropdownMenuItem, useForwardProps } from 'radix-vue';\nimport { computed } from 'vue';\n\nconst props = defineProps<\n  { class?: any; inset?: boolean } & DropdownMenuItemProps\n>();\n\nconst delegatedProps = computed(() => {\n  const { class: _, ...delegated } = props;\n\n  return delegated;\n});\n\nconst forwardedProps = useForwardProps(delegatedProps);\n</script>\n\n<template>\n  <DropdownMenuItem\n    v-bind=\"forwardedProps\"\n    :class=\"\n      cn(\n        'focus:bg-accent focus:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors data-[disabled]:pointer-events-none data-[disabled]:opacity-50',\n        inset && 'pl-8',\n        props.class,\n      )\n    \"\n  >\n    <slot></slot>\n  </DropdownMenuItem>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/dropdown-menu/DropdownMenuLabel.vue",
    "content": "<script setup lang=\"ts\">\nimport type { DropdownMenuLabelProps } from 'radix-vue';\n\nimport { cn } from '@vben-core/shared/utils';\nimport { DropdownMenuLabel, useForwardProps } from 'radix-vue';\nimport { computed } from 'vue';\n\nconst props = defineProps<\n  { class?: any; inset?: boolean } & DropdownMenuLabelProps\n>();\n\nconst delegatedProps = computed(() => {\n  const { class: _, ...delegated } = props;\n\n  return delegated;\n});\n\nconst forwardedProps = useForwardProps(delegatedProps);\n</script>\n\n<template>\n  <DropdownMenuLabel\n    v-bind=\"forwardedProps\"\n    :class=\"\n      cn('px-2 py-1.5 text-sm font-semibold', inset && 'pl-8', props.class)\n    \"\n  >\n    <slot></slot>\n  </DropdownMenuLabel>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/dropdown-menu/DropdownMenuRadioGroup.vue",
    "content": "<script setup lang=\"ts\">\nimport type {\n  DropdownMenuRadioGroupEmits,\n  DropdownMenuRadioGroupProps,\n} from 'radix-vue';\n\nimport { DropdownMenuRadioGroup, useForwardPropsEmits } from 'radix-vue';\n\nconst props = defineProps<DropdownMenuRadioGroupProps>();\nconst emits = defineEmits<DropdownMenuRadioGroupEmits>();\n\nconst forwarded = useForwardPropsEmits(props, emits);\n</script>\n\n<template>\n  <DropdownMenuRadioGroup v-bind=\"forwarded\">\n    <slot></slot>\n  </DropdownMenuRadioGroup>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/dropdown-menu/DropdownMenuRadioItem.vue",
    "content": "<script setup lang=\"ts\">\nimport type {\n  DropdownMenuRadioItemEmits,\n  DropdownMenuRadioItemProps,\n} from 'radix-vue';\n\nimport { cn } from '@vben-core/shared/utils';\nimport { Circle } from 'lucide-vue-next';\nimport {\n  DropdownMenuItemIndicator,\n  DropdownMenuRadioItem,\n  useForwardPropsEmits,\n} from 'radix-vue';\nimport { computed } from 'vue';\n\nconst props = defineProps<{ class?: any } & DropdownMenuRadioItemProps>();\n\nconst emits = defineEmits<DropdownMenuRadioItemEmits>();\n\nconst delegatedProps = computed(() => {\n  const { class: _, ...delegated } = props;\n\n  return delegated;\n});\n\nconst forwarded = useForwardPropsEmits(delegatedProps, emits);\n</script>\n\n<template>\n  <DropdownMenuRadioItem\n    v-bind=\"forwarded\"\n    :class=\"\n      cn(\n        'focus:bg-accent focus:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors data-[disabled]:pointer-events-none data-[disabled]:opacity-50',\n        props.class,\n      )\n    \"\n  >\n    <span class=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\">\n      <DropdownMenuItemIndicator>\n        <Circle class=\"h-2 w-2 fill-current\" />\n      </DropdownMenuItemIndicator>\n    </span>\n    <slot></slot>\n  </DropdownMenuRadioItem>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/dropdown-menu/DropdownMenuSeparator.vue",
    "content": "<script setup lang=\"ts\">\nimport type { DropdownMenuSeparatorProps } from 'radix-vue';\n\nimport { cn } from '@vben-core/shared/utils';\nimport { DropdownMenuSeparator } from 'radix-vue';\nimport { computed } from 'vue';\n\nconst props = defineProps<\n  {\n    class?: any;\n  } & DropdownMenuSeparatorProps\n>();\n\nconst delegatedProps = computed(() => {\n  const { class: _, ...delegated } = props;\n\n  return delegated;\n});\n</script>\n\n<template>\n  <DropdownMenuSeparator\n    v-bind=\"delegatedProps\"\n    :class=\"cn('bg-border -mx-1 my-1 h-px', props.class)\"\n  />\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/dropdown-menu/DropdownMenuShortcut.vue",
    "content": "<script setup lang=\"ts\">\nimport { cn } from '@vben-core/shared/utils';\n\nconst props = defineProps<{\n  class?: any;\n}>();\n</script>\n\n<template>\n  <span :class=\"cn('ml-auto text-xs tracking-widest opacity-60', props.class)\">\n    <slot></slot>\n  </span>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/dropdown-menu/DropdownMenuSub.vue",
    "content": "<script setup lang=\"ts\">\nimport type { DropdownMenuSubEmits, DropdownMenuSubProps } from 'radix-vue';\n\nimport { DropdownMenuSub, useForwardPropsEmits } from 'radix-vue';\n\nconst props = defineProps<DropdownMenuSubProps>();\nconst emits = defineEmits<DropdownMenuSubEmits>();\n\nconst forwarded = useForwardPropsEmits(props, emits);\n</script>\n\n<template>\n  <DropdownMenuSub v-bind=\"forwarded\">\n    <slot></slot>\n  </DropdownMenuSub>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/dropdown-menu/DropdownMenuSubContent.vue",
    "content": "<script setup lang=\"ts\">\nimport type {\n  DropdownMenuSubContentEmits,\n  DropdownMenuSubContentProps,\n} from 'radix-vue';\n\nimport { cn } from '@vben-core/shared/utils';\nimport { DropdownMenuSubContent, useForwardPropsEmits } from 'radix-vue';\nimport { computed } from 'vue';\n\nconst props = defineProps<{ class?: any } & DropdownMenuSubContentProps>();\nconst emits = defineEmits<DropdownMenuSubContentEmits>();\n\nconst delegatedProps = computed(() => {\n  const { class: _, ...delegated } = props;\n\n  return delegated;\n});\n\nconst forwarded = useForwardPropsEmits(delegatedProps, emits);\n</script>\n\n<template>\n  <DropdownMenuSubContent\n    v-bind=\"forwarded\"\n    :class=\"\n      cn(\n        'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 border-border z-50 min-w-32 overflow-hidden rounded-md border p-1 shadow-lg',\n        props.class,\n      )\n    \"\n  >\n    <slot></slot>\n  </DropdownMenuSubContent>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/dropdown-menu/DropdownMenuSubTrigger.vue",
    "content": "<script setup lang=\"ts\">\nimport type { DropdownMenuSubTriggerProps } from 'radix-vue';\n\nimport { cn } from '@vben-core/shared/utils';\nimport { ChevronRight } from 'lucide-vue-next';\nimport { DropdownMenuSubTrigger, useForwardProps } from 'radix-vue';\nimport { computed } from 'vue';\n\nconst props = defineProps<{ class?: any } & DropdownMenuSubTriggerProps>();\n\nconst delegatedProps = computed(() => {\n  const { class: _, ...delegated } = props;\n\n  return delegated;\n});\n\nconst forwardedProps = useForwardProps(delegatedProps);\n</script>\n\n<template>\n  <DropdownMenuSubTrigger\n    v-bind=\"forwardedProps\"\n    :class=\"\n      cn(\n        'focus:bg-accent data-[state=open]:bg-accent flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none',\n        props.class,\n      )\n    \"\n  >\n    <slot></slot>\n    <ChevronRight class=\"ml-auto h-4 w-4\" />\n  </DropdownMenuSubTrigger>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/dropdown-menu/DropdownMenuTrigger.vue",
    "content": "<script setup lang=\"ts\">\nimport type { DropdownMenuTriggerProps } from 'radix-vue';\n\nimport { DropdownMenuTrigger, useForwardProps } from 'radix-vue';\n\nconst props = defineProps<DropdownMenuTriggerProps>();\n\nconst forwardedProps = useForwardProps(props);\n</script>\n\n<template>\n  <DropdownMenuTrigger class=\"outline-none\" v-bind=\"forwardedProps\">\n    <slot></slot>\n  </DropdownMenuTrigger>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/dropdown-menu/index.ts",
    "content": "export { default as DropdownMenu } from './DropdownMenu.vue';\n\nexport { default as DropdownMenuCheckboxItem } from './DropdownMenuCheckboxItem.vue';\nexport { default as DropdownMenuContent } from './DropdownMenuContent.vue';\nexport { default as DropdownMenuGroup } from './DropdownMenuGroup.vue';\nexport { default as DropdownMenuItem } from './DropdownMenuItem.vue';\nexport { default as DropdownMenuLabel } from './DropdownMenuLabel.vue';\nexport { default as DropdownMenuRadioGroup } from './DropdownMenuRadioGroup.vue';\nexport { default as DropdownMenuRadioItem } from './DropdownMenuRadioItem.vue';\nexport { default as DropdownMenuSeparator } from './DropdownMenuSeparator.vue';\nexport { default as DropdownMenuShortcut } from './DropdownMenuShortcut.vue';\nexport { default as DropdownMenuSub } from './DropdownMenuSub.vue';\nexport { default as DropdownMenuSubContent } from './DropdownMenuSubContent.vue';\nexport { default as DropdownMenuSubTrigger } from './DropdownMenuSubTrigger.vue';\nexport { default as DropdownMenuTrigger } from './DropdownMenuTrigger.vue';\nexport { DropdownMenuPortal } from 'radix-vue';\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/form/FormControl.vue",
    "content": "<script lang=\"ts\" setup>\nimport { Slot } from 'radix-vue';\n\nimport { useFormField } from './useFormField';\n\nconst { error, formDescriptionId, formItemId, formMessageId } = useFormField();\n</script>\n\n<template>\n  <Slot\n    :id=\"formItemId\"\n    :aria-describedby=\"\n      !error ? `${formDescriptionId}` : `${formDescriptionId} ${formMessageId}`\n    \"\n    :aria-invalid=\"!!error\"\n  >\n    <slot></slot>\n  </Slot>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/form/FormDescription.vue",
    "content": "<script lang=\"ts\" setup>\nimport { cn } from '@vben-core/shared/utils';\n\nimport { useFormField } from './useFormField';\n\nconst props = defineProps<{\n  class?: any;\n}>();\n\nconst { formDescriptionId } = useFormField();\n</script>\n\n<template>\n  <p\n    :id=\"formDescriptionId\"\n    :class=\"cn('text-muted-foreground text-sm', props.class)\"\n  >\n    <slot></slot>\n  </p>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/form/FormItem.vue",
    "content": "<script lang=\"ts\" setup>\nimport { provide, useId } from 'vue';\n\nimport { cn } from '@vben-core/shared/utils';\n\nimport { FORM_ITEM_INJECTION_KEY } from './injectionKeys';\n\nconst props = defineProps<{\n  class?: any;\n}>();\n\nconst id = useId() as string;\nprovide(FORM_ITEM_INJECTION_KEY, id);\n</script>\n\n<template>\n  <div :class=\"cn(props.class)\">\n    <slot></slot>\n  </div>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/form/FormLabel.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { LabelProps } from 'radix-vue';\n\nimport { cn } from '@vben-core/shared/utils';\n\nimport { Label } from '../label';\nimport { useFormField } from './useFormField';\n\nconst props = defineProps<{ class?: any } & LabelProps>();\n\nconst { formItemId } = useFormField();\n</script>\n\n<template>\n  <Label :class=\"cn(props.class)\" :for=\"formItemId\">\n    <slot></slot>\n  </Label>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/form/FormMessage.vue",
    "content": "<script lang=\"ts\" setup>\nimport { toValue } from 'vue';\n\nimport { ErrorMessage } from 'vee-validate';\n\nimport { useFormField } from './useFormField';\n\nconst { formMessageId, name } = useFormField();\n</script>\n\n<template>\n  <ErrorMessage\n    :id=\"formMessageId\"\n    :name=\"toValue(name)\"\n    as=\"p\"\n    class=\"text-destructive text-[0.8rem]\"\n  />\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/form/index.ts",
    "content": "export { default as FormControl } from './FormControl.vue';\nexport { default as FormDescription } from './FormDescription.vue';\nexport { default as FormItem } from './FormItem.vue';\nexport { default as FormLabel } from './FormLabel.vue';\nexport { default as FormMessage } from './FormMessage.vue';\nexport { FORM_ITEM_INJECTION_KEY } from './injectionKeys';\nexport {\n  Field as FormField,\n  FieldArray as FormFieldArray,\n  Form,\n} from 'vee-validate';\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/form/injectionKeys.ts",
    "content": "import type { InjectionKey } from 'vue';\n\n// eslint-disable-next-line symbol-description\nexport const FORM_ITEM_INJECTION_KEY = Symbol() as InjectionKey<string>;\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/form/useFormField.ts",
    "content": "import { inject } from 'vue';\n\nimport {\n  FieldContextKey,\n  useFieldError,\n  useIsFieldDirty,\n  useIsFieldTouched,\n  useIsFieldValid,\n} from 'vee-validate';\n\nimport { FORM_ITEM_INJECTION_KEY } from './injectionKeys';\n\nexport function useFormField() {\n  const fieldContext = inject(FieldContextKey);\n  const fieldItemContext = inject(FORM_ITEM_INJECTION_KEY);\n\n  if (!fieldContext)\n    throw new Error('useFormField should be used within <FormField>');\n\n  const { name } = fieldContext;\n  const id = fieldItemContext;\n\n  const fieldState = {\n    error: useFieldError(name),\n    isDirty: useIsFieldDirty(name),\n    isTouched: useIsFieldTouched(name),\n    valid: useIsFieldValid(name),\n  };\n\n  return {\n    formDescriptionId: `${id}-form-item-description`,\n    formItemId: `${id}-form-item`,\n    formMessageId: `${id}-form-item-message`,\n    id,\n    name,\n    ...fieldState,\n  };\n}\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/hover-card/HoverCard.vue",
    "content": "<script setup lang=\"ts\">\nimport type { HoverCardRootEmits, HoverCardRootProps } from 'radix-vue';\n\nimport { HoverCardRoot, useForwardPropsEmits } from 'radix-vue';\n\nconst props = defineProps<HoverCardRootProps>();\nconst emits = defineEmits<HoverCardRootEmits>();\n\nconst forwarded = useForwardPropsEmits(props, emits);\n</script>\n\n<template>\n  <HoverCardRoot v-bind=\"forwarded\">\n    <slot></slot>\n  </HoverCardRoot>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/hover-card/HoverCardContent.vue",
    "content": "<script setup lang=\"ts\">\nimport type { HoverCardContentProps } from 'radix-vue';\n\nimport { computed } from 'vue';\n\nimport { cn } from '@vben-core/shared/utils';\n\nimport { HoverCardContent, HoverCardPortal, useForwardProps } from 'radix-vue';\n\nconst props = withDefaults(\n  defineProps<HoverCardContentProps & { class?: any }>(),\n  {\n    sideOffset: 4,\n  },\n);\n\nconst delegatedProps = computed(() => {\n  const { class: _, ...delegated } = props;\n\n  return delegated;\n});\n\nconst forwardedProps = useForwardProps(delegatedProps);\n</script>\n\n<template>\n  <HoverCardPortal>\n    <HoverCardContent\n      v-bind=\"forwardedProps\"\n      :class=\"\n        cn(\n          'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 border-border z-popup w-64 rounded-md border p-4 shadow-md outline-none',\n          props.class,\n        )\n      \"\n    >\n      <slot></slot>\n    </HoverCardContent>\n  </HoverCardPortal>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/hover-card/HoverCardTrigger.vue",
    "content": "<script setup lang=\"ts\">\nimport type { HoverCardTriggerProps } from 'radix-vue';\n\nimport { HoverCardTrigger } from 'radix-vue';\n\nconst props = defineProps<HoverCardTriggerProps>();\n</script>\n\n<template>\n  <HoverCardTrigger v-bind=\"props\">\n    <slot></slot>\n  </HoverCardTrigger>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/hover-card/index.ts",
    "content": "export { default as HoverCard } from './HoverCard.vue';\nexport { default as HoverCardContent } from './HoverCardContent.vue';\nexport { default as HoverCardTrigger } from './HoverCardTrigger.vue';\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/index.ts",
    "content": "export * from './accordion';\nexport * from './alert-dialog';\nexport * from './avatar';\nexport * from './badge';\nexport * from './breadcrumb';\nexport * from './button';\nexport * from './card';\nexport * from './checkbox';\nexport * from './dialog';\nexport * from './dropdown-menu';\nexport * from './form';\nexport * from './hover-card';\nexport * from './input';\nexport * from './label';\nexport * from './number-field';\nexport * from './pagination';\nexport * from './pin-input';\nexport * from './popover';\nexport * from './radio-group';\nexport * from './resizable';\nexport * from './scroll-area';\nexport * from './select';\nexport * from './separator';\nexport * from './sheet';\nexport * from './switch';\nexport * from './tabs';\nexport * from './textarea';\nexport * from './toggle';\nexport * from './toggle-group';\nexport * from './tooltip';\nexport * from './tree';\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/input/Input.vue",
    "content": "<script setup lang=\"ts\">\nimport { cn } from '@vben-core/shared/utils';\n\nimport { useVModel } from '@vueuse/core';\n\nconst props = defineProps<{\n  class?: any;\n  defaultValue?: number | string;\n  modelValue?: number | string;\n}>();\n\nconst emits = defineEmits<{\n  (e: 'update:modelValue', payload: number | string): void;\n}>();\n\nconst modelValue = useVModel(props, 'modelValue', emits, {\n  defaultValue: props.defaultValue,\n  passive: true,\n});\n</script>\n\n<template>\n  <input\n    v-model=\"modelValue\"\n    :class=\"\n      cn(\n        'border-input bg-background ring-offset-background placeholder:text-muted-foreground flex h-10 w-full rounded-md border px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50',\n        props.class,\n      )\n    \"\n  />\n</template>\n<style lang=\"scss\" scoped>\ninput {\n  --ring: var(--primary);\n}\n</style>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/input/index.ts",
    "content": "export { default as Input } from './Input.vue';\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/label/Label.vue",
    "content": "<script setup lang=\"ts\">\nimport type { LabelProps } from 'radix-vue';\n\nimport { cn } from '@vben-core/shared/utils';\nimport { Label } from 'radix-vue';\nimport { computed } from 'vue';\n\nconst props = defineProps<{ class?: any } & LabelProps>();\n\nconst delegatedProps = computed(() => {\n  const { class: _, ...delegated } = props;\n\n  return delegated;\n});\n</script>\n\n<template>\n  <Label\n    v-bind=\"delegatedProps\"\n    :class=\"\n      cn(\n        'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',\n        props.class,\n      )\n    \"\n  >\n    <slot></slot>\n  </Label>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/label/index.ts",
    "content": "export { default as Label } from './Label.vue';\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/number-field/NumberField.vue",
    "content": "<script setup lang=\"ts\">\nimport type { NumberFieldRootEmits, NumberFieldRootProps } from 'radix-vue';\n\nimport { cn } from '@vben-core/shared/utils';\nimport { NumberFieldRoot, useForwardPropsEmits } from 'radix-vue';\nimport { computed } from 'vue';\n\nconst props = defineProps<{ class?: any } & NumberFieldRootProps>();\nconst emits = defineEmits<NumberFieldRootEmits>();\n\nconst delegatedProps = computed(() => {\n  const { class: _, ...delegated } = props;\n\n  return delegated;\n});\n\nconst forwarded = useForwardPropsEmits(delegatedProps, emits);\n</script>\n\n<template>\n  <NumberFieldRoot v-bind=\"forwarded\" :class=\"cn('grid gap-1.5', props.class)\">\n    <slot></slot>\n  </NumberFieldRoot>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/number-field/NumberFieldContent.vue",
    "content": "<script setup lang=\"ts\">\nimport { cn } from '@vben-core/shared/utils';\n\nconst props = defineProps<{\n  class?: any;\n}>();\n</script>\n\n<template>\n  <div\n    :class=\"\n      cn(\n        'relative [&>[data-slot=input]]:has-[[data-slot=decrement]]:pl-5 [&>[data-slot=input]]:has-[[data-slot=increment]]:pr-5',\n        props.class,\n      )\n    \"\n  >\n    <slot></slot>\n  </div>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/number-field/NumberFieldDecrement.vue",
    "content": "<script setup lang=\"ts\">\nimport type { NumberFieldDecrementProps } from 'radix-vue';\n\nimport { cn } from '@vben-core/shared/utils';\nimport { Minus } from 'lucide-vue-next';\nimport { NumberFieldDecrement, useForwardProps } from 'radix-vue';\nimport { computed } from 'vue';\n\nconst props = defineProps<{ class?: any } & NumberFieldDecrementProps>();\n\nconst delegatedProps = computed(() => {\n  const { class: _, ...delegated } = props;\n\n  return delegated;\n});\n\nconst forwarded = useForwardProps(delegatedProps);\n</script>\n\n<template>\n  <NumberFieldDecrement\n    data-slot=\"decrement\"\n    v-bind=\"forwarded\"\n    :class=\"\n      cn(\n        'absolute left-0 top-1/2 -translate-y-1/2 p-3 disabled:cursor-not-allowed disabled:opacity-20',\n        props.class,\n      )\n    \"\n  >\n    <slot>\n      <Minus class=\"h-4 w-4\" />\n    </slot>\n  </NumberFieldDecrement>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/number-field/NumberFieldIncrement.vue",
    "content": "<script setup lang=\"ts\">\nimport type { NumberFieldIncrementProps } from 'radix-vue';\n\nimport { cn } from '@vben-core/shared/utils';\nimport { Plus } from 'lucide-vue-next';\nimport { NumberFieldIncrement, useForwardProps } from 'radix-vue';\nimport { computed } from 'vue';\n\nconst props = defineProps<{ class?: any } & NumberFieldIncrementProps>();\n\nconst delegatedProps = computed(() => {\n  const { class: _, ...delegated } = props;\n\n  return delegated;\n});\n\nconst forwarded = useForwardProps(delegatedProps);\n</script>\n\n<template>\n  <NumberFieldIncrement\n    data-slot=\"increment\"\n    v-bind=\"forwarded\"\n    :class=\"\n      cn(\n        'absolute right-0 top-1/2 -translate-y-1/2 p-3 disabled:cursor-not-allowed disabled:opacity-20',\n        props.class,\n      )\n    \"\n  >\n    <slot>\n      <Plus class=\"h-4 w-4\" />\n    </slot>\n  </NumberFieldIncrement>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/number-field/NumberFieldInput.vue",
    "content": "<script setup lang=\"ts\">\nimport { cn } from '@vben-core/shared/utils';\n\nimport { NumberFieldInput } from 'radix-vue';\n</script>\n\n<template>\n  <NumberFieldInput\n    :class=\"\n      cn(\n        'border-input placeholder:text-muted-foreground focus-visible:ring-ring flex h-9 w-full rounded-md border bg-transparent py-1 text-center text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 disabled:cursor-not-allowed disabled:opacity-50',\n      )\n    \"\n    data-slot=\"input\"\n  />\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/number-field/index.ts",
    "content": "export { default as NumberField } from './NumberField.vue';\nexport { default as NumberFieldContent } from './NumberFieldContent.vue';\nexport { default as NumberFieldDecrement } from './NumberFieldDecrement.vue';\nexport { default as NumberFieldIncrement } from './NumberFieldIncrement.vue';\nexport { default as NumberFieldInput } from './NumberFieldInput.vue';\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/pagination/PaginationEllipsis.vue",
    "content": "<script setup lang=\"ts\">\nimport type { PaginationEllipsisProps } from 'radix-vue';\n\nimport { cn } from '@vben-core/shared/utils';\nimport { MoreHorizontal } from 'lucide-vue-next';\nimport { PaginationEllipsis } from 'radix-vue';\nimport { computed } from 'vue';\n\nconst props = defineProps<{ class?: any } & PaginationEllipsisProps>();\n\nconst delegatedProps = computed(() => {\n  const { class: _, ...delegated } = props;\n\n  return delegated;\n});\n</script>\n\n<template>\n  <PaginationEllipsis\n    v-bind=\"delegatedProps\"\n    :class=\"cn('flex size-8 items-center justify-center', props.class)\"\n  >\n    <slot>\n      <MoreHorizontal class=\"size-4\" />\n    </slot>\n  </PaginationEllipsis>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/pagination/PaginationFirst.vue",
    "content": "<script setup lang=\"ts\">\nimport type { PaginationFirstProps } from 'radix-vue';\n\nimport { cn } from '@vben-core/shared/utils';\nimport { ChevronsLeft } from 'lucide-vue-next';\nimport { PaginationFirst } from 'radix-vue';\nimport { computed } from 'vue';\n\nimport { Button } from '../button';\n\nconst props = withDefaults(\n  defineProps<{ class?: any } & PaginationFirstProps>(),\n  {\n    asChild: true,\n  },\n);\n\nconst delegatedProps = computed(() => {\n  const { class: _, ...delegated } = props;\n\n  return delegated;\n});\n</script>\n\n<template>\n  <PaginationFirst v-bind=\"delegatedProps\">\n    <Button :class=\"cn('size-8 p-0', props.class)\" variant=\"outline\">\n      <slot>\n        <ChevronsLeft class=\"size-4\" />\n      </slot>\n    </Button>\n  </PaginationFirst>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/pagination/PaginationLast.vue",
    "content": "<script setup lang=\"ts\">\nimport type { PaginationLastProps } from 'radix-vue';\n\nimport { cn } from '@vben-core/shared/utils';\nimport { ChevronsRight } from 'lucide-vue-next';\nimport { PaginationLast } from 'radix-vue';\nimport { computed } from 'vue';\n\nimport { Button } from '../button';\n\nconst props = withDefaults(\n  defineProps<{ class?: any } & PaginationLastProps>(),\n  {\n    asChild: true,\n  },\n);\n\nconst delegatedProps = computed(() => {\n  const { class: _, ...delegated } = props;\n\n  return delegated;\n});\n</script>\n\n<template>\n  <PaginationLast v-bind=\"delegatedProps\">\n    <Button :class=\"cn('size-8 p-0', props.class)\" variant=\"outline\">\n      <slot>\n        <ChevronsRight class=\"size-4\" />\n      </slot>\n    </Button>\n  </PaginationLast>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/pagination/PaginationNext.vue",
    "content": "<script setup lang=\"ts\">\nimport type { PaginationNextProps } from 'radix-vue';\n\nimport { cn } from '@vben-core/shared/utils';\nimport { ChevronRight } from 'lucide-vue-next';\nimport { PaginationNext } from 'radix-vue';\nimport { computed } from 'vue';\n\nimport { Button } from '../button';\n\nconst props = withDefaults(\n  defineProps<{ class?: any } & PaginationNextProps>(),\n  {\n    asChild: true,\n  },\n);\n\nconst delegatedProps = computed(() => {\n  const { class: _, ...delegated } = props;\n\n  return delegated;\n});\n</script>\n\n<template>\n  <PaginationNext v-bind=\"delegatedProps\">\n    <Button :class=\"cn('size-8 p-0', props.class)\" variant=\"outline\">\n      <slot>\n        <ChevronRight class=\"size-4\" />\n      </slot>\n    </Button>\n  </PaginationNext>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/pagination/PaginationPrev.vue",
    "content": "<script setup lang=\"ts\">\nimport type { PaginationPrevProps } from 'radix-vue';\n\nimport { cn } from '@vben-core/shared/utils';\nimport { ChevronLeft } from 'lucide-vue-next';\nimport { PaginationPrev } from 'radix-vue';\nimport { computed } from 'vue';\n\nimport { Button } from '../button';\n\nconst props = withDefaults(\n  defineProps<{ class?: any } & PaginationPrevProps>(),\n  {\n    asChild: true,\n  },\n);\n\nconst delegatedProps = computed(() => {\n  const { class: _, ...delegated } = props;\n\n  return delegated;\n});\n</script>\n\n<template>\n  <PaginationPrev v-bind=\"delegatedProps\">\n    <Button :class=\"cn('size-8 p-0', props.class)\" variant=\"outline\">\n      <slot>\n        <ChevronLeft class=\"size-4\" />\n      </slot>\n    </Button>\n  </PaginationPrev>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/pagination/index.ts",
    "content": "export { default as PaginationEllipsis } from './PaginationEllipsis.vue';\nexport { default as PaginationFirst } from './PaginationFirst.vue';\nexport { default as PaginationLast } from './PaginationLast.vue';\nexport { default as PaginationNext } from './PaginationNext.vue';\nexport { default as PaginationPrev } from './PaginationPrev.vue';\nexport {\n  PaginationList,\n  PaginationListItem,\n  PaginationRoot as Pagination,\n} from 'radix-vue';\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/pin-input/PinInput.vue",
    "content": "<script setup lang=\"ts\">\nimport type { PinInputRootEmits, PinInputRootProps } from 'radix-vue';\n\nimport { cn } from '@vben-core/shared/utils';\nimport { PinInputRoot, useForwardPropsEmits } from 'radix-vue';\nimport { computed } from 'vue';\n\nconst props = defineProps<{ class?: any } & PinInputRootProps>();\nconst emits = defineEmits<PinInputRootEmits>();\n\nconst delegatedProps = computed(() => {\n  const { class: _, ...delegated } = props;\n  return delegated;\n});\n\nconst forwarded = useForwardPropsEmits(delegatedProps, emits);\n</script>\n\n<template>\n  <PinInputRoot\n    v-bind=\"forwarded\"\n    :class=\"cn('flex items-center gap-2', props.class)\"\n  >\n    <slot></slot>\n  </PinInputRoot>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/pin-input/PinInputGroup.vue",
    "content": "<script setup lang=\"ts\">\nimport type { PrimitiveProps } from 'radix-vue';\n\nimport { cn } from '@vben-core/shared/utils';\nimport { Primitive, useForwardProps } from 'radix-vue';\nimport { computed } from 'vue';\n\nconst props = defineProps<{ class?: any } & PrimitiveProps>();\nconst delegatedProps = computed(() => {\n  const { class: _, ...delegated } = props;\n  return delegated;\n});\nconst forwardedProps = useForwardProps(delegatedProps);\n</script>\n\n<template>\n  <Primitive\n    v-bind=\"forwardedProps\"\n    :class=\"cn('flex items-center', props.class)\"\n  >\n    <slot></slot>\n  </Primitive>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/pin-input/PinInputInput.vue",
    "content": "<script setup lang=\"ts\">\nimport type { PinInputInputProps } from 'radix-vue';\n\nimport { cn } from '@vben-core/shared/utils';\nimport { PinInputInput, useForwardProps } from 'radix-vue';\nimport { computed } from 'vue';\n\nconst props = defineProps<{ class?: any } & PinInputInputProps>();\n\nconst delegatedProps = computed(() => {\n  const { class: _, ...delegated } = props;\n  return delegated;\n});\n\nconst forwardedProps = useForwardProps(delegatedProps);\n</script>\n\n<template>\n  <PinInputInput\n    v-bind=\"forwardedProps\"\n    :class=\"\n      cn(\n        'border-input bg-background relative flex h-10 w-8 items-center justify-center border-y border-r text-center text-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md focus:relative focus:z-10 focus:outline-none focus:ring-2 md:w-10',\n        props.class,\n      )\n    \"\n  />\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/pin-input/PinInputSeparator.vue",
    "content": "<script setup lang=\"ts\">\nimport type { PrimitiveProps } from 'radix-vue';\n\nimport { Dot } from 'lucide-vue-next';\nimport { Primitive, useForwardProps } from 'radix-vue';\n\nconst props = defineProps<PrimitiveProps>();\nconst forwardedProps = useForwardProps(props);\n</script>\n\n<template>\n  <Primitive v-bind=\"forwardedProps\">\n    <slot>\n      <Dot />\n    </slot>\n  </Primitive>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/pin-input/index.ts",
    "content": "export { default as PinInput } from './PinInput.vue';\nexport { default as PinInputGroup } from './PinInputGroup.vue';\nexport { default as PinInputInput } from './PinInputInput.vue';\nexport { default as PinInputSeparator } from './PinInputSeparator.vue';\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/popover/Popover.vue",
    "content": "<script setup lang=\"ts\">\nimport type { PopoverRootEmits, PopoverRootProps } from 'radix-vue';\n\nimport { PopoverRoot, useForwardPropsEmits } from 'radix-vue';\n\nconst props = defineProps<PopoverRootProps>();\nconst emits = defineEmits<PopoverRootEmits>();\n\nconst forwarded = useForwardPropsEmits(props, emits);\n</script>\n\n<template>\n  <PopoverRoot v-bind=\"forwarded\">\n    <slot></slot>\n  </PopoverRoot>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/popover/PopoverContent.vue",
    "content": "<script setup lang=\"ts\">\nimport type { PopoverContentEmits, PopoverContentProps } from 'radix-vue';\n\nimport { cn } from '@vben-core/shared/utils';\nimport { PopoverContent, PopoverPortal, useForwardPropsEmits } from 'radix-vue';\nimport { computed } from 'vue';\n\ndefineOptions({\n  inheritAttrs: false,\n});\n\nconst props = withDefaults(\n  defineProps<{ class?: any } & PopoverContentProps>(),\n  {\n    align: 'center',\n    sideOffset: 4,\n  },\n);\nconst emits = defineEmits<PopoverContentEmits>();\n\nconst delegatedProps = computed(() => {\n  const { class: _, ...delegated } = props;\n\n  return delegated;\n});\n\nconst forwarded = useForwardPropsEmits(delegatedProps, emits);\n</script>\n\n<template>\n  <PopoverPortal>\n    <PopoverContent\n      v-bind=\"{ ...forwarded, ...$attrs }\"\n      :class=\"\n        cn(\n          'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 border-border w-72 rounded-md border p-4 shadow-md outline-none',\n          props.class,\n        )\n      \"\n    >\n      <slot></slot>\n    </PopoverContent>\n  </PopoverPortal>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/popover/PopoverTrigger.vue",
    "content": "<script setup lang=\"ts\">\nimport type { PopoverTriggerProps } from 'radix-vue';\n\nimport { PopoverTrigger } from 'radix-vue';\n\nconst props = defineProps<PopoverTriggerProps>();\n</script>\n\n<template>\n  <PopoverTrigger v-bind=\"props\">\n    <slot></slot>\n  </PopoverTrigger>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/popover/index.ts",
    "content": "export { default as Popover } from './Popover.vue';\nexport { default as PopoverContent } from './PopoverContent.vue';\nexport { default as PopoverTrigger } from './PopoverTrigger.vue';\nexport { PopoverAnchor } from 'radix-vue';\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/radio-group/RadioGroup.vue",
    "content": "<script setup lang=\"ts\">\nimport type { RadioGroupRootEmits, RadioGroupRootProps } from 'radix-vue';\n\nimport { cn } from '@vben-core/shared/utils';\nimport { RadioGroupRoot, useForwardPropsEmits } from 'radix-vue';\nimport { computed } from 'vue';\n\nconst props = defineProps<{ class?: any } & RadioGroupRootProps>();\nconst emits = defineEmits<RadioGroupRootEmits>();\n\nconst delegatedProps = computed(() => {\n  const { class: _, ...delegated } = props;\n\n  return delegated;\n});\n\nconst forwarded = useForwardPropsEmits(delegatedProps, emits);\n</script>\n\n<template>\n  <RadioGroupRoot :class=\"cn('grid gap-2', props.class)\" v-bind=\"forwarded\">\n    <slot></slot>\n  </RadioGroupRoot>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/radio-group/RadioGroupItem.vue",
    "content": "<script setup lang=\"ts\">\nimport type { RadioGroupItemProps } from 'radix-vue';\n\nimport { cn } from '@vben-core/shared/utils';\nimport { Circle } from 'lucide-vue-next';\nimport {\n  RadioGroupIndicator,\n  RadioGroupItem,\n  useForwardProps,\n} from 'radix-vue';\nimport { computed } from 'vue';\n\nconst props = defineProps<{ class?: any } & RadioGroupItemProps>();\n\nconst delegatedProps = computed(() => {\n  const { class: _, ...delegated } = props;\n\n  return delegated;\n});\n\nconst forwardedProps = useForwardProps(delegatedProps);\n</script>\n\n<template>\n  <RadioGroupItem\n    v-bind=\"forwardedProps\"\n    :class=\"\n      cn(\n        'border-primary text-primary focus-visible:ring-ring aspect-square h-4 w-4 rounded-full border shadow focus:outline-none focus-visible:ring-1 disabled:cursor-not-allowed disabled:opacity-50',\n        props.class,\n      )\n    \"\n  >\n    <RadioGroupIndicator class=\"flex items-center justify-center\">\n      <Circle class=\"h-2.5 w-2.5 fill-current text-current\" />\n    </RadioGroupIndicator>\n  </RadioGroupItem>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/radio-group/index.ts",
    "content": "export { default as RadioGroup } from './RadioGroup.vue';\nexport { default as RadioGroupItem } from './RadioGroupItem.vue';\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/resizable/ResizableHandle.vue",
    "content": "<script setup lang=\"ts\">\nimport type {\n  SplitterResizeHandleEmits,\n  SplitterResizeHandleProps,\n} from 'radix-vue';\nimport type { HTMLAttributes } from 'vue';\n\nimport { GripVertical } from '@vben-core/icons';\nimport { cn } from '@vben-core/shared/utils';\nimport { SplitterResizeHandle, useForwardPropsEmits } from 'radix-vue';\nimport { computed } from 'vue';\n\nconst props = defineProps<\n  {\n    class?: HTMLAttributes['class'];\n    withHandle?: boolean;\n  } & SplitterResizeHandleProps\n>();\nconst emits = defineEmits<SplitterResizeHandleEmits>();\n\nconst delegatedProps = computed(() => {\n  const { class: _, ...delegated } = props;\n  return delegated;\n});\n\nconst forwarded = useForwardPropsEmits(delegatedProps, emits);\n</script>\n\n<template>\n  <SplitterResizeHandle\n    v-bind=\"forwarded\"\n    :class=\"\n      cn(\n        'bg-border focus-visible:ring-ring relative flex w-px items-center justify-center after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-offset-1 [&[data-orientation=vertical]>div]:rotate-90 [&[data-orientation=vertical]]:h-px [&[data-orientation=vertical]]:w-full [&[data-orientation=vertical]]:after:left-0 [&[data-orientation=vertical]]:after:h-1 [&[data-orientation=vertical]]:after:w-full [&[data-orientation=vertical]]:after:-translate-y-1/2 [&[data-orientation=vertical]]:after:translate-x-0',\n        props.class,\n      )\n    \"\n  >\n    <template v-if=\"props.withHandle\">\n      <div\n        class=\"bg-border z-10 flex h-4 w-3 items-center justify-center rounded-sm border\"\n      >\n        <GripVertical class=\"h-2.5 w-2.5\" />\n      </div>\n    </template>\n  </SplitterResizeHandle>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/resizable/ResizablePanelGroup.vue",
    "content": "<script setup lang=\"ts\">\nimport type { SplitterGroupEmits, SplitterGroupProps } from 'radix-vue';\nimport type { HTMLAttributes } from 'vue';\n\nimport { cn } from '@vben-core/shared/utils';\nimport { SplitterGroup, useForwardPropsEmits } from 'radix-vue';\nimport { computed } from 'vue';\n\nconst props = defineProps<\n  { class?: HTMLAttributes['class'] } & SplitterGroupProps\n>();\nconst emits = defineEmits<SplitterGroupEmits>();\n\nconst delegatedProps = computed(() => {\n  const { class: _, ...delegated } = props;\n  return delegated;\n});\n\nconst forwarded = useForwardPropsEmits(delegatedProps, emits);\n</script>\n\n<template>\n  <SplitterGroup\n    v-bind=\"forwarded\"\n    :class=\"\n      cn(\n        'flex h-full w-full data-[panel-group-direction=vertical]:flex-col',\n        props.class,\n      )\n    \"\n  >\n    <slot></slot>\n  </SplitterGroup>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/resizable/index.ts",
    "content": "export { default as ResizableHandle } from './ResizableHandle.vue';\nexport { default as ResizablePanelGroup } from './ResizablePanelGroup.vue';\nexport { SplitterPanel as ResizablePanel } from 'radix-vue';\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/scroll-area/ScrollArea.vue",
    "content": "<script setup lang=\"ts\">\nimport type { ScrollAreaRootProps } from 'radix-vue';\n\nimport { cn } from '@vben-core/shared/utils';\nimport {\n  ScrollAreaCorner,\n  ScrollAreaRoot,\n  ScrollAreaViewport,\n} from 'radix-vue';\nimport { computed } from 'vue';\n\nimport ScrollBar from './ScrollBar.vue';\n\nconst props = withDefaults(\n  defineProps<\n    {\n      class?: any;\n      onScroll?: (event: Event) => void;\n      viewportProps?: { onScroll: (event: Event) => void };\n    } & ScrollAreaRootProps\n  >(),\n  {\n    onScroll: () => {},\n  },\n);\n\nconst delegatedProps = computed(() => {\n  const { class: _, ...delegated } = props;\n  return delegated;\n});\n</script>\n\n<template>\n  <ScrollAreaRoot\n    v-bind=\"delegatedProps\"\n    :class=\"cn('relative overflow-hidden', props.class)\"\n  >\n    <ScrollAreaViewport\n      as-child\n      class=\"h-full w-full rounded-[inherit] focus:outline-none\"\n      @scroll=\"onScroll\"\n    >\n      <slot></slot>\n    </ScrollAreaViewport>\n    <ScrollBar />\n    <ScrollAreaCorner />\n  </ScrollAreaRoot>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/scroll-area/ScrollBar.vue",
    "content": "<script setup lang=\"ts\">\nimport type { ScrollAreaScrollbarProps } from 'radix-vue';\n\nimport { cn } from '@vben-core/shared/utils';\nimport { ScrollAreaScrollbar, ScrollAreaThumb } from 'radix-vue';\nimport { computed } from 'vue';\n\nconst props = withDefaults(\n  defineProps<{ class?: any } & ScrollAreaScrollbarProps>(),\n  {\n    orientation: 'vertical',\n  },\n);\n\nconst delegatedProps = computed(() => {\n  const { class: _, ...delegated } = props;\n\n  return delegated;\n});\n</script>\n\n<template>\n  <ScrollAreaScrollbar\n    v-bind=\"delegatedProps\"\n    :class=\"\n      cn(\n        'flex touch-none select-none transition-colors',\n        orientation === 'vertical' &&\n          'h-full w-2.5 border-l border-l-transparent p-px',\n        orientation === 'horizontal' &&\n          'h-2.5 flex-col border-t border-t-transparent p-px',\n        props.class,\n      )\n    \"\n  >\n    <ScrollAreaThumb class=\"bg-border relative flex-1 rounded-full\" />\n  </ScrollAreaScrollbar>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/scroll-area/index.ts",
    "content": "export { default as ScrollArea } from './ScrollArea.vue';\nexport { default as ScrollBar } from './ScrollBar.vue';\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/select/Select.vue",
    "content": "<script setup lang=\"ts\">\nimport type { SelectRootEmits, SelectRootProps } from 'radix-vue';\n\nimport { SelectRoot, useForwardPropsEmits } from 'radix-vue';\n\nconst props = defineProps<SelectRootProps>();\nconst emits = defineEmits<SelectRootEmits>();\n\nconst forwarded = useForwardPropsEmits(props, emits);\n</script>\n\n<template>\n  <SelectRoot v-bind=\"forwarded\">\n    <slot></slot>\n  </SelectRoot>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/select/SelectContent.vue",
    "content": "<script setup lang=\"ts\">\nimport type { SelectContentEmits, SelectContentProps } from 'radix-vue';\n\nimport { computed } from 'vue';\n\nimport { cn } from '@vben-core/shared/utils';\n\nimport {\n  SelectContent,\n  SelectPortal,\n  SelectViewport,\n  useForwardPropsEmits,\n} from 'radix-vue';\n\nimport SelectScrollDownButton from './SelectScrollDownButton.vue';\nimport SelectScrollUpButton from './SelectScrollUpButton.vue';\n\ndefineOptions({\n  inheritAttrs: false,\n});\n\nconst props = withDefaults(\n  defineProps<SelectContentProps & { class?: any }>(),\n  {\n    position: 'popper',\n  },\n);\nconst emits = defineEmits<SelectContentEmits>();\n\nconst delegatedProps = computed(() => {\n  const { class: _, ...delegated } = props;\n\n  return delegated;\n});\n\nconst forwarded = useForwardPropsEmits(delegatedProps, emits);\n</script>\n\n<template>\n  <SelectPortal>\n    <SelectContent\n      v-bind=\"{ ...forwarded, ...$attrs }\"\n      :class=\"\n        cn(\n          'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 border-border z-popup relative max-h-96 min-w-32 overflow-hidden rounded-md border shadow-md',\n          position === 'popper' &&\n            'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',\n          props.class,\n        )\n      \"\n    >\n      <SelectScrollUpButton />\n      <SelectViewport\n        :class=\"\n          cn(\n            'p-1',\n            position === 'popper' &&\n              'h-[--radix-select-trigger-height] w-full min-w-[--radix-select-trigger-width]',\n          )\n        \"\n      >\n        <slot></slot>\n      </SelectViewport>\n      <SelectScrollDownButton />\n    </SelectContent>\n  </SelectPortal>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/select/SelectGroup.vue",
    "content": "<script setup lang=\"ts\">\nimport type { SelectGroupProps } from 'radix-vue';\n\nimport { cn } from '@vben-core/shared/utils';\nimport { SelectGroup } from 'radix-vue';\nimport { computed } from 'vue';\n\nconst props = defineProps<{ class?: any } & SelectGroupProps>();\n\nconst delegatedProps = computed(() => {\n  const { class: _, ...delegated } = props;\n\n  return delegated;\n});\n</script>\n\n<template>\n  <SelectGroup :class=\"cn('w-full p-1', props.class)\" v-bind=\"delegatedProps\">\n    <slot></slot>\n  </SelectGroup>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/select/SelectItem.vue",
    "content": "<script setup lang=\"ts\">\nimport type { SelectItemProps } from 'radix-vue';\n\nimport { cn } from '@vben-core/shared/utils';\nimport { Check } from 'lucide-vue-next';\nimport {\n  SelectItem,\n  SelectItemIndicator,\n  SelectItemText,\n  useForwardProps,\n} from 'radix-vue';\nimport { computed } from 'vue';\n\nconst props = defineProps<{ class?: any } & SelectItemProps>();\n\nconst delegatedProps = computed(() => {\n  const { class: _, ...delegated } = props;\n\n  return delegated;\n});\n\nconst forwardedProps = useForwardProps(delegatedProps);\n</script>\n\n<template>\n  <SelectItem\n    v-bind=\"forwardedProps\"\n    :class=\"\n      cn(\n        'focus:bg-accent focus:text-accent-foreground relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50',\n        props.class,\n      )\n    \"\n  >\n    <span class=\"absolute right-2 flex h-3.5 w-3.5 items-center justify-center\">\n      <SelectItemIndicator>\n        <Check class=\"h-4 w-4\" />\n      </SelectItemIndicator>\n    </span>\n\n    <SelectItemText>\n      <slot></slot>\n    </SelectItemText>\n  </SelectItem>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/select/SelectItemText.vue",
    "content": "<script setup lang=\"ts\">\nimport type { SelectItemTextProps } from 'radix-vue';\n\nimport { SelectItemText } from 'radix-vue';\n\nconst props = defineProps<SelectItemTextProps>();\n</script>\n\n<template>\n  <SelectItemText v-bind=\"props\">\n    <slot></slot>\n  </SelectItemText>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/select/SelectLabel.vue",
    "content": "<script setup lang=\"ts\">\nimport type { SelectLabelProps } from 'radix-vue';\n\nimport { cn } from '@vben-core/shared/utils';\nimport { SelectLabel } from 'radix-vue';\n\nconst props = defineProps<{ class?: any } & SelectLabelProps>();\n</script>\n\n<template>\n  <SelectLabel :class=\"cn('px-2 py-1.5 text-sm font-semibold', props.class)\">\n    <slot></slot>\n  </SelectLabel>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/select/SelectScrollDownButton.vue",
    "content": "<script setup lang=\"ts\">\nimport type { SelectScrollDownButtonProps } from 'radix-vue';\n\nimport { cn } from '@vben-core/shared/utils';\nimport { ChevronDown } from 'lucide-vue-next';\nimport { SelectScrollDownButton, useForwardProps } from 'radix-vue';\nimport { computed } from 'vue';\n\nconst props = defineProps<{ class?: any } & SelectScrollDownButtonProps>();\n\nconst delegatedProps = computed(() => {\n  const { class: _, ...delegated } = props;\n\n  return delegated;\n});\n\nconst forwardedProps = useForwardProps(delegatedProps);\n</script>\n\n<template>\n  <SelectScrollDownButton\n    v-bind=\"forwardedProps\"\n    :class=\"\n      cn('flex cursor-default items-center justify-center py-1', props.class)\n    \"\n  >\n    <slot>\n      <ChevronDown class=\"h-4 w-4\" />\n    </slot>\n  </SelectScrollDownButton>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/select/SelectScrollUpButton.vue",
    "content": "<script setup lang=\"ts\">\nimport type { SelectScrollUpButtonProps } from 'radix-vue';\n\nimport { cn } from '@vben-core/shared/utils';\nimport { ChevronUp } from 'lucide-vue-next';\nimport { SelectScrollUpButton, useForwardProps } from 'radix-vue';\nimport { computed } from 'vue';\n\nconst props = defineProps<{ class?: any } & SelectScrollUpButtonProps>();\n\nconst delegatedProps = computed(() => {\n  const { class: _, ...delegated } = props;\n\n  return delegated;\n});\n\nconst forwardedProps = useForwardProps(delegatedProps);\n</script>\n\n<template>\n  <SelectScrollUpButton\n    v-bind=\"forwardedProps\"\n    :class=\"\n      cn('flex cursor-default items-center justify-center py-1', props.class)\n    \"\n  >\n    <slot>\n      <ChevronUp class=\"h-4 w-4\" />\n    </slot>\n  </SelectScrollUpButton>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/select/SelectSeparator.vue",
    "content": "<script setup lang=\"ts\">\nimport type { SelectSeparatorProps } from 'radix-vue';\n\nimport { cn } from '@vben-core/shared/utils';\nimport { SelectSeparator } from 'radix-vue';\nimport { computed } from 'vue';\n\nconst props = defineProps<{ class?: any } & SelectSeparatorProps>();\n\nconst delegatedProps = computed(() => {\n  const { class: _, ...delegated } = props;\n\n  return delegated;\n});\n</script>\n\n<template>\n  <SelectSeparator\n    v-bind=\"delegatedProps\"\n    :class=\"cn('bg-muted -mx-1 my-1 h-px', props.class)\"\n  />\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/select/SelectTrigger.vue",
    "content": "<script setup lang=\"ts\">\nimport type { SelectTriggerProps } from 'radix-vue';\n\nimport { computed } from 'vue';\n\nimport { cn } from '@vben-core/shared/utils';\n\nimport { ChevronDown } from 'lucide-vue-next';\nimport { SelectIcon, SelectTrigger, useForwardProps } from 'radix-vue';\n\nconst props = defineProps<SelectTriggerProps & { class?: any }>();\n\nconst delegatedProps = computed(() => {\n  const { class: _, ...delegated } = props;\n\n  return delegated;\n});\n\nconst forwardedProps = useForwardProps(delegatedProps);\n</script>\n\n<template>\n  <SelectTrigger\n    v-bind=\"forwardedProps\"\n    :class=\"\n      cn(\n        'border-input ring-offset-background placeholder:text-muted-foreground flex h-10 w-full items-center justify-between whitespace-nowrap rounded-md border bg-transparent px-3 py-2 text-sm shadow-sm focus:outline-none disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1',\n        props.class,\n      )\n    \"\n  >\n    <slot></slot>\n    <SelectIcon as-child>\n      <ChevronDown class=\"h-4 w-4 opacity-50\" />\n    </SelectIcon>\n  </SelectTrigger>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/select/SelectValue.vue",
    "content": "<script setup lang=\"ts\">\nimport type { SelectValueProps } from 'radix-vue';\n\nimport { SelectValue } from 'radix-vue';\n\nconst props = defineProps<SelectValueProps>();\n</script>\n\n<template>\n  <SelectValue v-bind=\"props\">\n    <slot></slot>\n  </SelectValue>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/select/index.ts",
    "content": "export { default as Select } from './Select.vue';\nexport { default as SelectContent } from './SelectContent.vue';\nexport { default as SelectGroup } from './SelectGroup.vue';\nexport { default as SelectItem } from './SelectItem.vue';\nexport { default as SelectItemText } from './SelectItemText.vue';\nexport { default as SelectLabel } from './SelectLabel.vue';\nexport { default as SelectScrollDownButton } from './SelectScrollDownButton.vue';\nexport { default as SelectScrollUpButton } from './SelectScrollUpButton.vue';\nexport { default as SelectSeparator } from './SelectSeparator.vue';\nexport { default as SelectTrigger } from './SelectTrigger.vue';\nexport { default as SelectValue } from './SelectValue.vue';\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/separator/Separator.vue",
    "content": "<script setup lang=\"ts\">\nimport type { SeparatorProps } from 'radix-vue';\n\nimport { cn } from '@vben-core/shared/utils';\nimport { Separator } from 'radix-vue';\nimport { computed } from 'vue';\n\nconst props = defineProps<{ class?: any; label?: string } & SeparatorProps>();\n\nconst delegatedProps = computed(() => {\n  const { class: _, ...delegated } = props;\n\n  return delegated;\n});\n</script>\n\n<template>\n  <Separator\n    v-bind=\"delegatedProps\"\n    :class=\"\n      cn(\n        'bg-border relative shrink-0',\n        props.orientation === 'vertical' ? 'h-full w-px' : 'h-px w-full',\n        props.class,\n      )\n    \"\n  >\n    <span\n      v-if=\"props.label\"\n      :class=\"\n        cn(\n          'text-muted-foreground bg-background absolute left-1/2 top-1/2 flex -translate-x-1/2 -translate-y-1/2 items-center justify-center text-xs',\n          props.orientation === 'vertical'\n            ? 'w-[1px] px-1 py-2'\n            : 'h-[1px] px-2 py-1',\n        )\n      \"\n    >\n      {{ props.label }}\n    </span>\n  </Separator>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/separator/index.ts",
    "content": "export { default as Separator } from './Separator.vue';\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/sheet/Sheet.vue",
    "content": "<script setup lang=\"ts\">\nimport type { DialogRootEmits, DialogRootProps } from 'radix-vue';\n\nimport { DialogRoot, useForwardPropsEmits } from 'radix-vue';\n\nconst props = defineProps<DialogRootProps>();\nconst emits = defineEmits<DialogRootEmits>();\n\nconst forwarded = useForwardPropsEmits(props, emits);\n</script>\n\n<template>\n  <DialogRoot v-bind=\"forwarded\">\n    <slot></slot>\n  </DialogRoot>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/sheet/SheetClose.vue",
    "content": "<script setup lang=\"ts\">\nimport type { DialogCloseProps } from 'radix-vue';\n\nimport { DialogClose } from 'radix-vue';\n\nconst props = defineProps<DialogCloseProps>();\n</script>\n\n<template>\n  <DialogClose v-bind=\"props\">\n    <slot></slot>\n  </DialogClose>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/sheet/SheetContent.vue",
    "content": "<script setup lang=\"ts\">\nimport type { DialogContentEmits, DialogContentProps } from 'radix-vue';\n\nimport type { SheetVariants } from './sheet';\n\nimport { computed, ref } from 'vue';\n\nimport { cn } from '@vben-core/shared/utils';\n\nimport { DialogContent, useForwardPropsEmits } from 'radix-vue';\n\nimport { sheetVariants } from './sheet';\nimport SheetOverlay from './SheetOverlay.vue';\n\ninterface SheetContentProps extends DialogContentProps {\n  appendTo?: HTMLElement | string;\n  class?: any;\n  modal?: boolean;\n  open?: boolean;\n  overlayBlur?: number;\n  side?: SheetVariants['side'];\n  zIndex?: number;\n}\n\ndefineOptions({\n  inheritAttrs: false,\n});\n\nconst props = withDefaults(defineProps<SheetContentProps>(), {\n  appendTo: 'body',\n});\n\nconst emits = defineEmits<\n  DialogContentEmits & { close: []; closed: []; opened: [] }\n>();\n\nconst delegatedProps = computed(() => {\n  const {\n    class: _,\n    modal: _modal,\n    open: _open,\n    side: _side,\n    ...delegated\n  } = props;\n\n  return delegated;\n});\n\nfunction isAppendToBody() {\n  return (\n    props.appendTo === 'body' ||\n    props.appendTo === document.body ||\n    !props.appendTo\n  );\n}\n\nconst position = computed(() => {\n  return isAppendToBody() ? 'fixed' : 'absolute';\n});\n\nconst forwarded = useForwardPropsEmits(delegatedProps, emits);\nconst contentRef = ref<InstanceType<typeof DialogContent> | null>(null);\nfunction onAnimationEnd(event: AnimationEvent) {\n  // 只有在 contentRef 的动画结束时才触发 opened/closed 事件\n  if (event.target === contentRef.value?.$el) {\n    if (props.open) {\n      emits('opened');\n    } else {\n      emits('closed');\n    }\n  }\n}\n</script>\n\n<template>\n  <Teleport defer :to=\"appendTo\">\n    <Transition name=\"fade\">\n      <SheetOverlay\n        v-if=\"open && modal\"\n        :style=\"{\n          ...(zIndex ? { zIndex } : {}),\n          position,\n          backdropFilter:\n            overlayBlur && overlayBlur > 0 ? `blur(${overlayBlur}px)` : 'none',\n        }\"\n      />\n    </Transition>\n    <DialogContent\n      ref=\"contentRef\"\n      :class=\"cn('z-popup', sheetVariants({ side }), props.class)\"\n      :style=\"{\n        ...(zIndex ? { zIndex } : {}),\n        position,\n      }\"\n      @animationend=\"onAnimationEnd\"\n      v-bind=\"{ ...forwarded, ...$attrs }\"\n    >\n      <slot></slot>\n\n      <!-- <DialogClose\n        class=\"data-[state=open]:bg-secondary absolute right-4 top-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none disabled:pointer-events-none\"\n      >\n        <Cross2Icon class=\"h-5 w-\" />\n      </DialogClose> -->\n    </DialogContent>\n  </Teleport>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/sheet/SheetDescription.vue",
    "content": "<script setup lang=\"ts\">\nimport type { DialogDescriptionProps } from 'radix-vue';\n\nimport { cn } from '@vben-core/shared/utils';\nimport { DialogDescription } from 'radix-vue';\nimport { computed } from 'vue';\n\nconst props = defineProps<{ class?: any } & DialogDescriptionProps>();\n\nconst delegatedProps = computed(() => {\n  const { class: _, ...delegated } = props;\n\n  return delegated;\n});\n</script>\n\n<template>\n  <DialogDescription\n    :class=\"cn('text-muted-foreground text-sm', props.class)\"\n    v-bind=\"delegatedProps\"\n  >\n    <slot></slot>\n  </DialogDescription>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/sheet/SheetFooter.vue",
    "content": "<script setup lang=\"ts\">\nimport { cn } from '@vben-core/shared/utils';\n\nconst props = defineProps<{ class?: any }>();\n</script>\n\n<template>\n  <div\n    :class=\"\n      cn('flex flex-row flex-col-reverse justify-end gap-x-2', props.class)\n    \"\n  >\n    <slot></slot>\n  </div>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/sheet/SheetHeader.vue",
    "content": "<script setup lang=\"ts\">\nimport { cn } from '@vben-core/shared/utils';\n\nconst props = defineProps<{ class?: any }>();\n</script>\n\n<template>\n  <div :class=\"cn('flex flex-col text-center sm:text-left', props.class)\">\n    <slot></slot>\n  </div>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/sheet/SheetOverlay.vue",
    "content": "<script setup lang=\"ts\">\nimport { inject } from 'vue';\n\nimport { useScrollLock } from '@vben-core/composables';\n\nuseScrollLock();\nconst id = inject('DISMISSABLE_DRAWER_ID');\n</script>\n<template>\n  <div :data-dismissable-drawer=\"id\" class=\"bg-overlay z-popup inset-0\"></div>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/sheet/SheetTitle.vue",
    "content": "<script setup lang=\"ts\">\nimport type { DialogTitleProps } from 'radix-vue';\n\nimport { cn } from '@vben-core/shared/utils';\nimport { DialogTitle } from 'radix-vue';\nimport { computed } from 'vue';\n\nconst props = defineProps<{ class?: any } & DialogTitleProps>();\n\nconst delegatedProps = computed(() => {\n  const { class: _, ...delegated } = props;\n\n  return delegated;\n});\n</script>\n\n<template>\n  <DialogTitle\n    :class=\"cn('text-foreground font-medium', props.class)\"\n    v-bind=\"delegatedProps\"\n  >\n    <slot></slot>\n  </DialogTitle>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/sheet/SheetTrigger.vue",
    "content": "<script setup lang=\"ts\">\nimport type { DialogTriggerProps } from 'radix-vue';\n\nimport { DialogTrigger } from 'radix-vue';\n\nconst props = defineProps<DialogTriggerProps>();\n</script>\n\n<template>\n  <DialogTrigger v-bind=\"props\">\n    <slot></slot>\n  </DialogTrigger>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/sheet/index.ts",
    "content": "export * from './sheet';\nexport { default as Sheet } from './Sheet.vue';\nexport { default as SheetClose } from './SheetClose.vue';\nexport { default as SheetContent } from './SheetContent.vue';\nexport { default as SheetDescription } from './SheetDescription.vue';\nexport { default as SheetFooter } from './SheetFooter.vue';\nexport { default as SheetHeader } from './SheetHeader.vue';\nexport { default as SheetTitle } from './SheetTitle.vue';\n\nexport { default as SheetTrigger } from './SheetTrigger.vue';\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/sheet/sheet.ts",
    "content": "import type { VariantProps } from 'class-variance-authority';\n\nimport { cva } from 'class-variance-authority';\n\nexport const sheetVariants = cva(\n  'bg-background shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500 border-border',\n  {\n    defaultVariants: {\n      side: 'right',\n    },\n    variants: {\n      side: {\n        bottom:\n          'inset-x-0 bottom-0 border-t border-border data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom',\n        left: 'inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left ',\n        right:\n          'inset-y-0 right-0 w-3/4 border-l  data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right',\n        top: 'inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top',\n      },\n    },\n  },\n);\n\nexport type SheetVariants = VariantProps<typeof sheetVariants>;\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/switch/Switch.vue",
    "content": "<script setup lang=\"ts\">\nimport type { SwitchRootEmits, SwitchRootProps } from 'radix-vue';\n\nimport { cn } from '@vben-core/shared/utils';\nimport { SwitchRoot, SwitchThumb, useForwardPropsEmits } from 'radix-vue';\nimport { computed } from 'vue';\n\nconst props = defineProps<{ class?: any } & SwitchRootProps>();\n\nconst emits = defineEmits<SwitchRootEmits>();\n\nconst delegatedProps = computed(() => {\n  const { class: _, ...delegated } = props;\n\n  return delegated;\n});\n\nconst forwarded = useForwardPropsEmits(delegatedProps, emits);\n</script>\n\n<template>\n  <SwitchRoot\n    v-bind=\"forwarded\"\n    :class=\"\n      cn(\n        'focus-visible:ring-ring focus-visible:ring-offset-background data-[state=checked]:bg-primary data-[state=unchecked]:bg-input peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',\n        props.class,\n      )\n    \"\n  >\n    <SwitchThumb\n      :class=\"\n        cn(\n          'bg-background pointer-events-none block h-4 w-4 rounded-full shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0',\n        )\n      \"\n    />\n  </SwitchRoot>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/switch/index.ts",
    "content": "export { default as Switch } from './Switch.vue';\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/tabs/Tabs.vue",
    "content": "<script setup lang=\"ts\">\nimport type { TabsRootEmits, TabsRootProps } from 'radix-vue';\n\nimport { TabsRoot, useForwardPropsEmits } from 'radix-vue';\n\nconst props = defineProps<TabsRootProps>();\nconst emits = defineEmits<TabsRootEmits>();\n\nconst forwarded = useForwardPropsEmits(props, emits);\n</script>\n\n<template>\n  <TabsRoot v-bind=\"forwarded\">\n    <slot></slot>\n  </TabsRoot>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/tabs/TabsContent.vue",
    "content": "<script setup lang=\"ts\">\nimport type { TabsContentProps } from 'radix-vue';\n\nimport { cn } from '@vben-core/shared/utils';\nimport { TabsContent } from 'radix-vue';\nimport { computed } from 'vue';\n\nconst props = defineProps<{ class?: any } & TabsContentProps>();\n\nconst delegatedProps = computed(() => {\n  const { class: _, ...delegated } = props;\n\n  return delegated;\n});\n</script>\n\n<template>\n  <TabsContent\n    :class=\"\n      cn(\n        'ring-offset-background focus-visible:ring-ring mt-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2',\n        props.class,\n      )\n    \"\n    v-bind=\"delegatedProps\"\n  >\n    <slot></slot>\n  </TabsContent>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/tabs/TabsList.vue",
    "content": "<script setup lang=\"ts\">\nimport type { TabsListProps } from 'radix-vue';\n\nimport { cn } from '@vben-core/shared/utils';\nimport { TabsList } from 'radix-vue';\nimport { computed } from 'vue';\n\nconst props = defineProps<{ class?: any } & TabsListProps>();\n\nconst delegatedProps = computed(() => {\n  const { class: _, ...delegated } = props;\n\n  return delegated;\n});\n</script>\n\n<template>\n  <TabsList\n    v-bind=\"delegatedProps\"\n    :class=\"\n      cn(\n        'bg-muted text-muted-foreground inline-flex h-9 items-center justify-center rounded-lg p-1',\n        props.class,\n      )\n    \"\n  >\n    <slot></slot>\n  </TabsList>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/tabs/TabsTrigger.vue",
    "content": "<script setup lang=\"ts\">\nimport type { TabsTriggerProps } from 'radix-vue';\n\nimport { cn } from '@vben-core/shared/utils';\nimport { TabsTrigger, useForwardProps } from 'radix-vue';\nimport { computed } from 'vue';\n\nconst props = defineProps<{ class?: any } & TabsTriggerProps>();\n\nconst delegatedProps = computed(() => {\n  const { class: _, ...delegated } = props;\n\n  return delegated;\n});\n\nconst forwardedProps = useForwardProps(delegatedProps);\n</script>\n\n<template>\n  <TabsTrigger\n    v-bind=\"forwardedProps\"\n    :class=\"\n      cn(\n        'ring-offset-background focus-visible:ring-ring data-[state=active]:bg-background data-[state=active]:text-foreground inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow',\n        props.class,\n      )\n    \"\n  >\n    <slot></slot>\n  </TabsTrigger>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/tabs/index.ts",
    "content": "export { default as Tabs } from './Tabs.vue';\nexport { default as TabsContent } from './TabsContent.vue';\nexport { default as TabsList } from './TabsList.vue';\nexport { default as TabsTrigger } from './TabsTrigger.vue';\nexport { TabsIndicator } from 'radix-vue';\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/textarea/Textarea.vue",
    "content": "<script setup lang=\"ts\">\nimport { cn } from '@vben-core/shared/utils';\n\nimport { useVModel } from '@vueuse/core';\n\nconst props = defineProps<{\n  class?: any;\n  defaultValue?: number | string;\n  modelValue?: number | string;\n}>();\n\nconst emits = defineEmits<{\n  (e: 'update:modelValue', payload: number | string): void;\n}>();\n\nconst modelValue = useVModel(props, 'modelValue', emits, {\n  defaultValue: props.defaultValue,\n  passive: true,\n});\n</script>\n\n<template>\n  <textarea\n    v-model=\"modelValue\"\n    :class=\"\n      cn(\n        'border-input placeholder:text-muted-foreground focus-visible:ring-ring flex min-h-[60px] w-full rounded-md border bg-transparent px-3 py-2 text-sm shadow-sm focus-visible:outline-none focus-visible:ring-1 disabled:cursor-not-allowed disabled:opacity-50',\n        props.class,\n      )\n    \"\n  ></textarea>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/textarea/index.ts",
    "content": "export { default as Textarea } from './Textarea.vue';\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/toggle/Toggle.vue",
    "content": "<script setup lang=\"ts\">\nimport type { ToggleEmits, ToggleProps } from 'radix-vue';\n\nimport type { ToggleVariants } from './toggle';\n\nimport { cn } from '@vben-core/shared/utils';\nimport { Toggle, useForwardPropsEmits } from 'radix-vue';\nimport { computed } from 'vue';\n\nimport { toggleVariants } from './toggle';\n\nconst props = withDefaults(\n  defineProps<\n    {\n      class?: any;\n      size?: ToggleVariants['size'];\n      variant?: ToggleVariants['variant'];\n    } & ToggleProps\n  >(),\n  {\n    disabled: false,\n    size: 'default',\n    variant: 'default',\n  },\n);\n\nconst emits = defineEmits<ToggleEmits>();\n\nconst delegatedProps = computed(() => {\n  const { class: _, size: _size, variant: _variant, ...delegated } = props;\n\n  return delegated;\n});\n\nconst forwarded = useForwardPropsEmits(delegatedProps, emits);\n</script>\n\n<template>\n  <Toggle\n    v-bind=\"forwarded\"\n    :class=\"cn(toggleVariants({ variant, size }), props.class)\"\n  >\n    <slot></slot>\n  </Toggle>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/toggle/index.ts",
    "content": "export * from './toggle';\nexport { default as Toggle } from './Toggle.vue';\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/toggle/toggle.ts",
    "content": "import type { VariantProps } from 'class-variance-authority';\n\nimport { cva } from 'class-variance-authority';\n\nexport const toggleVariants = cva(\n  'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground',\n  {\n    defaultVariants: {\n      size: 'default',\n      variant: 'default',\n    },\n    variants: {\n      size: {\n        default: 'h-9 px-3',\n        lg: 'h-10 px-3',\n        sm: 'h-8 px-2',\n      },\n      variant: {\n        default: 'bg-transparent',\n        outline:\n          'border border-input bg-transparent shadow-sm hover:bg-accent hover:text-accent-foreground',\n      },\n    },\n  },\n);\n\nexport type ToggleVariants = VariantProps<typeof toggleVariants>;\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/toggle-group/ToggleGroup.vue",
    "content": "<script setup lang=\"ts\">\nimport type { VariantProps } from 'class-variance-authority';\nimport type { ToggleGroupRootEmits, ToggleGroupRootProps } from 'radix-vue';\n\nimport type { toggleVariants } from '../toggle';\n\nimport { cn } from '@vben-core/shared/utils';\nimport { ToggleGroupRoot, useForwardPropsEmits } from 'radix-vue';\nimport { computed, provide } from 'vue';\n\ntype ToggleGroupVariants = VariantProps<typeof toggleVariants>;\n\nconst props = defineProps<\n  {\n    class?: any;\n    size?: ToggleGroupVariants['size'];\n    variant?: ToggleGroupVariants['variant'];\n  } & ToggleGroupRootProps\n>();\nconst emits = defineEmits<ToggleGroupRootEmits>();\n\nprovide('toggleGroup', {\n  size: props.size,\n  variant: props.variant,\n});\n\nconst delegatedProps = computed(() => {\n  const { class: _, ...delegated } = props;\n  return delegated;\n});\n\nconst forwarded = useForwardPropsEmits(delegatedProps, emits);\n</script>\n\n<template>\n  <ToggleGroupRoot\n    v-bind=\"forwarded\"\n    :class=\"cn('flex items-center justify-center gap-1', props.class)\"\n  >\n    <slot></slot>\n  </ToggleGroupRoot>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/toggle-group/ToggleGroupItem.vue",
    "content": "<script setup lang=\"ts\">\nimport type { VariantProps } from 'class-variance-authority';\nimport type { ToggleGroupItemProps } from 'radix-vue';\n\nimport { cn } from '@vben-core/shared/utils';\nimport { ToggleGroupItem, useForwardProps } from 'radix-vue';\nimport { computed, inject } from 'vue';\n\nimport { toggleVariants } from '../toggle';\n\ntype ToggleGroupVariants = VariantProps<typeof toggleVariants>;\n\nconst props = defineProps<\n  {\n    class?: any;\n    size?: ToggleGroupVariants['size'];\n    variant?: ToggleGroupVariants['variant'];\n  } & ToggleGroupItemProps\n>();\n\nconst context = inject<ToggleGroupVariants>('toggleGroup');\n\nconst delegatedProps = computed(() => {\n  const { class: _, size: _size, variant: _variant, ...delegated } = props;\n  return delegated;\n});\n\nconst forwardedProps = useForwardProps(delegatedProps);\n</script>\n\n<template>\n  <ToggleGroupItem\n    v-bind=\"forwardedProps\"\n    :class=\"\n      cn(\n        toggleVariants({\n          variant: context?.variant || variant,\n          size: context?.size || size,\n        }),\n        props.class,\n      )\n    \"\n  >\n    <slot></slot>\n  </ToggleGroupItem>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/toggle-group/index.ts",
    "content": "export { default as ToggleGroup } from './ToggleGroup.vue';\nexport { default as ToggleGroupItem } from './ToggleGroupItem.vue';\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/tooltip/Tooltip.vue",
    "content": "<script setup lang=\"ts\">\nimport type { TooltipRootEmits, TooltipRootProps } from 'radix-vue';\n\nimport { TooltipRoot, useForwardPropsEmits } from 'radix-vue';\n\nconst props = defineProps<TooltipRootProps>();\nconst emits = defineEmits<TooltipRootEmits>();\n\nconst forwarded = useForwardPropsEmits(props, emits);\n</script>\n\n<template>\n  <TooltipRoot v-bind=\"forwarded\">\n    <slot></slot>\n  </TooltipRoot>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/tooltip/TooltipContent.vue",
    "content": "<script setup lang=\"ts\">\nimport type { TooltipContentEmits, TooltipContentProps } from 'radix-vue';\n\nimport { computed } from 'vue';\n\nimport { cn } from '@vben-core/shared/utils';\n\nimport { TooltipContent, TooltipPortal, useForwardPropsEmits } from 'radix-vue';\n\ndefineOptions({\n  inheritAttrs: false,\n});\n\nconst props = withDefaults(\n  defineProps<TooltipContentProps & { class?: any }>(),\n  {\n    class: '',\n    side: 'right',\n    sideOffset: 5,\n  },\n);\n\nconst emits = defineEmits<TooltipContentEmits>();\n\nconst delegatedProps = computed(() => {\n  const { class: _, ...delegated } = props;\n\n  return delegated;\n});\n\nconst forwarded = useForwardPropsEmits(delegatedProps, emits);\n</script>\n\n<template>\n  <TooltipPortal>\n    <TooltipContent\n      v-bind=\"{ ...forwarded, ...$attrs }\"\n      :class=\"\n        cn(\n          'z-popup bg-accent text-accent-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 border-border shadow-float overflow-hidden rounded-sm border px-4 py-2 text-xs',\n          props.class,\n        )\n      \"\n    >\n      <slot></slot>\n    </TooltipContent>\n  </TooltipPortal>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/tooltip/TooltipProvider.vue",
    "content": "<script setup lang=\"ts\">\nimport type { TooltipProviderProps } from 'radix-vue';\n\nimport { TooltipProvider } from 'radix-vue';\n\nconst props = defineProps<TooltipProviderProps>();\n</script>\n\n<template>\n  <TooltipProvider v-bind=\"props\">\n    <slot></slot>\n  </TooltipProvider>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/tooltip/TooltipTrigger.vue",
    "content": "<script setup lang=\"ts\">\nimport type { TooltipTriggerProps } from 'radix-vue';\n\nimport { TooltipTrigger } from 'radix-vue';\n\nconst props = defineProps<TooltipTriggerProps>();\n</script>\n\n<template>\n  <TooltipTrigger v-bind=\"props\">\n    <slot></slot>\n  </TooltipTrigger>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/tooltip/index.ts",
    "content": "export { default as Tooltip } from './Tooltip.vue';\nexport { default as TooltipContent } from './TooltipContent.vue';\nexport { default as TooltipProvider } from './TooltipProvider.vue';\nexport { default as TooltipTrigger } from './TooltipTrigger.vue';\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/tree/index.ts",
    "content": "export { default as VbenTree } from './tree.vue';\nexport type { TreeProps } from './types';\nexport { treePropsDefaults } from './types';\nexport type { FlattenedItem } from 'radix-vue';\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/tree/tree.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { Arrayable } from '@vueuse/core';\nimport type { FlattenedItem } from 'radix-vue';\n\nimport type { ClassType, Recordable } from '@vben-core/typings';\n\nimport type { TreeProps } from './types';\n\nimport { onMounted, ref, watchEffect } from 'vue';\n\nimport { ChevronRight, IconifyIcon } from '@vben-core/icons';\nimport { cn, get } from '@vben-core/shared/utils';\n\nimport { TreeItem, TreeRoot } from 'radix-vue';\n\nimport { Checkbox } from '../checkbox';\nimport { treePropsDefaults } from './types';\n\nconst props = withDefaults(defineProps<TreeProps>(), treePropsDefaults());\n\nconst emits = defineEmits<{\n  expand: [value: FlattenedItem<Recordable<any>>];\n  select: [value: FlattenedItem<Recordable<any>>];\n}>();\n\ninterface InnerFlattenItem<T = Recordable<any>, P = number | string> {\n  hasChildren: boolean;\n  id: P;\n  level: number;\n  parentId: null | P;\n  parents: P[];\n  value: T;\n}\n\nfunction flatten<T = Recordable<any>, P = number | string>(\n  items: T[],\n  childrenField: string = 'children',\n  level = 0,\n  parentId: null | P = null,\n  parents: P[] = [],\n): InnerFlattenItem<T, P>[] {\n  const result: InnerFlattenItem<T, P>[] = [];\n  items.forEach((item) => {\n    const children = get(item, childrenField) as Array<T>;\n    const id = get(item, props.valueField) as P;\n    const val: InnerFlattenItem<T, P> = {\n      hasChildren: Array.isArray(children) && children.length > 0,\n      id,\n      level,\n      parentId,\n      parents: [...parents],\n      value: item,\n    };\n    result.push(val);\n    if (val.hasChildren)\n      result.push(\n        ...flatten(children, childrenField, level + 1, id, [...parents, id]),\n      );\n  });\n  return result;\n}\n\nconst flattenData = ref<Array<InnerFlattenItem>>([]);\nconst modelValue = defineModel<Arrayable<number | string>>();\nconst expanded = ref<Array<number | string>>(props.defaultExpandedKeys ?? []);\n\nconst treeValue = ref();\n\nonMounted(() => {\n  watchEffect(() => {\n    flattenData.value = flatten(props.treeData, props.childrenField);\n    updateTreeValue();\n    if (\n      props.defaultExpandedLevel !== undefined &&\n      props.defaultExpandedLevel > 0\n    )\n      expandToLevel(props.defaultExpandedLevel);\n  });\n});\n\nfunction getItemByValue(value: number | string) {\n  return flattenData.value.find(\n    (item) => get(item.value, props.valueField) === value,\n  )?.value;\n}\n\nfunction updateTreeValue() {\n  const val = modelValue.value;\n  if (val === undefined) {\n    treeValue.value = undefined;\n  } else {\n    if (Array.isArray(val)) {\n      const filteredValues = val.filter((v) => {\n        const item = getItemByValue(v);\n        return item && !get(item, props.disabledField);\n      });\n      treeValue.value = filteredValues.map((v) => getItemByValue(v));\n\n      if (filteredValues.length !== val.length) {\n        modelValue.value = filteredValues;\n      }\n    } else {\n      const item = getItemByValue(val);\n      if (item && !get(item, props.disabledField)) {\n        treeValue.value = item;\n      } else {\n        treeValue.value = undefined;\n        modelValue.value = undefined;\n      }\n    }\n  }\n}\n\nfunction updateModelValue(val: Arrayable<Recordable<any>>) {\n  if (Array.isArray(val)) {\n    const filteredVal = val.filter((v) => !get(v, props.disabledField));\n    modelValue.value = filteredVal.map((v) => get(v, props.valueField));\n  } else {\n    if (val && !get(val, props.disabledField)) {\n      modelValue.value = get(val, props.valueField);\n    }\n  }\n}\n\nfunction expandToLevel(level: number) {\n  const keys: string[] = [];\n  flattenData.value.forEach((item) => {\n    if (item.level <= level - 1) {\n      keys.push(get(item.value, props.valueField));\n    }\n  });\n  expanded.value = keys;\n}\n\nfunction collapseNodes(value: Arrayable<number | string>) {\n  const keys = new Set(Array.isArray(value) ? value : [value]);\n  expanded.value = expanded.value.filter((key) => !keys.has(key));\n}\n\nfunction expandNodes(value: Arrayable<number | string>) {\n  const keys = [...(Array.isArray(value) ? value : [value])];\n  keys.forEach((key) => {\n    if (expanded.value.includes(key)) return;\n    const item = getItemByValue(key);\n    if (item) {\n      expanded.value.push(key);\n    }\n  });\n}\n\nfunction expandAll() {\n  expanded.value = flattenData.value\n    .filter((item) => item.hasChildren)\n    .map((item) => get(item.value, props.valueField));\n}\n\nfunction collapseAll() {\n  expanded.value = [];\n}\n\nfunction checkAll() {\n  if (!props.multiple) return;\n  modelValue.value = [\n    ...new Set(\n      flattenData.value\n        .filter((item) => !get(item.value, props.disabledField))\n        .map((item) => get(item.value, props.valueField)),\n    ),\n  ];\n  updateTreeValue();\n}\n\nfunction unCheckAll() {\n  if (!props.multiple) return;\n  modelValue.value = [];\n  updateTreeValue();\n}\n\nfunction isNodeDisabled(item: FlattenedItem<Recordable<any>>) {\n  return props.disabled || get(item.value, props.disabledField);\n}\n\nfunction onToggle(item: FlattenedItem<Recordable<any>>) {\n  emits('expand', item);\n}\nfunction onSelect(item: FlattenedItem<Recordable<any>>, isSelected: boolean) {\n  if (isNodeDisabled(item)) {\n    return;\n  }\n\n  if (\n    !props.checkStrictly &&\n    props.multiple &&\n    props.autoCheckParent &&\n    isSelected\n  ) {\n    flattenData.value\n      .find((i) => {\n        return (\n          get(i.value, props.valueField) === get(item.value, props.valueField)\n        );\n      })\n      ?.parents?.filter((item) => !get(item, props.disabledField))\n      ?.forEach((p) => {\n        if (Array.isArray(modelValue.value) && !modelValue.value.includes(p)) {\n          modelValue.value.push(p);\n        }\n      });\n  }\n  if (\n    !props.checkStrictly &&\n    props.multiple &&\n    props.autoCheckParent &&\n    !isSelected\n  ) {\n    flattenData.value\n      .find((i) => {\n        return (\n          get(i.value, props.valueField) === get(item.value, props.valueField)\n        );\n      })\n      ?.parents?.filter((item) => !get(item, props.disabledField))\n      ?.reverse()\n      .forEach((p) => {\n        const children = flattenData.value.filter((i) => {\n          return (\n            i.parents.length > 0 &&\n            i.parents.includes(p) &&\n            i.id !== item._id &&\n            i.parentId === p\n          );\n        });\n        if (Array.isArray(modelValue.value)) {\n          const hasSelectedChild = children.some((child) =>\n            (modelValue.value as unknown[]).includes(\n              get(child.value, props.valueField),\n            ),\n          );\n          if (!hasSelectedChild) {\n            const index = modelValue.value.indexOf(p);\n            if (index !== -1) {\n              modelValue.value.splice(index, 1);\n            }\n          }\n        }\n      });\n  }\n  updateTreeValue();\n  emits('select', item);\n}\n\ndefineExpose({\n  collapseAll,\n  collapseNodes,\n  expandAll,\n  expandNodes,\n  checkAll,\n  unCheckAll,\n  expandToLevel,\n  getItemByValue,\n});\n</script>\n<template>\n  <TreeRoot\n    :get-key=\"(item) => get(item, valueField)\"\n    :get-children=\"(item) => get(item, childrenField)\"\n    :items=\"treeData\"\n    :model-value=\"treeValue\"\n    v-model:expanded=\"expanded as string[]\"\n    :default-expanded=\"defaultExpandedKeys as string[]\"\n    :propagate-select=\"!checkStrictly\"\n    :multiple=\"multiple\"\n    :disabled=\"disabled\"\n    :selection-behavior=\"allowClear || multiple ? 'toggle' : 'replace'\"\n    @update:model-value=\"updateModelValue\"\n    v-slot=\"{ flattenItems }\"\n    :class=\"\n      cn(\n        'text-blackA11 container select-none list-none rounded-lg text-sm font-medium',\n        $attrs.class as unknown as ClassType,\n        bordered ? 'border' : '',\n      )\n    \"\n  >\n    <div\n      :class=\"\n        cn('my-0.5 flex w-full items-center p-1', bordered ? 'border-b' : '')\n      \"\n      v-if=\"$slots.header\"\n    >\n      <slot name=\"header\"> </slot>\n    </div>\n    <div\n      :class=\"\n        cn('my-0.5 flex w-full items-center p-1', bordered ? 'border-b' : '')\n      \"\n      v-if=\"treeData.length > 0\"\n    >\n      <div\n        class=\"flex size-5 flex-1 cursor-pointer items-center\"\n        @click=\"() => (expanded?.length > 0 ? collapseAll() : expandAll())\"\n      >\n        <ChevronRight\n          :class=\"{ 'rotate-90': expanded?.length > 0 }\"\n          class=\"text-foreground/80 hover:text-foreground size-4 cursor-pointer transition\"\n        />\n        <Checkbox\n          v-if=\"multiple\"\n          @click.stop\n          @update:checked=\"(checked) => (checked ? checkAll() : unCheckAll())\"\n        />\n      </div>\n    </div>\n    <TransitionGroup :name=\"transition ? 'fade' : ''\">\n      <TreeItem\n        v-for=\"item in flattenItems\"\n        v-slot=\"{\n          isExpanded,\n          isSelected,\n          isIndeterminate,\n          handleSelect,\n          handleToggle,\n        }\"\n        :key=\"item._id\"\n        :style=\"{ 'margin-left': `${item.level - 1}rem` }\"\n        :class=\"\n          cn('cursor-pointer', getNodeClass?.(item), {\n            'data-[selected]:bg-accent': !multiple,\n            'text-foreground/50 cursor-not-allowed': isNodeDisabled(item),\n          })\n        \"\n        v-bind=\"\n          Object.assign(item.bind, {\n            onfocus: isNodeDisabled(item) ? 'this.blur()' : undefined,\n            disabled: isNodeDisabled(item),\n          })\n        \"\n        @select=\"\n          (event: any) => {\n            if (isNodeDisabled(item)) {\n              event.preventDefault();\n              event.stopPropagation();\n              return;\n            }\n            if (event.detail.originalEvent.type === 'click') {\n              event.preventDefault();\n            }\n            onSelect(item, event.detail.isSelected);\n          }\n        \"\n        @toggle=\"\n          (event: any) => {\n            if (event.detail.originalEvent.type === 'click') {\n              event.preventDefault();\n            }\n            !isNodeDisabled(item) && onToggle(item);\n          }\n        \"\n        class=\"tree-node focus:ring-grass8 my-0.5 flex items-center rounded p-1 outline-none focus:ring-2\"\n      >\n        <ChevronRight\n          v-if=\"\n            item.hasChildren &&\n            Array.isArray(item.value[childrenField]) &&\n            item.value[childrenField].length > 0\n          \"\n          class=\"text-foreground/80 hover:text-foreground size-4 cursor-pointer transition\"\n          :class=\"{ 'rotate-90': isExpanded }\"\n          @click.stop=\"\n            () => {\n              handleToggle();\n              onToggle(item);\n            }\n          \"\n        />\n        <div v-else class=\"h-4 w-4\"></div>\n        <div class=\"flex items-center gap-1\">\n          <Checkbox\n            v-if=\"multiple\"\n            :checked=\"isSelected && !isNodeDisabled(item)\"\n            :disabled=\"isNodeDisabled(item)\"\n            :indeterminate=\"isIndeterminate && !isNodeDisabled(item)\"\n            @click=\"\n              (event: MouseEvent) => {\n                if (isNodeDisabled(item)) {\n                  event.preventDefault();\n                  event.stopPropagation();\n                  return;\n                }\n                handleSelect();\n              }\n            \"\n          />\n          <div\n            class=\"flex items-center gap-1\"\n            @click=\"\n              (event: MouseEvent) => {\n                if (isNodeDisabled(item)) {\n                  event.preventDefault();\n                  event.stopPropagation();\n                  return;\n                }\n                handleSelect();\n              }\n            \"\n          >\n            <slot name=\"node\" v-bind=\"item\">\n              <IconifyIcon\n                class=\"size-4\"\n                v-if=\"showIcon && get(item.value, iconField)\"\n                :icon=\"get(item.value, iconField)\"\n              />\n              {{ get(item.value, labelField) }}\n            </slot>\n          </div>\n        </div>\n        <div class=\"h-4 w-4\"></div>\n      </TreeItem>\n    </TransitionGroup>\n    <div\n      :class=\"\n        cn('my-0.5 flex w-full items-center p-1', bordered ? 'border-t' : '')\n      \"\n      v-if=\"$slots.footer\"\n    >\n      <slot name=\"footer\"> </slot>\n    </div>\n  </TreeRoot>\n</template>\n<style lang=\"scss\" scoped>\n.container {\n  position: relative;\n  padding: 0;\n  list-style-type: none;\n}\n\n.item {\n  box-sizing: border-box;\n  width: 100%;\n  height: 30px;\n  background-color: #f3f3f3;\n  border: 1px solid #666;\n}\n\n/* 1. 声明过渡效果 */\n.fade-move,\n.fade-enter-active,\n.fade-leave-active {\n  transition: all 0.5s cubic-bezier(0.55, 0, 0.1, 1);\n}\n\n/* 2. 声明进入和离开的状态 */\n.fade-enter-from,\n.fade-leave-to {\n  opacity: 0;\n  transform: scaleY(0.01) translate(30px, 0);\n}\n\n/* 3. 确保离开的项目被移除出了布局流\n      以便正确地计算移动时的动画效果。 */\n.fade-leave-active {\n  position: absolute;\n}\n</style>\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/src/ui/tree/types.ts",
    "content": "import type { Arrayable } from '@vueuse/core';\nimport type { FlattenedItem } from 'radix-vue';\n\nimport type { Recordable } from '@vben-core/typings';\n\nexport interface TreeProps {\n  /** 单选时允许取消已有选项 */\n  allowClear?: boolean;\n  /** 非关联选择时，自动选中上级节点 */\n  autoCheckParent?: boolean;\n  /** 显示边框 */\n  bordered?: boolean;\n  /** 取消父子关联选择 */\n  checkStrictly?: boolean;\n  /** 子级字段名 */\n  childrenField?: string;\n  /** 默认展开的键 */\n  defaultExpandedKeys?: Array<number | string>;\n  /** 默认展开的级别（优先级高于defaultExpandedKeys） */\n  defaultExpandedLevel?: number;\n  /** 默认值 */\n  defaultValue?: Arrayable<number | string>;\n  /** 禁用 */\n  disabled?: boolean;\n  /** 禁用字段名 */\n  disabledField?: string;\n  /** 自定义节点类名 */\n  getNodeClass?: (item: FlattenedItem<Recordable<any>>) => string;\n  iconField?: string;\n  /** label字段 */\n  labelField?: string;\n  /** 是否多选 */\n  multiple?: boolean;\n  /** 显示由iconField指定的图标 */\n  showIcon?: boolean;\n  /** 启用展开收缩动画 */\n  transition?: boolean;\n  /** 树数据 */\n  treeData: Recordable<any>[];\n  /** 值字段 */\n  valueField?: string;\n}\n\nexport function treePropsDefaults() {\n  return {\n    allowClear: false,\n    autoCheckParent: true,\n    bordered: false,\n    checkStrictly: false,\n    defaultExpandedKeys: () => [],\n    defaultExpandedLevel: 0,\n    disabled: false,\n    disabledField: 'disabled',\n    iconField: 'icon',\n    labelField: 'label',\n    multiple: false,\n    showIcon: true,\n    transition: true,\n    valueField: 'value',\n    childrenField: 'children',\n  };\n}\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/tailwind.config.mjs",
    "content": "export { default } from '@vben/tailwind-config';\n"
  },
  {
    "path": "packages/@core/ui-kit/shadcn-ui/tsconfig.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/tsconfig\",\n  \"extends\": \"@vben/tsconfig/web.json\",\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"@vben-core/shadcn-ui/*\": [\"./src/*\"]\n    }\n  },\n  \"include\": [\"src\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "packages/@core/ui-kit/tabs-ui/build.config.ts",
    "content": "import { defineBuildConfig } from 'unbuild';\n\nexport default defineBuildConfig({\n  clean: true,\n  declaration: true,\n  entries: [\n    {\n      builder: 'mkdist',\n      input: './src',\n      loaders: ['vue'],\n      pattern: ['**/*.vue'],\n    },\n    {\n      builder: 'mkdist',\n      format: 'esm',\n      input: './src',\n      loaders: ['js'],\n      pattern: ['**/*.ts'],\n    },\n  ],\n});\n"
  },
  {
    "path": "packages/@core/ui-kit/tabs-ui/package.json",
    "content": "{\n  \"name\": \"@vben-core/tabs-ui\",\n  \"version\": \"5.5.9\",\n  \"homepage\": \"https://github.com/vbenjs/vue-vben-admin\",\n  \"bugs\": \"https://github.com/vbenjs/vue-vben-admin/issues\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/vbenjs/vue-vben-admin.git\",\n    \"directory\": \"packages/@vben-core/uikit/tabs-ui\"\n  },\n  \"license\": \"MIT\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"build\": \"pnpm unbuild\",\n    \"prepublishOnly\": \"npm run build\"\n  },\n  \"files\": [\n    \"dist\"\n  ],\n  \"sideEffects\": [\n    \"**/*.css\"\n  ],\n  \"main\": \"./dist/index.mjs\",\n  \"module\": \"./dist/index.mjs\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./src/index.ts\",\n      \"development\": \"./src/index.ts\",\n      \"default\": \"./dist/index.mjs\"\n    }\n  },\n  \"publishConfig\": {\n    \"exports\": {\n      \".\": {\n        \"default\": \"./dist/index.mjs\"\n      }\n    }\n  },\n  \"dependencies\": {\n    \"@vben-core/composables\": \"workspace:*\",\n    \"@vben-core/icons\": \"workspace:*\",\n    \"@vben-core/shadcn-ui\": \"workspace:*\",\n    \"@vben-core/typings\": \"workspace:*\",\n    \"@vueuse/core\": \"catalog:\",\n    \"vue\": \"catalog:\"\n  }\n}\n"
  },
  {
    "path": "packages/@core/ui-kit/tabs-ui/postcss.config.mjs",
    "content": "export { default } from '@vben/tailwind-config/postcss';\n"
  },
  {
    "path": "packages/@core/ui-kit/tabs-ui/src/components/index.ts",
    "content": "export { default as Tabs } from './tabs/tabs.vue';\nexport { default as TabsChrome } from './tabs-chrome/tabs.vue';\n"
  },
  {
    "path": "packages/@core/ui-kit/tabs-ui/src/components/tabs/tabs.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { TabDefinition } from '@vben-core/typings';\n\nimport type { TabConfig, TabsProps } from '../../types';\n\nimport { computed } from 'vue';\n\nimport { Pin, X } from '@vben-core/icons';\nimport { VbenContextMenu, VbenIcon } from '@vben-core/shadcn-ui';\n\ninterface Props extends TabsProps {}\n\ndefineOptions({\n  name: 'VbenTabs',\n\n  inheritAttrs: false,\n});\nconst props = withDefaults(defineProps<Props>(), {\n  contentClass: 'vben-tabs-content',\n  contextMenus: () => [],\n  tabs: () => [],\n});\n\nconst emit = defineEmits<{\n  close: [string];\n  unpin: [TabDefinition];\n}>();\nconst active = defineModel<string>('active');\n\nconst typeWithClass = computed(() => {\n  const typeClasses: Record<string, { content: string }> = {\n    brisk: {\n      content: `h-full after:content-['']  after:absolute after:bottom-0 after:left-0 after:w-full after:h-[1.5px] after:bg-primary after:scale-x-0 after:transition-[transform] after:ease-out after:duration-300 hover:after:scale-x-100 after:origin-left [&.is-active]:after:scale-x-100 [&:not(:first-child)]:border-l last:border-r last:border-r border-border`,\n    },\n    card: {\n      content:\n        'h-[calc(100%-6px)] rounded-md ml-2 border border-border  transition-all',\n    },\n    plain: {\n      content:\n        'h-full [&:not(:first-child)]:border-l last:border-r border-border',\n    },\n  };\n\n  return typeClasses[props.styleType || 'plain'] || { content: '' };\n});\n\nconst tabsView = computed(() => {\n  return props.tabs.map((tab) => {\n    const { fullPath, meta, name, path, key } = tab || {};\n    const { affixTab, icon, newTabTitle, tabClosable, title } = meta || {};\n    return {\n      affixTab: !!affixTab,\n      closable: Reflect.has(meta, 'tabClosable') ? !!tabClosable : true,\n      fullPath,\n      icon: icon as string,\n      key,\n      meta,\n      name,\n      path,\n      title: (newTabTitle || title || name) as string,\n    } as TabConfig;\n  });\n});\n\nfunction onMouseDown(e: MouseEvent, tab: TabConfig) {\n  if (\n    e.button === 1 &&\n    tab.closable &&\n    !tab.affixTab &&\n    tabsView.value.length > 1 &&\n    props.middleClickToClose\n  ) {\n    e.preventDefault();\n    e.stopPropagation();\n    emit('close', tab.key);\n  }\n}\n</script>\n\n<template>\n  <div\n    :class=\"contentClass\"\n    class=\"relative !flex h-full w-max items-center overflow-hidden pr-6\"\n  >\n    <TransitionGroup name=\"slide-left\">\n      <div\n        v-for=\"(tab, i) in tabsView\"\n        :key=\"tab.key\"\n        :class=\"[\n          {\n            'is-active dark:bg-accent bg-primary/15': tab.key === active,\n            draggable: !tab.affixTab,\n            'affix-tab': tab.affixTab,\n          },\n          typeWithClass.content,\n        ]\"\n        :data-index=\"i\"\n        class=\"tab-item [&:not(.is-active)]:hover:bg-accent translate-all group relative flex cursor-pointer select-none\"\n        data-tab-item=\"true\"\n        @click=\"active = tab.key\"\n        @mousedown=\"onMouseDown($event, tab)\"\n      >\n        <VbenContextMenu\n          :handler-data=\"tab\"\n          :menus=\"contextMenus\"\n          :modal=\"false\"\n          item-class=\"pr-6\"\n        >\n          <div class=\"relative flex size-full items-center\">\n            <!-- extra -->\n            <div\n              class=\"absolute right-1.5 top-1/2 z-[3] translate-y-[-50%] overflow-hidden\"\n            >\n              <!-- close-icon -->\n              <X\n                v-show=\"!tab.affixTab && tabsView.length > 1 && tab.closable\"\n                class=\"hover:bg-accent stroke-accent-foreground/80 hover:stroke-accent-foreground dark:group-[.is-active]:text-accent-foreground group-[.is-active]:text-primary size-3 cursor-pointer rounded-full transition-all\"\n                @click.stop=\"() => emit('close', tab.key)\"\n              />\n              <Pin\n                v-show=\"tab.affixTab && tabsView.length > 1 && tab.closable\"\n                class=\"hover:bg-accent hover:stroke-accent-foreground group-[.is-active]:text-primary dark:group-[.is-active]:text-accent-foreground mt-[1px] size-3.5 cursor-pointer rounded-full transition-all\"\n                @click.stop=\"() => emit('unpin', tab)\"\n              />\n            </div>\n\n            <!-- tab-item-main -->\n            <div\n              class=\"text-accent-foreground group-[.is-active]:text-primary dark:group-[.is-active]:text-accent-foreground mx-3 mr-4 flex h-full items-center overflow-hidden rounded-tl-[5px] rounded-tr-[5px] pr-3 transition-all duration-300\"\n            >\n              <VbenIcon\n                v-if=\"showIcon\"\n                :icon=\"tab.icon\"\n                class=\"mr-2 flex size-4 items-center overflow-hidden\"\n                fallback\n              />\n\n              <span class=\"flex-1 overflow-hidden whitespace-nowrap text-sm\">\n                {{ tab.title }}\n              </span>\n            </div>\n          </div>\n        </VbenContextMenu>\n      </div>\n    </TransitionGroup>\n  </div>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/tabs-ui/src/components/tabs-chrome/tabs.vue",
    "content": "<script setup lang=\"ts\">\nimport type { TabDefinition } from '@vben-core/typings';\n\nimport type { TabConfig, TabsProps } from '../../types';\n\nimport { computed, ref } from 'vue';\n\nimport { Pin, X } from '@vben-core/icons';\nimport { VbenContextMenu, VbenIcon } from '@vben-core/shadcn-ui';\n\ninterface Props extends TabsProps {}\n\ndefineOptions({\n  name: 'VbenTabsChrome',\n  inheritAttrs: false,\n});\n\nconst props = withDefaults(defineProps<Props>(), {\n  contentClass: 'vben-tabs-content',\n  contextMenus: () => [],\n  gap: 7,\n  tabs: () => [],\n});\n\nconst emit = defineEmits<{\n  close: [string];\n  unpin: [TabDefinition];\n}>();\nconst active = defineModel<string>('active');\n\nconst contentRef = ref();\nconst tabRef = ref();\n\nconst style = computed(() => {\n  const { gap } = props;\n  return {\n    '--gap': `${gap}px`,\n  };\n});\n\nconst tabsView = computed(() => {\n  return props.tabs.map((tab) => {\n    const { fullPath, meta, name, path, key } = tab || {};\n    const { affixTab, icon, newTabTitle, tabClosable, title } = meta || {};\n    return {\n      affixTab: !!affixTab,\n      closable: Reflect.has(meta, 'tabClosable') ? !!tabClosable : true,\n      fullPath,\n      icon: icon as string,\n      key,\n      meta,\n      name,\n      path,\n      title: (newTabTitle || title || name) as string,\n    } as TabConfig;\n  });\n});\n\nfunction onMouseDown(e: MouseEvent, tab: TabConfig) {\n  if (\n    e.button === 1 &&\n    tab.closable &&\n    !tab.affixTab &&\n    tabsView.value.length > 1 &&\n    props.middleClickToClose\n  ) {\n    e.preventDefault();\n    e.stopPropagation();\n    emit('close', tab.key);\n  }\n}\n</script>\n\n<template>\n  <div\n    ref=\"contentRef\"\n    :class=\"contentClass\"\n    :style=\"style\"\n    class=\"tabs-chrome !flex h-full w-max overflow-y-hidden pr-6\"\n  >\n    <TransitionGroup name=\"slide-left\">\n      <div\n        v-for=\"(tab, i) in tabsView\"\n        :key=\"tab.key\"\n        ref=\"tabRef\"\n        :class=\"[\n          {\n            'is-active': tab.key === active,\n            draggable: !tab.affixTab,\n            'affix-tab': tab.affixTab,\n          },\n        ]\"\n        :data-active-tab=\"active\"\n        :data-index=\"i\"\n        class=\"tabs-chrome__item draggable translate-all group relative -mr-3 flex h-full select-none items-center\"\n        data-tab-item=\"true\"\n        @click=\"active = tab.key\"\n        @mousedown=\"onMouseDown($event, tab)\"\n      >\n        <VbenContextMenu\n          :handler-data=\"tab\"\n          :menus=\"contextMenus\"\n          :modal=\"false\"\n          item-class=\"pr-6\"\n        >\n          <div class=\"relative size-full px-1\">\n            <!-- divider -->\n            <div\n              v-if=\"i !== 0 && tab.key !== active\"\n              class=\"tabs-chrome__divider bg-border absolute left-[var(--gap)] top-1/2 z-0 h-4 w-[1px] translate-y-[-50%] transition-all\"\n            ></div>\n            <!-- background -->\n            <div\n              class=\"tabs-chrome__background absolute z-[-1] size-full px-[calc(var(--gap)-1px)] py-0 transition-opacity duration-150\"\n            >\n              <div\n                class=\"tabs-chrome__background-content group-[.is-active]:bg-primary/15 dark:group-[.is-active]:bg-accent h-full rounded-tl-[var(--gap)] rounded-tr-[var(--gap)] duration-150\"\n              ></div>\n              <svg\n                class=\"tabs-chrome__background-before group-[.is-active]:fill-primary/15 dark:group-[.is-active]:fill-accent absolute bottom-0 left-[-1px] fill-transparent transition-all duration-150\"\n                height=\"7\"\n                width=\"7\"\n              >\n                <path d=\"M 0 7 A 7 7 0 0 0 7 0 L 7 7 Z\" />\n              </svg>\n              <svg\n                class=\"tabs-chrome__background-after group-[.is-active]:fill-primary/15 dark:group-[.is-active]:fill-accent absolute bottom-0 right-[-1px] fill-transparent transition-all duration-150\"\n                height=\"7\"\n                width=\"7\"\n              >\n                <path d=\"M 0 0 A 7 7 0 0 0 7 7 L 0 7 Z\" />\n              </svg>\n            </div>\n\n            <!-- extra -->\n            <div\n              class=\"tabs-chrome__extra absolute right-[var(--gap)] top-1/2 z-[3] size-4 translate-y-[-50%]\"\n            >\n              <!-- close-icon -->\n              <X\n                v-show=\"!tab.affixTab && tabsView.length > 1 && tab.closable\"\n                class=\"hover:bg-accent stroke-accent-foreground/80 hover:stroke-accent-foreground text-accent-foreground/80 group-[.is-active]:text-accent-foreground mt-[2px] size-3 cursor-pointer rounded-full transition-all\"\n                @click.stop=\"() => emit('close', tab.key)\"\n              />\n              <Pin\n                v-show=\"tab.affixTab && tabsView.length > 1 && tab.closable\"\n                class=\"hover:text-accent-foreground text-accent-foreground/80 group-[.is-active]:text-accent-foreground mt-[1px] size-3.5 cursor-pointer rounded-full transition-all\"\n                @click.stop=\"() => emit('unpin', tab)\"\n              />\n            </div>\n\n            <!-- tab-item-main -->\n            <div\n              class=\"tabs-chrome__item-main group-[.is-active]:text-primary dark:group-[.is-active]:text-accent-foreground text-accent-foreground z-[2] mx-[calc(var(--gap)*2)] my-0 flex h-full items-center overflow-hidden rounded-tl-[5px] rounded-tr-[5px] pl-2 pr-4 duration-150\"\n            >\n              <VbenIcon\n                v-if=\"showIcon\"\n                :icon=\"tab.icon\"\n                class=\"mr-1 flex size-4 items-center overflow-hidden\"\n              />\n              <span class=\"flex-1 overflow-hidden whitespace-nowrap text-sm\">\n                {{ tab.title }}\n              </span>\n            </div>\n          </div>\n        </VbenContextMenu>\n      </div>\n    </TransitionGroup>\n  </div>\n</template>\n\n<style scoped>\n.tabs-chrome {\n  &__item:not(.dragging) {\n    @apply cursor-pointer;\n\n    &:hover:not(.is-active) {\n      & + .tabs-chrome__item {\n        .tabs-chrome__divider {\n          @apply opacity-0;\n        }\n      }\n\n      .tabs-chrome__divider {\n        @apply opacity-0;\n      }\n\n      .tabs-chrome__background {\n        @apply pb-[2px];\n\n        &-content {\n          @apply bg-accent mx-[2px] rounded-md;\n        }\n      }\n    }\n\n    &.is-active {\n      @apply z-[2];\n\n      & + .tabs-chrome__item {\n        .tabs-chrome__divider {\n          @apply opacity-0 !important;\n        }\n      }\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "packages/@core/ui-kit/tabs-ui/src/components/widgets/index.ts",
    "content": "export { default as TabsToolMore } from './tool-more.vue';\nexport { default as TabsToolScreen } from './tool-screen.vue';\n"
  },
  {
    "path": "packages/@core/ui-kit/tabs-ui/src/components/widgets/tool-more.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { DropdownMenuProps } from '@vben-core/shadcn-ui';\n\nimport { ChevronDown } from '@vben-core/icons';\nimport { VbenDropdownMenu } from '@vben-core/shadcn-ui';\n\ndefineProps<DropdownMenuProps>();\n</script>\n\n<template>\n  <VbenDropdownMenu :menus=\"menus\" :modal=\"false\">\n    <div\n      class=\"flex-center hover:bg-muted hover:text-foreground text-muted-foreground border-border h-full cursor-pointer border-l px-2 text-lg font-semibold\"\n    >\n      <ChevronDown class=\"size-4\" />\n    </div>\n  </VbenDropdownMenu>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/tabs-ui/src/components/widgets/tool-screen.vue",
    "content": "<script lang=\"ts\" setup>\nimport { Fullscreen, Minimize2 } from '@vben-core/icons';\n\nconst screen = defineModel<boolean>('screen');\n\nfunction toggleScreen() {\n  screen.value = !screen.value;\n}\n</script>\n\n<template>\n  <div\n    class=\"flex-center hover:bg-muted hover:text-foreground text-muted-foreground border-border h-full cursor-pointer border-l px-2 text-lg font-semibold\"\n    @click=\"toggleScreen\"\n  >\n    <Minimize2 v-if=\"screen\" class=\"size-4\" />\n    <Fullscreen v-else class=\"size-4\" />\n  </div>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/tabs-ui/src/index.ts",
    "content": "export * from './components/widgets';\nexport { default as TabsView } from './tabs-view.vue';\nexport type { IContextMenuItem } from '@vben-core/shadcn-ui';\n"
  },
  {
    "path": "packages/@core/ui-kit/tabs-ui/src/tabs-view.vue",
    "content": "<script setup lang=\"ts\">\nimport type { TabsEmits, TabsProps } from './types';\n\nimport { useForwardPropsEmits } from '@vben-core/composables';\nimport { ChevronLeft, ChevronRight } from '@vben-core/icons';\nimport { VbenScrollbar } from '@vben-core/shadcn-ui';\n\nimport { Tabs, TabsChrome } from './components';\nimport { useTabsDrag } from './use-tabs-drag';\nimport { useTabsViewScroll } from './use-tabs-view-scroll';\n\ninterface Props extends TabsProps {}\n\ndefineOptions({\n  name: 'TabsView',\n});\n\nconst props = withDefaults(defineProps<Props>(), {\n  contentClass: 'vben-tabs-content',\n  draggable: true,\n  styleType: 'chrome',\n  wheelable: true,\n});\n\nconst emit = defineEmits<TabsEmits>();\n\nconst forward = useForwardPropsEmits(props, emit);\n\nconst {\n  handleScrollAt,\n  handleWheel,\n  scrollbarRef,\n  scrollDirection,\n  scrollIsAtLeft,\n  scrollIsAtRight,\n  showScrollButton,\n} = useTabsViewScroll(props);\n\nfunction onWheel(e: WheelEvent) {\n  if (props.wheelable) {\n    handleWheel(e);\n    e.stopPropagation();\n    e.preventDefault();\n  }\n}\n\nuseTabsDrag(props, emit);\n</script>\n\n<template>\n  <div class=\"flex h-full flex-1 overflow-hidden\">\n    <!-- 左侧滚动按钮 -->\n    <span\n      v-show=\"showScrollButton\"\n      :class=\"{\n        'hover:bg-muted text-muted-foreground cursor-pointer': !scrollIsAtLeft,\n        'pointer-events-none opacity-30': scrollIsAtLeft,\n      }\"\n      class=\"border-r px-2\"\n      @click=\"scrollDirection('left')\"\n    >\n      <ChevronLeft class=\"size-4 h-full\" />\n    </span>\n\n    <div\n      :class=\"{\n        'pt-[3px]': styleType === 'chrome',\n      }\"\n      class=\"size-full flex-1 overflow-hidden\"\n    >\n      <VbenScrollbar\n        ref=\"scrollbarRef\"\n        :shadow-bottom=\"false\"\n        :shadow-top=\"false\"\n        class=\"h-full\"\n        horizontal\n        scroll-bar-class=\"z-10 hidden \"\n        shadow\n        shadow-left\n        shadow-right\n        @scroll-at=\"handleScrollAt\"\n        @wheel=\"onWheel\"\n      >\n        <TabsChrome\n          v-if=\"styleType === 'chrome'\"\n          v-bind=\"{ ...forward, ...$attrs, ...$props }\"\n        />\n\n        <Tabs v-else v-bind=\"{ ...forward, ...$attrs, ...$props }\" />\n      </VbenScrollbar>\n    </div>\n\n    <!-- 右侧滚动按钮 -->\n    <span\n      v-show=\"showScrollButton\"\n      :class=\"{\n        'hover:bg-muted text-muted-foreground cursor-pointer': !scrollIsAtRight,\n        'pointer-events-none opacity-30': scrollIsAtRight,\n      }\"\n      class=\"hover:bg-muted text-muted-foreground cursor-pointer border-l px-2\"\n      @click=\"scrollDirection('right')\"\n    >\n      <ChevronRight class=\"size-4 h-full\" />\n    </span>\n  </div>\n</template>\n"
  },
  {
    "path": "packages/@core/ui-kit/tabs-ui/src/types.ts",
    "content": "import type { IContextMenuItem } from '@vben-core/shadcn-ui';\nimport type { TabDefinition, TabsStyleType } from '@vben-core/typings';\n\nexport type TabsEmits = {\n  close: [string];\n  sortTabs: [number, number];\n  unpin: [TabDefinition];\n};\n\nexport interface TabsProps {\n  active?: string;\n  /**\n   * @zh_CN content class\n   * @default tabs-chrome\n   */\n  contentClass?: string;\n  /**\n   * @zh_CN 右键菜单\n   */\n  contextMenus?: (data: any) => IContextMenuItem[];\n  /**\n   * @zh_CN 是否可以拖拽\n   */\n  draggable?: boolean;\n  /**\n   * @zh_CN 间隙\n   * @default 7\n   * 仅限 tabs-chrome\n   */\n  gap?: number;\n  /**\n   * @zh_CN tab 最大宽度\n   * 仅限 tabs-chrome\n   */\n  maxWidth?: number;\n  /**\n   * @zh_CN 点击中键时关闭Tab\n   */\n  middleClickToClose?: boolean;\n\n  /**\n   * @zh_CN tab最小宽度\n   * 仅限 tabs-chrome\n   */\n  minWidth?: number;\n\n  /**\n   * @zh_CN 是否显示图标\n   */\n  showIcon?: boolean;\n  /**\n   * @zh_CN 标签页风格\n   */\n  styleType?: TabsStyleType;\n\n  /**\n   * @zh_CN 选项卡数据\n   */\n  tabs?: TabDefinition[];\n\n  /**\n   * @zh_CN 是否响应滚轮事件\n   */\n  wheelable?: boolean;\n}\n\nexport interface TabConfig extends TabDefinition {\n  affixTab: boolean;\n  closable: boolean;\n  icon: string;\n  key: string;\n  title: string;\n}\n"
  },
  {
    "path": "packages/@core/ui-kit/tabs-ui/src/use-tabs-drag.ts",
    "content": "import type { Sortable } from '@vben-core/composables';\nimport type { EmitType } from '@vben-core/typings';\n\nimport type { TabsProps } from './types';\n\nimport { useIsMobile, useSortable } from '@vben-core/composables';\nimport { nextTick, onMounted, onUnmounted, ref, watch } from 'vue';\n\n// 可能会找到拖拽的子元素，这里需要确保拖拽的dom时tab元素\nfunction findParentElement(element: HTMLElement) {\n  const parentCls = 'group';\n  return element.classList.contains(parentCls)\n    ? element\n    : element.closest(`.${parentCls}`);\n}\n\nexport function useTabsDrag(props: TabsProps, emit: EmitType) {\n  const sortableInstance = ref<null | Sortable>(null);\n\n  async function initTabsSortable() {\n    await nextTick();\n\n    const el = document.querySelectorAll(\n      `.${props.contentClass}`,\n    )?.[0] as HTMLElement;\n\n    if (!el) {\n      console.warn('Element not found for sortable initialization');\n      return;\n    }\n\n    const resetElState = async () => {\n      el.style.cursor = 'default';\n      // el.classList.remove('dragging');\n      el.querySelector('.draggable')?.classList.remove('dragging');\n    };\n\n    const { initializeSortable } = useSortable(el, {\n      filter: (_evt, target: HTMLElement) => {\n        const parent = findParentElement(target);\n        const draggable = parent?.classList.contains('draggable');\n        return !draggable || !props.draggable;\n      },\n      onEnd(evt) {\n        const { newIndex, oldIndex } = evt;\n        // const fromElement = evt.item;\n        const { srcElement } = (evt as any).originalEvent;\n\n        if (!srcElement) {\n          resetElState();\n          return;\n        }\n\n        const srcParent = findParentElement(srcElement);\n\n        if (!srcParent) {\n          resetElState();\n          return;\n        }\n\n        if (!srcParent.classList.contains('draggable')) {\n          resetElState();\n\n          return;\n        }\n\n        if (\n          oldIndex !== undefined &&\n          newIndex !== undefined &&\n          !Number.isNaN(oldIndex) &&\n          !Number.isNaN(newIndex) &&\n          oldIndex !== newIndex\n        ) {\n          emit('sortTabs', oldIndex, newIndex);\n        }\n        resetElState();\n      },\n      onMove(evt) {\n        const parent = findParentElement(evt.related);\n        if (parent?.classList.contains('draggable') && props.draggable) {\n          const isCurrentAffix = evt.dragged.classList.contains('affix-tab');\n          const isRelatedAffix = evt.related.classList.contains('affix-tab');\n          // 不允许在固定的tab和非固定的tab之间互相拖拽\n          return isCurrentAffix === isRelatedAffix;\n        } else {\n          return false;\n        }\n      },\n      onStart: () => {\n        el.style.cursor = 'grabbing';\n        el.querySelector('.draggable')?.classList.add('dragging');\n        // el.classList.add('dragging');\n      },\n    });\n\n    sortableInstance.value = await initializeSortable();\n  }\n\n  async function init() {\n    const { isMobile } = useIsMobile();\n\n    // 移动端下tab不需要拖拽\n    if (isMobile.value) {\n      return;\n    }\n    await nextTick();\n    initTabsSortable();\n  }\n\n  onMounted(init);\n\n  watch(\n    () => props.styleType,\n    () => {\n      sortableInstance.value?.destroy();\n      init();\n    },\n  );\n\n  onUnmounted(() => {\n    sortableInstance.value?.destroy();\n  });\n}\n"
  },
  {
    "path": "packages/@core/ui-kit/tabs-ui/src/use-tabs-view-scroll.ts",
    "content": "import type { TabsProps } from './types';\n\nimport { nextTick, onMounted, onUnmounted, ref, watch } from 'vue';\n\nimport { VbenScrollbar } from '@vben-core/shadcn-ui';\n\nimport { useDebounceFn } from '@vueuse/core';\n\ntype DomElement = Element | null | undefined;\n\nexport function useTabsViewScroll(props: TabsProps) {\n  let resizeObserver: null | ResizeObserver = null;\n  let mutationObserver: MutationObserver | null = null;\n  let tabItemCount = 0;\n  const scrollbarRef = ref<InstanceType<typeof VbenScrollbar> | null>(null);\n  const scrollViewportEl = ref<DomElement>(null);\n  const showScrollButton = ref(false);\n  const scrollIsAtLeft = ref(true);\n  const scrollIsAtRight = ref(false);\n\n  function getScrollClientWidth() {\n    const scrollbarEl = scrollbarRef.value?.$el;\n    if (!scrollbarEl || !scrollViewportEl.value) return {};\n\n    const scrollbarWidth = scrollbarEl.clientWidth;\n    const scrollViewWidth = scrollViewportEl.value.clientWidth;\n\n    return {\n      scrollbarWidth,\n      scrollViewWidth,\n    };\n  }\n\n  function scrollDirection(\n    direction: 'left' | 'right',\n    distance: number = 150,\n  ) {\n    const { scrollbarWidth, scrollViewWidth } = getScrollClientWidth();\n\n    if (!scrollbarWidth || !scrollViewWidth) return;\n\n    if (scrollbarWidth > scrollViewWidth) return;\n\n    scrollViewportEl.value?.scrollBy({\n      behavior: 'smooth',\n      left:\n        direction === 'left'\n          ? -(scrollbarWidth - distance)\n          : +(scrollbarWidth - distance),\n    });\n  }\n\n  async function initScrollbar() {\n    await nextTick();\n\n    const scrollbarEl = scrollbarRef.value?.$el;\n    if (!scrollbarEl) {\n      return;\n    }\n\n    const viewportEl = scrollbarEl?.querySelector(\n      'div[data-radix-scroll-area-viewport]',\n    );\n\n    scrollViewportEl.value = viewportEl;\n    calcShowScrollbarButton();\n\n    await nextTick();\n    scrollToActiveIntoView();\n\n    // 监听大小变化\n    resizeObserver?.disconnect();\n    resizeObserver = new ResizeObserver(\n      useDebounceFn((_entries: ResizeObserverEntry[]) => {\n        calcShowScrollbarButton();\n        scrollToActiveIntoView();\n      }, 100),\n    );\n    resizeObserver.observe(viewportEl);\n\n    tabItemCount = props.tabs?.length || 0;\n    mutationObserver?.disconnect();\n    // 使用 MutationObserver 仅监听子节点数量变化\n    mutationObserver = new MutationObserver(() => {\n      const count = viewportEl.querySelectorAll(\n        `div[data-tab-item=\"true\"]`,\n      ).length;\n\n      if (count > tabItemCount) {\n        scrollToActiveIntoView();\n      }\n\n      if (count !== tabItemCount) {\n        calcShowScrollbarButton();\n        tabItemCount = count;\n      }\n    });\n\n    // 配置为仅监听子节点的添加和移除\n    mutationObserver.observe(viewportEl, {\n      attributes: false,\n      childList: true,\n      subtree: true,\n    });\n  }\n\n  async function scrollToActiveIntoView() {\n    if (!scrollViewportEl.value) {\n      return;\n    }\n    await nextTick();\n    const viewportEl = scrollViewportEl.value;\n    const { scrollbarWidth } = getScrollClientWidth();\n    const { scrollWidth } = viewportEl;\n\n    if (scrollbarWidth >= scrollWidth) {\n      return;\n    }\n\n    requestAnimationFrame(() => {\n      const activeItem = viewportEl?.querySelector('.is-active');\n      activeItem?.scrollIntoView({ behavior: 'smooth', inline: 'start' });\n    });\n  }\n\n  /**\n   * 计算tabs 宽度，用于判断是否显示左右滚动按钮\n   */\n  async function calcShowScrollbarButton() {\n    if (!scrollViewportEl.value) {\n      return;\n    }\n\n    const { scrollbarWidth } = getScrollClientWidth();\n\n    showScrollButton.value =\n      scrollViewportEl.value.scrollWidth > scrollbarWidth;\n  }\n\n  const handleScrollAt = useDebounceFn(({ left, right }) => {\n    scrollIsAtLeft.value = left;\n    scrollIsAtRight.value = right;\n  }, 100);\n\n  function handleWheel({ deltaY }: WheelEvent) {\n    scrollViewportEl.value?.scrollBy({\n      // behavior: 'smooth',\n      left: deltaY * 3,\n    });\n  }\n\n  watch(\n    () => props.active,\n    async () => {\n      // 200为了等待 tab 切换动画完成\n      // setTimeout(() => {\n      scrollToActiveIntoView();\n      // }, 300);\n    },\n    {\n      flush: 'post',\n    },\n  );\n\n  // watch(\n  //   () => props.tabs?.length,\n  //   async () => {\n  //     await nextTick();\n  //     calcShowScrollbarButton();\n  //   },\n  //   {\n  //     flush: 'post',\n  //   },\n  // );\n\n  watch(\n    () => props.styleType,\n    () => {\n      initScrollbar();\n    },\n  );\n\n  onMounted(initScrollbar);\n\n  onUnmounted(() => {\n    resizeObserver?.disconnect();\n    mutationObserver?.disconnect();\n    resizeObserver = null;\n    mutationObserver = null;\n  });\n\n  return {\n    handleScrollAt,\n    handleWheel,\n    initScrollbar,\n    scrollbarRef,\n    scrollDirection,\n    scrollIsAtLeft,\n    scrollIsAtRight,\n    showScrollButton,\n  };\n}\n"
  },
  {
    "path": "packages/@core/ui-kit/tabs-ui/tailwind.config.mjs",
    "content": "export { default } from '@vben/tailwind-config';\n"
  },
  {
    "path": "packages/@core/ui-kit/tabs-ui/tsconfig.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/tsconfig\",\n  \"extends\": \"@vben/tsconfig/web.json\",\n  \"include\": [\"src\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "packages/constants/README.md",
    "content": "# @vben/constants\n\n用于多个 `app` 公用的常量，继承了 `@vben-core/shared/constants` 的所有能力。业务上有通用常量可以放在这里。\n\n## 用法\n\n### 添加依赖\n\n```bash\n# 进入目标应用目录，例如 apps/xxxx-app\n# cd apps/xxxx-app\npnpm add @vben/constants\n```\n\n### 使用\n\n```ts\nimport { LOGIN_PATH } from '@vben/constants';\n```\n"
  },
  {
    "path": "packages/constants/package.json",
    "content": "{\n  \"name\": \"@vben/constants\",\n  \"version\": \"5.5.9\",\n  \"homepage\": \"https://github.com/vbenjs/vue-vben-admin\",\n  \"bugs\": \"https://github.com/vbenjs/vue-vben-admin/issues\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/vbenjs/vue-vben-admin.git\",\n    \"directory\": \"packages/constants\"\n  },\n  \"license\": \"MIT\",\n  \"type\": \"module\",\n  \"sideEffects\": [\n    \"**/*.css\"\n  ],\n  \"exports\": {\n    \".\": {\n      \"types\": \"./src/index.ts\",\n      \"default\": \"./src/index.ts\"\n    }\n  },\n  \"dependencies\": {\n    \"@vben-core/shared\": \"workspace:*\"\n  }\n}\n"
  },
  {
    "path": "packages/constants/src/core.ts",
    "content": "/**\n * @zh_CN 登录页面 url 地址\n */\nexport const LOGIN_PATH = '/auth/login';\n\nexport interface LanguageOption {\n  label: string;\n  value: 'en-US' | 'zh-CN';\n}\n\n/**\n * Supported languages\n */\nexport const SUPPORT_LANGUAGES: LanguageOption[] = [\n  {\n    label: '简体中文',\n    value: 'zh-CN',\n  },\n  {\n    label: 'English',\n    value: 'en-US',\n  },\n];\n\n/**\n * 默认租户ID\n */\nexport const DEFAULT_TENANT_ID = '000000';\n\n/**\n * 业务成功 状态码\n */\nexport const BUSINESS_SUCCESS_CODE = 200;\n\n/**\n * 未授权 状态码(登录超时)\n */\nexport const UNAUTHORIZED_CODE = 401;\n"
  },
  {
    "path": "packages/constants/src/index.ts",
    "content": "export * from './core';\nexport * from '@vben-core/shared/constants';\n"
  },
  {
    "path": "packages/constants/tsconfig.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/tsconfig\",\n  \"extends\": \"@vben/tsconfig/web.json\",\n  \"include\": [\"src\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "packages/effects/README.md",
    "content": "## Effects 目录\n\n`effects` 目录专门用于存放与轻微耦合相关的代码和逻辑。如果你的包具有以下特点，建议将其放置在 `effects` 目录下：\n\n- **状态管理**：使用状态管理框架 `pinia`，并包含处理副作用（如异步操作、API 调用）的部分。\n- **用户偏好设置**：使用 `@vben-core/preferences` 处理用户偏好设置，涉及本地存储或浏览器缓存逻辑（如使用 `localStorage`）。\n- **导航和路由**：处理导航、页面跳转等场景，需要管理路由变化的逻辑。\n- **组件库依赖**：包含与特定组件库紧密耦合或依赖大型仓库的部分。\n\n通过将相关代码归类到 `effects` 目录，可以使项目结构更加清晰，便于维护和扩展。\n"
  },
  {
    "path": "packages/effects/access/package.json",
    "content": "{\n  \"name\": \"@vben/access\",\n  \"version\": \"5.5.9\",\n  \"homepage\": \"https://github.com/vbenjs/vue-vben-admin\",\n  \"bugs\": \"https://github.com/vbenjs/vue-vben-admin/issues\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/vbenjs/vue-vben-admin.git\",\n    \"directory\": \"packages/effects/permissions\"\n  },\n  \"license\": \"MIT\",\n  \"type\": \"module\",\n  \"sideEffects\": [\n    \"**/*.css\"\n  ],\n  \"exports\": {\n    \".\": {\n      \"types\": \"./src/index.ts\",\n      \"default\": \"./src/index.ts\"\n    }\n  },\n  \"dependencies\": {\n    \"@vben/preferences\": \"workspace:*\",\n    \"@vben/stores\": \"workspace:*\",\n    \"@vben/types\": \"workspace:*\",\n    \"@vben/utils\": \"workspace:*\",\n    \"vue\": \"catalog:\"\n  }\n}\n"
  },
  {
    "path": "packages/effects/access/src/access-control.vue",
    "content": "<!--\n Access control component for fine-grained access control.\n TODO: 可以扩展更完善的功能：\n 1. 支持多个权限码，只要有一个权限码满足即可 或者 多个权限码全部满足\n 2. 支持多个角色，只要有一个角色满足即可 或者 多个角色全部满足\n 3. 支持自定义权限码和角色的判断逻辑\n-->\n<script lang=\"ts\" setup>\nimport { computed } from 'vue';\n\nimport { useAccess } from './use-access';\n\ninterface Props {\n  /**\n   * Specified codes is visible\n   * @default []\n   */\n  codes?: string[];\n\n  /**\n   * 通过什么方式来控制组件，如果是 role，则传入角色，如果是 code，则传入权限码\n   * @default 'role'\n   */\n  type?: 'code' | 'role';\n}\n\ndefineOptions({\n  name: 'AccessControl',\n});\n\nconst props = withDefaults(defineProps<Props>(), {\n  codes: () => [],\n  type: 'role',\n});\n\nconst { hasAccessByCodes, hasAccessByRoles } = useAccess();\n\nconst hasAuth = computed(() => {\n  const { codes, type } = props;\n  return type === 'role' ? hasAccessByRoles(codes) : hasAccessByCodes(codes);\n});\n</script>\n\n<template>\n  <slot v-if=\"!codes\"></slot>\n  <slot v-else-if=\"hasAuth\"></slot>\n</template>\n"
  },
  {
    "path": "packages/effects/access/src/accessible.ts",
    "content": "import type { Component, DefineComponent } from 'vue';\n\nimport type {\n  AccessModeType,\n  GenerateMenuAndRoutesOptions,\n  RouteRecordRaw,\n} from '@vben/types';\n\nimport { defineComponent, h } from 'vue';\n\nimport {\n  cloneDeep,\n  generateMenus,\n  generateRoutesByBackend,\n  generateRoutesByFrontend,\n  isFunction,\n  isString,\n  mapTree,\n  setObjToUrlParams,\n} from '@vben/utils';\n\nasync function generateAccessible(\n  mode: AccessModeType,\n  options: GenerateMenuAndRoutesOptions,\n) {\n  const { router } = options;\n\n  options.routes = cloneDeep(options.routes);\n  // 生成路由\n  const accessibleRoutes = await generateRoutes(mode, options);\n\n  const root = router.getRoutes().find((item) => item.path === '/');\n\n  // 获取已有的路由名称列表\n  const names = root?.children?.map((item) => item.name) ?? [];\n\n  // 动态添加到router实例内\n  accessibleRoutes.forEach((route) => {\n    /**\n     * 外链不应该被添加到路由 由menu处理\n     */\n    if (/^https?:\\/\\//.test(route.path)) {\n      return;\n    }\n    if (root && !route.meta?.noBasicLayout) {\n      // 为了兼容之前的版本用法，如果包含子路由，则将component移除，以免出现多层BasicLayout\n      // 如果你的项目已经跟进了本次修改，移除了所有自定义菜单首级的BasicLayout，可以将这段if代码删除\n      // TODO: 这里后期需要follow更新\n      if (route.children && route.children.length > 0) {\n        delete route.component;\n      }\n      // 根据router name判断，如果路由已经存在，则不再添加\n      if (names?.includes(route.name)) {\n        // 找到已存在的路由索引并更新，不更新会造成切换用户时，一级目录未更新，homePath 在二级目录导致的404问题\n        const index = root.children?.findIndex(\n          (item) => item.name === route.name,\n        );\n        if (index !== undefined && index !== -1 && root.children) {\n          root.children[index] = route;\n        }\n      } else {\n        root.children?.push(route);\n      }\n    } else {\n      router.addRoute(route);\n    }\n  });\n\n  if (root) {\n    if (root.name) {\n      router.removeRoute(root.name);\n    }\n    router.addRoute(root);\n  }\n\n  // 生成菜单\n  const accessibleMenus = generateMenus(accessibleRoutes, options.router);\n\n  return { accessibleMenus, accessibleRoutes };\n}\n\n/**\n * Generate routes\n * @param mode\n * @param options\n */\nasync function generateRoutes(\n  mode: AccessModeType,\n  options: GenerateMenuAndRoutesOptions,\n) {\n  const { forbiddenComponent, roles, routes } = options;\n\n  let resultRoutes: RouteRecordRaw[] = routes;\n  switch (mode) {\n    case 'backend': {\n      resultRoutes = await generateRoutesByBackend(options);\n      break;\n    }\n    case 'frontend': {\n      resultRoutes = await generateRoutesByFrontend(\n        routes,\n        roles || [],\n        forbiddenComponent,\n      );\n      break;\n    }\n    case 'mixed': {\n      const [frontend_resultRoutes, backend_resultRoutes] = await Promise.all([\n        generateRoutesByFrontend(routes, roles || [], forbiddenComponent),\n        generateRoutesByBackend(options),\n      ]);\n\n      resultRoutes = [...frontend_resultRoutes, ...backend_resultRoutes];\n      break;\n    }\n  }\n\n  /**\n   * 调整路由树，做以下处理：\n   * 1. 对未添加redirect的路由添加redirect\n   * 2. 将懒加载的组件名称修改为当前路由的名称（如果启用了keep-alive的话）\n   */\n  resultRoutes = mapTree(resultRoutes, (route) => {\n    // 重新包装component，使用与路由名称相同的name以支持keep-alive的条件缓存。\n    if (\n      route.meta?.keepAlive &&\n      isFunction(route.component) &&\n      route.name &&\n      isString(route.name)\n    ) {\n      const originalComponent = route.component as () => Promise<{\n        default: Component | DefineComponent;\n      }>;\n      route.component = async () => {\n        const component = await originalComponent();\n        if (!component.default) return component;\n        return defineComponent({\n          name: route.name as string,\n          setup(props, { attrs, slots }) {\n            return () => h(component.default, { ...props, ...attrs }, slots);\n          },\n        });\n      };\n    }\n\n    // 如果有redirect或者没有子路由，则直接返回\n    if (route.redirect || !route.children || route.children.length === 0) {\n      return route;\n    }\n    const firstChild = route.children[0];\n\n    // 如果子路由不是以/开头，则直接返回,这种情况需要计算全部父级的path才能得出正确的path，这里不做处理\n    if (!firstChild?.path || !firstChild.path.startsWith('/')) {\n      return route;\n    }\n\n    // 第一个路由如果有query参数 需要加上参数\n    const fistChildQuery = route.children[0]?.meta?.query;\n    // 根目录菜单固定只有一个children 且path为/ 不需要添加redirect\n    route.redirect =\n      fistChildQuery && route.children.length !== 1 && route.path !== '/'\n        ? setObjToUrlParams(firstChild.path, fistChildQuery)\n        : firstChild.path;\n\n    return route;\n  });\n\n  return resultRoutes;\n}\n\nexport { generateAccessible };\n"
  },
  {
    "path": "packages/effects/access/src/directive.ts",
    "content": "/**\n * Global authority directive\n * Used for fine-grained control of component permissions\n * @Example v-access:role=\"[ROLE_NAME]\" or v-access:role=\"ROLE_NAME\"\n * @Example v-access:code=\"[ROLE_CODE]\" or v-access:code=\"ROLE_CODE\"\n */\nimport type { App, Directive, DirectiveBinding } from 'vue';\n\nimport { useAccess } from './use-access';\n\nfunction isAccessible(\n  el: Element,\n  binding: DirectiveBinding<string | string[]>,\n) {\n  const { hasAccessByCodes, hasAccessByRoles } = useAccess();\n\n  const value = binding.value;\n\n  if (!value) return;\n  const authMethod =\n    binding.arg === 'role' ? hasAccessByRoles : hasAccessByCodes;\n\n  const values = Array.isArray(value) ? value : [value];\n\n  if (!authMethod(values)) {\n    el?.remove();\n  }\n}\n\nconst mounted = (el: Element, binding: DirectiveBinding<string | string[]>) => {\n  isAccessible(el, binding);\n};\n\nconst authDirective: Directive = {\n  mounted,\n};\n\nexport function registerAccessDirective(app: App) {\n  app.directive('access', authDirective);\n}\n"
  },
  {
    "path": "packages/effects/access/src/index.ts",
    "content": "export { default as AccessControl } from './access-control.vue';\nexport * from './accessible';\nexport * from './directive';\nexport * from './use-access';\n"
  },
  {
    "path": "packages/effects/access/src/use-access.ts",
    "content": "import { computed } from 'vue';\n\nimport { preferences, updatePreferences } from '@vben/preferences';\nimport { useAccessStore, useUserStore } from '@vben/stores';\n\nfunction useAccess() {\n  const accessStore = useAccessStore();\n  const userStore = useUserStore();\n  const accessMode = computed(() => {\n    return preferences.app.accessMode;\n  });\n\n  /**\n   * 基于角色判断是否有权限\n   * @description: Determine whether there is permission，The role is judged by the user's role\n   * @param roles\n   */\n  function hasAccessByRoles(roles: string[]) {\n    const userRoleSet = new Set(userStore.userRoles);\n    // 超管的角色\n    if (userRoleSet.has('superadmin')) {\n      return true;\n    }\n    const intersection = roles.filter((item) => userRoleSet.has(item));\n    return intersection.length > 0;\n  }\n\n  /**\n   * 基于权限码判断是否有权限\n   * @description: Determine whether there is permission，The permission code is judged by the user's permission code\n   * @param codes\n   */\n  function hasAccessByCodes(codes: string[]) {\n    const userCodesSet = new Set(accessStore.accessCodes);\n    /**\n     * 管理员权限\n     */\n    if (userCodesSet.has('*:*:*')) {\n      return true;\n    }\n    // 其他 判断是否存在\n    const intersection = codes.filter((item) => userCodesSet.has(item));\n    return intersection.length > 0;\n  }\n\n  async function toggleAccessMode() {\n    updatePreferences({\n      app: {\n        accessMode:\n          preferences.app.accessMode === 'frontend' ? 'backend' : 'frontend',\n      },\n    });\n  }\n\n  return {\n    accessMode,\n    hasAccessByCodes,\n    hasAccessByRoles,\n    toggleAccessMode,\n  };\n}\n\nexport { useAccess };\n"
  },
  {
    "path": "packages/effects/access/tsconfig.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/tsconfig\",\n  \"extends\": \"@vben/tsconfig/web.json\",\n  \"include\": [\"src\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "packages/effects/common-ui/package.json",
    "content": "{\n  \"name\": \"@vben/common-ui\",\n  \"version\": \"5.5.9\",\n  \"homepage\": \"https://github.com/vbenjs/vue-vben-admin\",\n  \"bugs\": \"https://github.com/vbenjs/vue-vben-admin/issues\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/vbenjs/vue-vben-admin.git\",\n    \"directory\": \"packages/effects/common-ui\"\n  },\n  \"license\": \"MIT\",\n  \"type\": \"module\",\n  \"sideEffects\": [\n    \"**/*.css\"\n  ],\n  \"exports\": {\n    \".\": {\n      \"types\": \"./src/index.ts\",\n      \"default\": \"./src/index.ts\"\n    },\n    \"./es/tippy\": {\n      \"types\": \"./src/components/tippy/index.ts\",\n      \"default\": \"./src/components/tippy/index.ts\"\n    },\n    \"./es/loading\": {\n      \"types\": \"./src/components/loading/index.ts\",\n      \"default\": \"./src/components/loading/index.ts\"\n    }\n  },\n  \"dependencies\": {\n    \"@codemirror/lang-html\": \"^6.4.9\",\n    \"@codemirror/lang-java\": \"^6.0.1\",\n    \"@codemirror/lang-javascript\": \"^6.2.2\",\n    \"@codemirror/lang-sql\": \"^6.7.1\",\n    \"@codemirror/lang-vue\": \"^0.1.3\",\n    \"@codemirror/lang-xml\": \"^6.1.0\",\n    \"@codemirror/theme-one-dark\": \"^6.1.2\",\n    \"@vben-core/form-ui\": \"workspace:*\",\n    \"@vben-core/popup-ui\": \"workspace:*\",\n    \"@vben-core/preferences\": \"workspace:*\",\n    \"@vben-core/shadcn-ui\": \"workspace:*\",\n    \"@vben-core/shared\": \"workspace:*\",\n    \"@vben/constants\": \"workspace:*\",\n    \"@vben/hooks\": \"workspace:*\",\n    \"@vben/icons\": \"workspace:*\",\n    \"@vben/locales\": \"workspace:*\",\n    \"@vben/preferences\": \"workspace:*\",\n    \"@vben/types\": \"workspace:*\",\n    \"@vueuse/core\": \"catalog:\",\n    \"@vueuse/integrations\": \"catalog:\",\n    \"codemirror\": \"6.0.1\",\n    \"json-bigint\": \"catalog:\",\n    \"qrcode\": \"catalog:\",\n    \"tippy.js\": \"catalog:\",\n    \"vditor\": \"3.10.9\",\n    \"vue\": \"catalog:\",\n    \"vue-codemirror6\": \"1.3.4\",\n    \"vue-json-pretty\": \"^2.4.0\",\n    \"vue-json-viewer\": \"catalog:\",\n    \"vue-router\": \"catalog:\",\n    \"vue-tippy\": \"catalog:\"\n  },\n  \"devDependencies\": {\n    \"@types/qrcode\": \"catalog:\"\n  }\n}\n"
  },
  {
    "path": "packages/effects/common-ui/src/components/api-component/api-component.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { Component } from 'vue';\n\nimport type { AnyPromiseFunction } from '@vben/types';\n\nimport { computed, nextTick, ref, unref, useAttrs, watch } from 'vue';\n\nimport { LoaderCircle } from '@vben/icons';\n\nimport { cloneDeep, get, isEqual, isFunction } from '@vben-core/shared/utils';\n\nimport { objectOmit } from '@vueuse/core';\n\ntype OptionsItem = {\n  [name: string]: any;\n  children?: OptionsItem[];\n  disabled?: boolean;\n  label?: string;\n  value?: string;\n};\n\ninterface Props {\n  /** 组件 */\n  component: Component;\n  /** 是否将value从数字转为string */\n  numberToString?: boolean;\n  /** 获取options数据的函数 */\n  api?: (arg?: any) => Promise<OptionsItem[] | Record<string, any>>;\n  /** 传递给api的参数 */\n  params?: Record<string, any>;\n  /** 从api返回的结果中提取options数组的字段名 */\n  resultField?: string;\n  /** label字段名 */\n  labelField?: string;\n  /** children字段名，需要层级数据的组件可用 */\n  childrenField?: string;\n  /** value字段名 */\n  valueField?: string;\n  /** 组件接收options数据的属性名 */\n  optionsPropName?: string;\n  /** 是否立即调用api */\n  immediate?: boolean;\n  /** 每次`visibleEvent`事件发生时都重新请求数据 */\n  alwaysLoad?: boolean;\n  /** 在api请求之前的回调函数 */\n  beforeFetch?: AnyPromiseFunction<any, any>;\n  /** 在api请求之后的回调函数 */\n  afterFetch?: AnyPromiseFunction<any, any>;\n  /** 直接传入选项数据，也作为api返回空数据时的后备数据 */\n  options?: OptionsItem[];\n  /** 组件的插槽名称，用来显示一个\"加载中\"的图标 */\n  loadingSlot?: string;\n  /** 触发api请求的事件名 */\n  visibleEvent?: string;\n  /** 组件的v-model属性名，默认为modelValue。部分组件可能为value */\n  modelPropName?: string;\n  /**\n   * 自动选择\n   * - `first`：自动选择第一个选项\n   * - `last`：自动选择最后一个选项\n   * - `one`: 当请求的结果只有一个选项时，自动选择该选项\n   * - 函数：自定义选择逻辑，函数的参数为请求的结果数组，返回值为选择的选项\n   * - false：不自动选择(默认)\n   */\n  autoSelect?:\n    | 'first'\n    | 'last'\n    | 'one'\n    | ((item: OptionsItem[]) => OptionsItem)\n    | false;\n}\n\ndefineOptions({ name: 'ApiComponent', inheritAttrs: false });\n\nconst props = withDefaults(defineProps<Props>(), {\n  labelField: 'label',\n  valueField: 'value',\n  childrenField: '',\n  optionsPropName: 'options',\n  resultField: '',\n  visibleEvent: '',\n  numberToString: false,\n  params: () => ({}),\n  immediate: true,\n  alwaysLoad: false,\n  loadingSlot: '',\n  beforeFetch: undefined,\n  afterFetch: undefined,\n  modelPropName: 'modelValue',\n  api: undefined,\n  autoSelect: false,\n  options: () => [],\n});\n\nconst emit = defineEmits<{\n  optionsChange: [OptionsItem[]];\n}>();\n\nconst modelValue = defineModel<any>({ default: undefined });\n\nconst attrs = useAttrs();\nconst innerParams = ref({});\nconst refOptions = ref<OptionsItem[]>([]);\nconst loading = ref(false);\n// 首次是否加载过了\nconst isFirstLoaded = ref(false);\n// 标记是否有待处理的请求\nconst hasPendingRequest = ref(false);\n\nconst getOptions = computed(() => {\n  const { labelField, valueField, childrenField, numberToString } = props;\n\n  const refOptionsData = unref(refOptions);\n\n  function transformData(data: OptionsItem[]): OptionsItem[] {\n    return data.map((item) => {\n      const value = get(item, valueField);\n      return {\n        ...objectOmit(item, [labelField, valueField, childrenField]),\n        label: get(item, labelField),\n        value: numberToString ? `${value}` : value,\n        ...(childrenField && item[childrenField]\n          ? { children: transformData(item[childrenField]) }\n          : {}),\n      };\n    });\n  }\n\n  const data: OptionsItem[] = transformData(refOptionsData);\n\n  return data.length > 0 ? data : props.options;\n});\n\nconst bindProps = computed(() => {\n  return {\n    [props.modelPropName]: unref(modelValue),\n    [props.optionsPropName]: unref(getOptions),\n    [`onUpdate:${props.modelPropName}`]: (val: string) => {\n      modelValue.value = val;\n    },\n    ...objectOmit(attrs, [`onUpdate:${props.modelPropName}`]),\n    ...(props.visibleEvent\n      ? {\n          [props.visibleEvent]: handleFetchForVisible,\n        }\n      : {}),\n  };\n});\n\nasync function fetchApi() {\n  const { api, beforeFetch, afterFetch, resultField } = props;\n\n  if (!api || !isFunction(api)) {\n    return;\n  }\n\n  // 如果正在加载，标记有待处理的请求并返回\n  if (loading.value) {\n    hasPendingRequest.value = true;\n    return;\n  }\n\n  refOptions.value = [];\n  try {\n    loading.value = true;\n    let finalParams = unref(mergedParams);\n    if (beforeFetch && isFunction(beforeFetch)) {\n      finalParams = (await beforeFetch(cloneDeep(finalParams))) || finalParams;\n    }\n    let res = await api(finalParams);\n    if (afterFetch && isFunction(afterFetch)) {\n      res = (await afterFetch(res)) || res;\n    }\n    isFirstLoaded.value = true;\n    if (Array.isArray(res)) {\n      refOptions.value = res;\n      emitChange();\n      return;\n    }\n    if (resultField) {\n      refOptions.value = get(res, resultField) || [];\n    }\n    emitChange();\n  } catch (error) {\n    console.warn(error);\n    // reset status\n    isFirstLoaded.value = false;\n  } finally {\n    loading.value = false;\n    // 如果有待处理的请求，立即触发新的请求\n    if (hasPendingRequest.value) {\n      hasPendingRequest.value = false;\n      // 使用 nextTick 确保状态更新完成后再触发新请求\n      await nextTick();\n      fetchApi();\n    }\n  }\n}\n\nasync function handleFetchForVisible(visible: boolean) {\n  if (visible) {\n    if (props.alwaysLoad) {\n      await fetchApi();\n    } else if (!props.immediate && !unref(isFirstLoaded)) {\n      await fetchApi();\n    }\n  }\n}\n\nconst mergedParams = computed(() => {\n  return {\n    ...props.params,\n    ...unref(innerParams),\n  };\n});\n\nwatch(\n  mergedParams,\n  (value, oldValue) => {\n    if (isEqual(value, oldValue)) {\n      return;\n    }\n    fetchApi();\n  },\n  { deep: true, immediate: props.immediate },\n);\n\nfunction emitChange() {\n  if (\n    modelValue.value === undefined &&\n    props.autoSelect &&\n    unref(getOptions).length > 0\n  ) {\n    let firstOption;\n    if (isFunction(props.autoSelect)) {\n      firstOption = props.autoSelect(unref(getOptions));\n    } else {\n      switch (props.autoSelect) {\n        case 'first': {\n          firstOption = unref(getOptions)[0];\n          break;\n        }\n        case 'last': {\n          firstOption = unref(getOptions)[unref(getOptions).length - 1];\n          break;\n        }\n        case 'one': {\n          if (unref(getOptions).length === 1) {\n            firstOption = unref(getOptions)[0];\n          }\n          break;\n        }\n      }\n    }\n\n    if (firstOption) modelValue.value = firstOption.value;\n  }\n  emit('optionsChange', unref(getOptions));\n}\nconst componentRef = ref();\ndefineExpose({\n  /** 获取options数据 */\n  getOptions: () => unref(getOptions),\n  /** 获取当前值 */\n  getValue: () => unref(modelValue),\n  /** 获取被包装的组件实例 */\n  getComponentRef: <T = any,>() => componentRef.value as T,\n  /** 更新Api参数 */\n  updateParam(newParams: Record<string, any>) {\n    innerParams.value = newParams;\n  },\n});\n</script>\n<template>\n  <component\n    :is=\"component\"\n    v-bind=\"bindProps\"\n    :placeholder=\"$attrs.placeholder\"\n    ref=\"componentRef\"\n  >\n    <template v-for=\"item in Object.keys($slots)\" #[item]=\"data\">\n      <slot :name=\"item\" v-bind=\"data || {}\"></slot>\n    </template>\n    <template v-if=\"loadingSlot && loading\" #[loadingSlot]>\n      <LoaderCircle class=\"animate-spin\" />\n    </template>\n  </component>\n</template>\n"
  },
  {
    "path": "packages/effects/common-ui/src/components/api-component/index.ts",
    "content": "export { default as ApiComponent } from './api-component.vue';\n"
  },
  {
    "path": "packages/effects/common-ui/src/components/captcha/hooks/useCaptchaPoints.ts",
    "content": "import type { CaptchaPoint } from '../types';\n\nimport { reactive } from 'vue';\n\nexport function useCaptchaPoints() {\n  const points = reactive<CaptchaPoint[]>([]);\n  function addPoint(point: CaptchaPoint) {\n    points.push(point);\n  }\n\n  function clearPoints() {\n    points.splice(0);\n  }\n  return {\n    addPoint,\n    clearPoints,\n    points,\n  };\n}\n"
  },
  {
    "path": "packages/effects/common-ui/src/components/captcha/index.ts",
    "content": "export { default as PointSelectionCaptcha } from './point-selection-captcha/index.vue';\nexport { default as PointSelectionCaptchaCard } from './point-selection-captcha/index.vue';\n\nexport { default as SliderCaptcha } from './slider-captcha/index.vue';\nexport { default as SliderRotateCaptcha } from './slider-rotate-captcha/index.vue';\nexport { default as SliderTranslateCaptcha } from './slider-translate-captcha/index.vue';\nexport type * from './types';\n"
  },
  {
    "path": "packages/effects/common-ui/src/components/captcha/point-selection-captcha/index.vue",
    "content": "<script setup lang=\"ts\">\nimport type { CaptchaPoint, PointSelectionCaptchaProps } from '../types';\n\nimport { RotateCw } from '@vben/icons';\nimport { $t } from '@vben/locales';\nimport { VbenButton, VbenIconButton } from '@vben-core/shadcn-ui';\n\nimport { useCaptchaPoints } from '../hooks/useCaptchaPoints';\nimport CaptchaCard from './point-selection-captcha-card.vue';\n\nconst props = withDefaults(defineProps<PointSelectionCaptchaProps>(), {\n  height: '220px',\n  hintImage: '',\n  hintText: '',\n  paddingX: '12px',\n  paddingY: '16px',\n  showConfirm: false,\n  title: '',\n  width: '300px',\n});\nconst emit = defineEmits<{\n  click: [CaptchaPoint];\n  confirm: [Array<CaptchaPoint>, clear: () => void];\n  refresh: [];\n}>();\nconst { addPoint, clearPoints, points } = useCaptchaPoints();\n\nif (!props.hintImage && !props.hintText) {\n  console.warn('At least one of hint image or hint text must be provided');\n}\n\nconst POINT_OFFSET = 11;\n\nfunction getElementPosition(element: HTMLElement) {\n  const rect = element.getBoundingClientRect();\n  return {\n    x: rect.left + window.scrollX,\n    y: rect.top + window.scrollY,\n  };\n}\n\nfunction handleClick(e: MouseEvent) {\n  try {\n    const dom = e.currentTarget as HTMLElement;\n    if (!dom) throw new Error('Element not found');\n\n    const { x: domX, y: domY } = getElementPosition(dom);\n\n    const mouseX = e.clientX + window.scrollX;\n    const mouseY = e.clientY + window.scrollY;\n\n    if (typeof mouseX !== 'number' || typeof mouseY !== 'number') {\n      throw new TypeError('Mouse coordinates not found');\n    }\n\n    const xPos = mouseX - domX;\n    const yPos = mouseY - domY;\n\n    const rect = dom.getBoundingClientRect();\n\n    // 点击位置边界校验\n    if (xPos < 0 || yPos < 0 || xPos > rect.width || yPos > rect.height) {\n      console.warn('Click position is out of the valid range');\n      return;\n    }\n\n    const x = Math.ceil(xPos);\n    const y = Math.ceil(yPos);\n\n    const point = {\n      i: points.length,\n      t: Date.now(),\n      x,\n      y,\n    };\n\n    addPoint(point);\n\n    emit('click', point);\n    e.stopPropagation();\n    e.preventDefault();\n  } catch (error) {\n    console.error('Error in handleClick:', error);\n  }\n}\n\nfunction clear() {\n  try {\n    clearPoints();\n  } catch (error) {\n    console.error('Error in clear:', error);\n  }\n}\n\nfunction handleRefresh() {\n  try {\n    clear();\n    emit('refresh');\n  } catch (error) {\n    console.error('Error in handleRefresh:', error);\n  }\n}\n\nfunction handleConfirm() {\n  if (!props.showConfirm) return;\n  try {\n    emit('confirm', points, clear);\n  } catch (error) {\n    console.error('Error in handleConfirm:', error);\n  }\n}\n</script>\n<template>\n  <CaptchaCard\n    :captcha-image=\"captchaImage\"\n    :height=\"height\"\n    :padding-x=\"paddingX\"\n    :padding-y=\"paddingY\"\n    :title=\"title\"\n    :width=\"width\"\n    @click=\"handleClick\"\n  >\n    <template #title>\n      <slot name=\"title\">{{ $t('ui.captcha.title') }}</slot>\n    </template>\n\n    <template #extra>\n      <VbenIconButton\n        :aria-label=\"$t('ui.captcha.refreshAriaLabel')\"\n        class=\"ml-1\"\n        @click=\"handleRefresh\"\n      >\n        <RotateCw class=\"size-5\" />\n      </VbenIconButton>\n      <VbenButton\n        v-if=\"showConfirm\"\n        :aria-label=\"$t('ui.captcha.confirmAriaLabel')\"\n        class=\"ml-2\"\n        size=\"sm\"\n        @click=\"handleConfirm\"\n      >\n        {{ $t('ui.captcha.confirm') }}\n      </VbenButton>\n    </template>\n\n    <div\n      v-for=\"(point, index) in points\"\n      :key=\"index\"\n      :aria-label=\"$t('ui.captcha.pointAriaLabel') + (index + 1)\"\n      :style=\"{\n        top: `${point.y - POINT_OFFSET}px`,\n        left: `${point.x - POINT_OFFSET}px`,\n      }\"\n      class=\"bg-primary text-primary-50 border-primary-50 absolute z-20 flex h-5 w-5 cursor-default items-center justify-center rounded-full border-2\"\n      role=\"button\"\n      tabindex=\"0\"\n    >\n      {{ index + 1 }}\n    </div>\n    <template #footer>\n      <img\n        v-if=\"hintImage\"\n        :alt=\"$t('ui.captcha.alt')\"\n        :src=\"hintImage\"\n        class=\"border-border h-10 w-full rounded border\"\n      />\n      <div\n        v-else-if=\"hintText\"\n        class=\"border-border flex-center h-10 w-full rounded border\"\n      >\n        {{ `${$t('ui.captcha.clickInOrder')}` + `【${hintText}】` }}\n      </div>\n    </template>\n  </CaptchaCard>\n</template>\n"
  },
  {
    "path": "packages/effects/common-ui/src/components/captcha/point-selection-captcha/point-selection-captcha-card.vue",
    "content": "<script setup lang=\"ts\">\nimport type { PointSelectionCaptchaCardProps } from '../types';\n\nimport { $t } from '@vben/locales';\nimport {\n  Card,\n  CardContent,\n  CardFooter,\n  CardHeader,\n  CardTitle,\n} from '@vben-core/shadcn-ui';\nimport { computed } from 'vue';\n\nconst props = withDefaults(defineProps<PointSelectionCaptchaCardProps>(), {\n  height: '220px',\n  paddingX: '12px',\n  paddingY: '16px',\n  title: '',\n  width: '300px',\n});\n\nconst emit = defineEmits<{\n  click: [MouseEvent];\n}>();\n\nconst parseValue = (value: number | string) => {\n  if (typeof value === 'number') {\n    return value;\n  }\n  const parsed = Number.parseFloat(value);\n  return Number.isNaN(parsed) ? 0 : parsed;\n};\n\nconst rootStyles = computed(() => ({\n  padding: `${parseValue(props.paddingY)}px ${parseValue(props.paddingX)}px`,\n  width: `${parseValue(props.width) + parseValue(props.paddingX) * 2}px`,\n}));\n\nconst captchaStyles = computed(() => {\n  return {\n    height: `${parseValue(props.height)}px`,\n    width: `${parseValue(props.width)}px`,\n  };\n});\n\nfunction handleClick(e: MouseEvent) {\n  emit('click', e);\n}\n</script>\n<template>\n  <Card :style=\"rootStyles\" aria-labelledby=\"captcha-title\" role=\"region\">\n    <CardHeader class=\"p-0\">\n      <CardTitle id=\"captcha-title\" class=\"flex items-center justify-between\">\n        <template v-if=\"$slots.title\">\n          <slot name=\"title\">{{ $t('ui.captcha.title') }}</slot>\n        </template>\n        <template v-else>\n          <span>{{ title }}</span>\n        </template>\n        <div class=\"flex items-center justify-end\">\n          <slot name=\"extra\"></slot>\n        </div>\n      </CardTitle>\n    </CardHeader>\n    <CardContent class=\"relative mt-2 flex w-full overflow-hidden rounded p-0\">\n      <img\n        v-show=\"captchaImage\"\n        :alt=\"$t('ui.captcha.alt')\"\n        :src=\"captchaImage\"\n        :style=\"captchaStyles\"\n        class=\"relative z-10\"\n        @click=\"handleClick\"\n      />\n      <div class=\"absolute inset-0\">\n        <slot></slot>\n      </div>\n    </CardContent>\n    <CardFooter class=\"mt-2 flex justify-between p-0\">\n      <slot name=\"footer\"></slot>\n    </CardFooter>\n  </Card>\n</template>\n"
  },
  {
    "path": "packages/effects/common-ui/src/components/captcha/slider-captcha/index.vue",
    "content": "<script setup lang=\"ts\">\nimport type {\n  CaptchaVerifyPassingData,\n  SliderCaptchaProps,\n  SliderRotateVerifyPassingData,\n} from '../types';\n\nimport { $t } from '@vben/locales';\nimport { cn } from '@vben-core/shared/utils';\nimport { useTimeoutFn } from '@vueuse/core';\nimport { reactive, unref, useTemplateRef, watch, watchEffect } from 'vue';\n\nimport SliderCaptchaAction from './slider-captcha-action.vue';\nimport SliderCaptchaBar from './slider-captcha-bar.vue';\nimport SliderCaptchaContent from './slider-captcha-content.vue';\n\nconst props = withDefaults(defineProps<SliderCaptchaProps>(), {\n  actionStyle: () => ({}),\n  barStyle: () => ({}),\n  contentStyle: () => ({}),\n  isSlot: false,\n  successText: '',\n  text: '',\n  wrapperStyle: () => ({}),\n});\n\nconst emit = defineEmits<{\n  end: [MouseEvent | TouchEvent];\n  move: [SliderRotateVerifyPassingData];\n  start: [MouseEvent | TouchEvent];\n  success: [CaptchaVerifyPassingData];\n}>();\n\nconst modelValue = defineModel<boolean>({ default: false });\n\nconst state = reactive({\n  endTime: 0,\n  isMoving: false,\n  isPassing: false,\n  moveDistance: 0,\n  startTime: 0,\n  toLeft: false,\n});\n\ndefineExpose({\n  resume,\n});\n\nconst wrapperRef = useTemplateRef<HTMLDivElement>('wrapperRef');\nconst barRef = useTemplateRef<typeof SliderCaptchaBar>('barRef');\nconst contentRef = useTemplateRef<typeof SliderCaptchaContent>('contentRef');\nconst actionRef = useTemplateRef<typeof SliderCaptchaAction>('actionRef');\n\nwatch(\n  () => state.isPassing,\n  (isPassing) => {\n    if (isPassing) {\n      const { endTime, startTime } = state;\n      const time = (endTime - startTime) / 1000;\n      emit('success', { isPassing, time: time.toFixed(1) });\n      modelValue.value = isPassing;\n    }\n  },\n);\n\nwatchEffect(() => {\n  state.isPassing = !!modelValue.value;\n});\n\nfunction getEventPageX(e: MouseEvent | TouchEvent): number {\n  if ('pageX' in e) {\n    return e.pageX;\n  } else if ('touches' in e && e.touches[0]) {\n    return e.touches[0].pageX;\n  }\n  return 0;\n}\n\nfunction handleDragStart(e: MouseEvent | TouchEvent) {\n  if (state.isPassing) {\n    return;\n  }\n  if (!actionRef.value) return;\n  emit('start', e);\n\n  state.moveDistance =\n    getEventPageX(e) -\n    Number.parseInt(\n      actionRef.value.getStyle().left.replace('px', '') || '0',\n      10,\n    );\n  state.startTime = Date.now();\n  state.isMoving = true;\n}\n\nfunction getOffset(actionEl: HTMLDivElement) {\n  const wrapperWidth = wrapperRef.value?.offsetWidth ?? 220;\n  const actionWidth = actionEl?.offsetWidth ?? 40;\n  const offset = wrapperWidth - actionWidth - 6;\n  return { actionWidth, offset, wrapperWidth };\n}\n\nfunction handleDragMoving(e: MouseEvent | TouchEvent) {\n  const { isMoving, moveDistance } = state;\n  if (isMoving) {\n    const actionEl = unref(actionRef);\n    const barEl = unref(barRef);\n    if (!actionEl || !barEl) return;\n    const { actionWidth, offset, wrapperWidth } = getOffset(actionEl.getEl());\n    const moveX = getEventPageX(e) - moveDistance;\n\n    emit('move', {\n      event: e,\n      moveDistance,\n      moveX,\n    });\n    if (moveX > 0 && moveX <= offset) {\n      actionEl.setLeft(`${moveX}px`);\n      barEl.setWidth(`${moveX + actionWidth / 2}px`);\n    } else if (moveX > offset) {\n      actionEl.setLeft(`${wrapperWidth - actionWidth}px`);\n      barEl.setWidth(`${wrapperWidth - actionWidth / 2}px`);\n      if (!props.isSlot) {\n        checkPass();\n      }\n    }\n  }\n}\n\nfunction handleDragOver(e: MouseEvent | TouchEvent) {\n  const { isMoving, isPassing, moveDistance } = state;\n  if (isMoving && !isPassing) {\n    emit('end', e);\n    const actionEl = actionRef.value;\n    const barEl = unref(barRef);\n    if (!actionEl || !barEl) return;\n    const moveX = getEventPageX(e) - moveDistance;\n    const { actionWidth, offset, wrapperWidth } = getOffset(actionEl.getEl());\n    if (moveX < offset) {\n      if (props.isSlot) {\n        setTimeout(() => {\n          if (modelValue.value) {\n            const contentEl = unref(contentRef);\n            if (contentEl) {\n              contentEl.getEl().style.width = `${Number.parseInt(barEl.getEl().style.width)}px`;\n            }\n          } else {\n            resume();\n          }\n        }, 0);\n      } else {\n        resume();\n      }\n    } else {\n      actionEl.setLeft(`${wrapperWidth - actionWidth}px`);\n      barEl.setWidth(`${wrapperWidth - actionWidth / 2}px`);\n      checkPass();\n    }\n    state.isMoving = false;\n  }\n}\n\nfunction checkPass() {\n  if (props.isSlot) {\n    resume();\n    return;\n  }\n  state.endTime = Date.now();\n  state.isPassing = true;\n  state.isMoving = false;\n}\n\nfunction resume() {\n  state.isMoving = false;\n  state.isPassing = false;\n  state.moveDistance = 0;\n  state.toLeft = false;\n  state.startTime = 0;\n  state.endTime = 0;\n  const actionEl = unref(actionRef);\n  const barEl = unref(barRef);\n  const contentEl = unref(contentRef);\n  if (!actionEl || !barEl || !contentEl) return;\n\n  contentEl.getEl().style.width = '100%';\n  state.toLeft = true;\n  useTimeoutFn(() => {\n    state.toLeft = false;\n    actionEl.setLeft('0');\n    barEl.setWidth('0');\n  }, 300);\n}\n</script>\n\n<template>\n  <div\n    ref=\"wrapperRef\"\n    :class=\"\n      cn(\n        'border-border bg-background-deep relative flex h-10 w-full items-center overflow-hidden rounded-md border text-center',\n        props.class,\n      )\n    \"\n    :style=\"wrapperStyle\"\n    @mouseleave=\"handleDragOver\"\n    @mousemove=\"handleDragMoving\"\n    @mouseup=\"handleDragOver\"\n    @touchend=\"handleDragOver\"\n    @touchmove=\"handleDragMoving\"\n  >\n    <SliderCaptchaBar\n      ref=\"barRef\"\n      :bar-style=\"barStyle\"\n      :to-left=\"state.toLeft\"\n    />\n    <SliderCaptchaContent\n      ref=\"contentRef\"\n      :content-style=\"contentStyle\"\n      :is-passing=\"state.isPassing\"\n      :success-text=\"successText || $t('ui.captcha.sliderSuccessText')\"\n      :text=\"text || $t('ui.captcha.sliderDefaultText')\"\n    >\n      <template v-if=\"$slots.text\" #text>\n        <slot :is-passing=\"state.isPassing\" name=\"text\"></slot>\n      </template>\n    </SliderCaptchaContent>\n\n    <SliderCaptchaAction\n      ref=\"actionRef\"\n      :action-style=\"actionStyle\"\n      :is-passing=\"state.isPassing\"\n      :to-left=\"state.toLeft\"\n      @mousedown=\"handleDragStart\"\n      @touchstart=\"handleDragStart\"\n    >\n      <template v-if=\"$slots.actionIcon\" #icon>\n        <slot :is-passing=\"state.isPassing\" name=\"actionIcon\"></slot>\n      </template>\n    </SliderCaptchaAction>\n  </div>\n</template>\n"
  },
  {
    "path": "packages/effects/common-ui/src/components/captcha/slider-captcha/slider-captcha-action.vue",
    "content": "<script setup lang=\"ts\">\nimport type { CSSProperties } from 'vue';\n\nimport { Check, ChevronsRight } from '@vben/icons';\nimport { Slot } from '@vben-core/shadcn-ui';\nimport { computed, ref, useTemplateRef } from 'vue';\n\nconst props = defineProps<{\n  actionStyle: CSSProperties;\n  isPassing: boolean;\n  toLeft: boolean;\n}>();\n\nconst actionRef = useTemplateRef<HTMLDivElement>('actionRef');\n\nconst left = ref('0');\n\nconst style = computed(() => {\n  const { actionStyle } = props;\n  return {\n    ...actionStyle,\n    left: left.value,\n  };\n});\n\nconst isDragging = computed(() => {\n  const currentLeft = Number.parseInt(left.value as string);\n\n  return currentLeft > 10 && !props.isPassing;\n});\n\ndefineExpose({\n  getEl: () => {\n    return actionRef.value;\n  },\n  getStyle: () => {\n    return actionRef?.value?.style;\n  },\n  setLeft: (val: string) => {\n    left.value = val;\n  },\n});\n</script>\n\n<template>\n  <div\n    ref=\"actionRef\"\n    :class=\"{\n      'transition-width !left-0 duration-300': toLeft,\n      'rounded-md': isDragging,\n    }\"\n    :style=\"style\"\n    class=\"bg-background dark:bg-accent absolute left-0 top-0 flex h-full cursor-move items-center justify-center px-3.5 shadow-md\"\n    name=\"captcha-action\"\n  >\n    <Slot :is-passing=\"isPassing\" class=\"text-foreground/60 size-4\">\n      <slot name=\"icon\">\n        <ChevronsRight v-if=\"!isPassing\" />\n        <Check v-else />\n      </slot>\n    </Slot>\n  </div>\n</template>\n"
  },
  {
    "path": "packages/effects/common-ui/src/components/captcha/slider-captcha/slider-captcha-bar.vue",
    "content": "<script setup lang=\"ts\">\nimport type { CSSProperties } from 'vue';\n\nimport { computed, ref, useTemplateRef } from 'vue';\n\nconst props = defineProps<{\n  barStyle: CSSProperties;\n  toLeft: boolean;\n}>();\n\nconst barRef = useTemplateRef<HTMLDivElement>('barRef');\n\nconst width = ref('0');\n\nconst style = computed(() => {\n  const { barStyle } = props;\n  return {\n    ...barStyle,\n    width: width.value,\n  };\n});\n\ndefineExpose({\n  getEl: () => {\n    return barRef.value;\n  },\n  setWidth: (val: string) => {\n    width.value = val;\n  },\n});\n</script>\n\n<template>\n  <div\n    ref=\"barRef\"\n    :class=\"toLeft && 'transition-width !w-0 duration-300'\"\n    :style=\"style\"\n    class=\"bg-success absolute h-full\"\n  ></div>\n</template>\n"
  },
  {
    "path": "packages/effects/common-ui/src/components/captcha/slider-captcha/slider-captcha-content.vue",
    "content": "<script setup lang=\"ts\">\nimport type { CSSProperties } from 'vue';\n\nimport { VbenSpineText } from '@vben-core/shadcn-ui';\nimport { computed, useTemplateRef } from 'vue';\n\nconst props = defineProps<{\n  contentStyle: CSSProperties;\n  isPassing: boolean;\n  successText: string;\n  text: string;\n}>();\n\nconst contentRef = useTemplateRef<HTMLDivElement>('contentRef');\n\nconst style = computed(() => {\n  const { contentStyle } = props;\n\n  return {\n    ...contentStyle,\n  };\n});\n\ndefineExpose({\n  getEl: () => {\n    return contentRef.value;\n  },\n});\n</script>\n\n<template>\n  <div\n    ref=\"contentRef\"\n    :class=\"{\n      [$style.success]: isPassing,\n    }\"\n    :style=\"style\"\n    class=\"absolute top-0 flex size-full select-none items-center justify-center text-xs\"\n  >\n    <slot name=\"text\">\n      <VbenSpineText class=\"flex h-full items-center\">\n        {{ isPassing ? successText : text }}\n      </VbenSpineText>\n    </slot>\n  </div>\n</template>\n\n<style module>\n.success {\n  -webkit-text-fill-color: hsl(0deg 0% 98%);\n}\n</style>\n"
  },
  {
    "path": "packages/effects/common-ui/src/components/captcha/slider-rotate-captcha/index.vue",
    "content": "<script setup lang=\"ts\">\nimport type {\n  CaptchaVerifyPassingData,\n  SliderCaptchaActionType,\n  SliderRotateCaptchaProps,\n  SliderRotateVerifyPassingData,\n} from '../types';\n\nimport { computed, reactive, unref, useTemplateRef, watch } from 'vue';\n\nimport { $t } from '@vben/locales';\n\nimport { useTimeoutFn } from '@vueuse/core';\n\nimport SliderCaptcha from '../slider-captcha/index.vue';\n\nconst props = withDefaults(defineProps<SliderRotateCaptchaProps>(), {\n  defaultTip: '',\n  diffDegree: 20,\n  imageSize: 260,\n  maxDegree: 300,\n  minDegree: 120,\n  src: '',\n});\n\nconst emit = defineEmits<{\n  success: [CaptchaVerifyPassingData];\n}>();\n\nconst slideBarRef = useTemplateRef<SliderCaptchaActionType>('slideBarRef');\n\nconst state = reactive({\n  currentRotate: 0,\n  dragging: false,\n  endTime: 0,\n  imgStyle: {},\n  isPassing: false,\n  randomRotate: 0,\n  showTip: false,\n  startTime: 0,\n  toOrigin: false,\n});\n\nconst modalValue = defineModel<boolean>({ default: false });\n\nwatch(\n  () => state.isPassing,\n  (isPassing) => {\n    if (isPassing) {\n      const { endTime, startTime } = state;\n      const time = (endTime - startTime) / 1000;\n      emit('success', { isPassing, time: time.toFixed(1) });\n    }\n    modalValue.value = isPassing;\n  },\n);\n\nconst getImgWrapStyleRef = computed(() => {\n  const { imageSize, imageWrapperStyle } = props;\n  return {\n    height: `${imageSize}px`,\n    width: `${imageSize}px`,\n    ...imageWrapperStyle,\n  };\n});\n\nconst getFactorRef = computed(() => {\n  const { maxDegree, minDegree } = props;\n  if (minDegree > maxDegree) {\n    console.warn('minDegree should not be greater than maxDegree');\n  }\n\n  if (minDegree === maxDegree) {\n    return Math.floor(1 + Math.random() * 1) / 10 + 1;\n  }\n  return 1;\n});\n\nfunction handleStart() {\n  state.startTime = Date.now();\n}\n\nfunction handleDragBarMove(data: SliderRotateVerifyPassingData) {\n  state.dragging = true;\n  const { imageSize, maxDegree } = props;\n  const { moveX } = data;\n  const denominator = imageSize!;\n  if (denominator === 0) {\n    return;\n  }\n  const currentRotate = Math.ceil(\n    (moveX / denominator) * 1.5 * maxDegree! * unref(getFactorRef),\n  );\n  state.currentRotate = currentRotate;\n  setImgRotate(state.randomRotate - currentRotate);\n}\n\nfunction handleImgOnLoad() {\n  const { maxDegree, minDegree } = props;\n  const ranRotate = Math.floor(\n    minDegree! + Math.random() * (maxDegree! - minDegree!),\n  ); // 生成随机角度\n  state.randomRotate = ranRotate;\n  setImgRotate(ranRotate);\n}\n\nfunction handleDragEnd() {\n  const { currentRotate, randomRotate } = state;\n  const { diffDegree } = props;\n\n  if (Math.abs(randomRotate - currentRotate) >= (diffDegree || 20)) {\n    setImgRotate(randomRotate);\n    state.toOrigin = true;\n    useTimeoutFn(() => {\n      state.toOrigin = false;\n      state.showTip = true;\n      //  时间与动画时间保持一致\n    }, 300);\n  } else {\n    checkPass();\n  }\n  state.showTip = true;\n  state.dragging = false;\n}\n\nfunction setImgRotate(deg: number) {\n  state.imgStyle = {\n    transform: `rotateZ(${deg}deg)`,\n  };\n}\n\nfunction checkPass() {\n  state.isPassing = true;\n  state.endTime = Date.now();\n}\n\nfunction resume() {\n  state.showTip = false;\n  const basicEl = unref(slideBarRef);\n  if (!basicEl) {\n    return;\n  }\n  state.isPassing = false;\n\n  basicEl.resume();\n  handleImgOnLoad();\n}\n\nconst imgCls = computed(() => {\n  return state.toOrigin ? ['transition-transform duration-300'] : [];\n});\n\nconst verifyTip = computed(() => {\n  return state.isPassing\n    ? $t('ui.captcha.sliderRotateSuccessTip', [\n        ((state.endTime - state.startTime) / 1000).toFixed(1),\n      ])\n    : $t('ui.captcha.sliderRotateFailTip');\n});\n\ndefineExpose({\n  resume,\n});\n</script>\n\n<template>\n  <div class=\"relative flex flex-col items-center\">\n    <div\n      :style=\"getImgWrapStyleRef\"\n      class=\"border-border relative cursor-pointer overflow-hidden rounded-full border shadow-md\"\n    >\n      <img\n        :class=\"imgCls\"\n        :src=\"src\"\n        :style=\"state.imgStyle\"\n        alt=\"verify\"\n        class=\"w-full rounded-full\"\n        @click=\"resume\"\n        @load=\"handleImgOnLoad\"\n      />\n      <div\n        class=\"absolute bottom-3 left-0 z-10 block h-7 w-full text-center text-xs leading-[30px] text-white\"\n      >\n        <div\n          v-if=\"state.showTip\"\n          :class=\"{\n            'bg-success/80': state.isPassing,\n            'bg-destructive/80': !state.isPassing,\n          }\"\n        >\n          {{ verifyTip }}\n        </div>\n        <div v-if=\"!state.dragging\" class=\"bg-black/30\">\n          {{ defaultTip || $t('ui.captcha.sliderRotateDefaultTip') }}\n        </div>\n      </div>\n    </div>\n\n    <SliderCaptcha\n      ref=\"slideBarRef\"\n      v-model=\"modalValue\"\n      class=\"mt-5\"\n      is-slot\n      @end=\"handleDragEnd\"\n      @move=\"handleDragBarMove\"\n      @start=\"handleStart\"\n    >\n      <template v-for=\"(_, key) in $slots\" :key=\"key\" #[key]=\"slotProps\">\n        <slot :name=\"key\" v-bind=\"slotProps\"></slot>\n      </template>\n    </SliderCaptcha>\n  </div>\n</template>\n"
  },
  {
    "path": "packages/effects/common-ui/src/components/captcha/slider-translate-captcha/index.vue",
    "content": "<script setup lang=\"ts\">\nimport type {\n  CaptchaVerifyPassingData,\n  SliderCaptchaActionType,\n  SliderRotateVerifyPassingData,\n  SliderTranslateCaptchaProps,\n} from '../types';\n\nimport {\n  computed,\n  onMounted,\n  reactive,\n  ref,\n  unref,\n  useTemplateRef,\n  watch,\n} from 'vue';\n\nimport { $t } from '@vben/locales';\n\nimport SliderCaptcha from '../slider-captcha/index.vue';\n\nconst props = withDefaults(defineProps<SliderTranslateCaptchaProps>(), {\n  defaultTip: '',\n  canvasWidth: 420,\n  canvasHeight: 280,\n  squareLength: 42,\n  circleRadius: 10,\n  src: '',\n  diffDistance: 3,\n});\n\nconst emit = defineEmits<{\n  success: [CaptchaVerifyPassingData];\n}>();\n\nconst PI: number = Math.PI;\nenum CanvasOpr {\n  // eslint-disable-next-line no-unused-vars\n  Clip = 'clip',\n  // eslint-disable-next-line no-unused-vars\n  Fill = 'fill',\n}\n\nconst modalValue = defineModel<boolean>({ default: false });\n\nconst slideBarRef = useTemplateRef<SliderCaptchaActionType>('slideBarRef');\nconst puzzleCanvasRef = useTemplateRef<HTMLCanvasElement>('puzzleCanvasRef');\nconst pieceCanvasRef = useTemplateRef<HTMLCanvasElement>('pieceCanvasRef');\n\nconst state = reactive({\n  dragging: false,\n  startTime: 0,\n  endTime: 0,\n  pieceX: 0,\n  pieceY: 0,\n  moveDistance: 0,\n  isPassing: false,\n  showTip: false,\n});\n\nconst left = ref('0');\n\nconst pieceStyle = computed(() => {\n  return {\n    left: left.value,\n  };\n});\n\nfunction setLeft(val: string) {\n  left.value = val;\n}\n\nconst verifyTip = computed(() => {\n  return state.isPassing\n    ? $t('ui.captcha.sliderTranslateSuccessTip', [\n        ((state.endTime - state.startTime) / 1000).toFixed(1),\n      ])\n    : $t('ui.captcha.sliderTranslateFailTip');\n});\nfunction handleStart() {\n  state.startTime = Date.now();\n}\n\nfunction handleDragBarMove(data: SliderRotateVerifyPassingData) {\n  state.dragging = true;\n  const { moveX } = data;\n  state.moveDistance = moveX;\n  setLeft(`${moveX}px`);\n}\n\nfunction handleDragEnd() {\n  const { pieceX } = state;\n  const { diffDistance } = props;\n\n  if (Math.abs(pieceX - state.moveDistance) >= (diffDistance || 3)) {\n    setLeft('0');\n    state.moveDistance = 0;\n  } else {\n    checkPass();\n  }\n  state.showTip = true;\n  state.dragging = false;\n}\n\nfunction checkPass() {\n  state.isPassing = true;\n  state.endTime = Date.now();\n}\n\nwatch(\n  () => state.isPassing,\n  (isPassing) => {\n    if (isPassing) {\n      const { endTime, startTime } = state;\n      const time = (endTime - startTime) / 1000;\n      emit('success', { isPassing, time: time.toFixed(1) });\n    }\n    modalValue.value = isPassing;\n  },\n);\n\nfunction resetCanvas() {\n  const { canvasWidth, canvasHeight } = props;\n  const puzzleCanvas = unref(puzzleCanvasRef);\n  const pieceCanvas = unref(pieceCanvasRef);\n  if (!puzzleCanvas || !pieceCanvas) return;\n  pieceCanvas.width = canvasWidth;\n  const puzzleCanvasCtx = puzzleCanvas.getContext('2d');\n  // Canvas2D: Multiple readback operations using getImageData\n  // are faster with the willReadFrequently attribute set to true.\n  // See: https://html.spec.whatwg.org/multipage/canvas.html#concept-canvas-will-read-frequently (anonymous)\n  const pieceCanvasCtx = pieceCanvas.getContext('2d', {\n    willReadFrequently: true,\n  });\n  if (!puzzleCanvasCtx || !pieceCanvasCtx) return;\n  puzzleCanvasCtx.clearRect(0, 0, canvasWidth, canvasHeight);\n  pieceCanvasCtx.clearRect(0, 0, canvasWidth, canvasHeight);\n}\n\nfunction initCanvas() {\n  const { canvasWidth, canvasHeight, squareLength, circleRadius, src } = props;\n  const puzzleCanvas = unref(puzzleCanvasRef);\n  const pieceCanvas = unref(pieceCanvasRef);\n  if (!puzzleCanvas || !pieceCanvas) return;\n  const puzzleCanvasCtx = puzzleCanvas.getContext('2d');\n  // Canvas2D: Multiple readback operations using getImageData\n  // are faster with the willReadFrequently attribute set to true.\n  // See: https://html.spec.whatwg.org/multipage/canvas.html#concept-canvas-will-read-frequently (anonymous)\n  const pieceCanvasCtx = pieceCanvas.getContext('2d', {\n    willReadFrequently: true,\n  });\n  if (!puzzleCanvasCtx || !pieceCanvasCtx) return;\n  const img = new Image();\n  // 解决跨域\n  img.crossOrigin = 'Anonymous';\n  img.src = src;\n  img.addEventListener('load', () => {\n    draw(puzzleCanvasCtx, pieceCanvasCtx);\n    puzzleCanvasCtx.drawImage(img, 0, 0, canvasWidth, canvasHeight);\n    pieceCanvasCtx.drawImage(img, 0, 0, canvasWidth, canvasHeight);\n    const pieceLength = squareLength + 2 * circleRadius + 3;\n    const sx = state.pieceX;\n    const sy = state.pieceY - 2 * circleRadius - 1;\n    const imageData = pieceCanvasCtx.getImageData(\n      sx,\n      sy,\n      pieceLength,\n      pieceLength,\n    );\n    pieceCanvas.width = pieceLength;\n    pieceCanvasCtx.putImageData(imageData, 0, sy);\n    setLeft('0');\n  });\n}\n\nfunction getRandomNumberByRange(start: number, end: number) {\n  return Math.round(Math.random() * (end - start) + start);\n}\n\n// 绘制拼图\nfunction draw(ctx1: CanvasRenderingContext2D, ctx2: CanvasRenderingContext2D) {\n  const { canvasWidth, canvasHeight, squareLength, circleRadius } = props;\n  state.pieceX = getRandomNumberByRange(\n    squareLength + 2 * circleRadius,\n    canvasWidth - (squareLength + 2 * circleRadius),\n  );\n  state.pieceY = getRandomNumberByRange(\n    3 * circleRadius,\n    canvasHeight - (squareLength + 2 * circleRadius),\n  );\n  drawPiece(ctx1, state.pieceX, state.pieceY, CanvasOpr.Fill);\n  drawPiece(ctx2, state.pieceX, state.pieceY, CanvasOpr.Clip);\n}\n\n// 绘制拼图切块\nfunction drawPiece(\n  ctx: CanvasRenderingContext2D,\n  x: number,\n  y: number,\n  opr: CanvasOpr,\n) {\n  const { squareLength, circleRadius } = props;\n  ctx.beginPath();\n  ctx.moveTo(x, y);\n  ctx.arc(\n    x + squareLength / 2,\n    y - circleRadius + 2,\n    circleRadius,\n    0.72 * PI,\n    2.26 * PI,\n  );\n  ctx.lineTo(x + squareLength, y);\n  ctx.arc(\n    x + squareLength + circleRadius - 2,\n    y + squareLength / 2,\n    circleRadius,\n    1.21 * PI,\n    2.78 * PI,\n  );\n  ctx.lineTo(x + squareLength, y + squareLength);\n  ctx.lineTo(x, y + squareLength);\n  ctx.arc(\n    x + circleRadius - 2,\n    y + squareLength / 2,\n    circleRadius + 0.4,\n    2.76 * PI,\n    1.24 * PI,\n    true,\n  );\n  ctx.lineTo(x, y);\n  ctx.lineWidth = 2;\n  ctx.fillStyle = 'rgba(255, 255, 255, 0.7)';\n  ctx.strokeStyle = 'rgba(255, 255, 255, 0.7)';\n  ctx.stroke();\n  opr === CanvasOpr.Clip ? ctx.clip() : ctx.fill();\n  ctx.globalCompositeOperation = 'destination-over';\n}\n\nfunction resume() {\n  state.showTip = false;\n  const basicEl = unref(slideBarRef);\n  if (!basicEl) {\n    return;\n  }\n  state.dragging = false;\n  state.isPassing = false;\n  state.pieceX = 0;\n  state.pieceY = 0;\n\n  basicEl.resume();\n  resetCanvas();\n  initCanvas();\n}\n\nonMounted(() => {\n  initCanvas();\n});\n</script>\n\n<template>\n  <div class=\"relative flex flex-col items-center\">\n    <div\n      class=\"border-border relative flex cursor-pointer overflow-hidden border shadow-md\"\n    >\n      <canvas\n        ref=\"puzzleCanvasRef\"\n        :width=\"canvasWidth\"\n        :height=\"canvasHeight\"\n        @click=\"resume\"\n      ></canvas>\n      <canvas\n        ref=\"pieceCanvasRef\"\n        :width=\"canvasWidth\"\n        :height=\"canvasHeight\"\n        :style=\"pieceStyle\"\n        class=\"absolute\"\n        @click=\"resume\"\n      ></canvas>\n      <div\n        class=\"h-15 absolute bottom-3 left-0 z-10 block w-full text-center text-xs leading-[30px] text-white\"\n      >\n        <div\n          v-if=\"state.showTip\"\n          :class=\"{\n            'bg-success/80': state.isPassing,\n            'bg-destructive/80': !state.isPassing,\n          }\"\n        >\n          {{ verifyTip }}\n        </div>\n        <div v-if=\"!state.dragging\" class=\"bg-black/30\">\n          {{ defaultTip || $t('ui.captcha.sliderTranslateDefaultTip') }}\n        </div>\n      </div>\n    </div>\n    <SliderCaptcha\n      ref=\"slideBarRef\"\n      v-model=\"modalValue\"\n      class=\"mt-5\"\n      is-slot\n      @end=\"handleDragEnd\"\n      @move=\"handleDragBarMove\"\n      @start=\"handleStart\"\n    >\n      <template v-for=\"(_, key) in $slots\" :key=\"key\" #[key]=\"slotProps\">\n        <slot :name=\"key\" v-bind=\"slotProps\"></slot>\n      </template>\n    </SliderCaptcha>\n  </div>\n</template>\n"
  },
  {
    "path": "packages/effects/common-ui/src/components/captcha/types.ts",
    "content": "import type { ClassType } from '@vben/types';\nimport type { CSSProperties } from 'vue';\n\nexport interface CaptchaData {\n  /**\n   * x\n   */\n  x: number;\n  /**\n   * y\n   */\n  y: number;\n  /**\n   * 时间戳\n   */\n  t: number;\n}\nexport interface CaptchaPoint extends CaptchaData {\n  /**\n   * 数据索引\n   */\n  i: number;\n}\nexport interface PointSelectionCaptchaCardProps {\n  /**\n   * 验证码图片\n   */\n  captchaImage: string;\n  /**\n   * 验证码图片高度\n   * @default '220px'\n   */\n  height?: number | string;\n  /**\n   * 水平内边距\n   * @default '12px'\n   */\n  paddingX?: number | string;\n  /**\n   * 垂直内边距\n   * @default '16px'\n   */\n  paddingY?: number | string;\n  /**\n   * 标题\n   * @default '请按图依次点击'\n   */\n  title?: string;\n  /**\n   * 验证码图片宽度\n   * @default '300px'\n   */\n  width?: number | string;\n}\n\nexport interface PointSelectionCaptchaProps\n  extends PointSelectionCaptchaCardProps {\n  /**\n   * 是否展示确定按钮\n   * @default false\n   */\n  showConfirm?: boolean;\n  /**\n   * 提示图片\n   * @default ''\n   */\n  hintImage?: string;\n  /**\n   * 提示文本\n   * @default ''\n   */\n  hintText?: string;\n}\n\nexport interface SliderCaptchaProps {\n  class?: ClassType;\n  /**\n   * @description 滑块的样式\n   * @default {}\n   */\n  actionStyle?: CSSProperties;\n\n  /**\n   * @description 滑块条的样式\n   * @default {}\n   */\n  barStyle?: CSSProperties;\n\n  /**\n   * @description 内容的样式\n   * @default {}\n   */\n  contentStyle?: CSSProperties;\n\n  /**\n   * @description 组件的样式\n   * @default {}\n   */\n  wrapperStyle?: CSSProperties;\n\n  /**\n   * @description 是否作为插槽使用，用于联动组件，可参考旋转校验组件\n   * @default false\n   */\n  isSlot?: boolean;\n\n  /**\n   * @description 验证成功的提示\n   * @default '验证通过'\n   */\n  successText?: string;\n\n  /**\n   * @description 提示文字\n   * @default '请按住滑块拖动'\n   */\n  text?: string;\n}\n\nexport interface SliderRotateCaptchaProps {\n  /**\n   * @description 旋转的角度\n   * @default 20\n   */\n  diffDegree?: number;\n\n  /**\n   * @description 图片的宽度\n   * @default 260\n   */\n  imageSize?: number;\n\n  /**\n   * @description 图片的样式\n   * @default {}\n   */\n  imageWrapperStyle?: CSSProperties;\n\n  /**\n   * @description 最大旋转角度\n   * @default 270\n   */\n  maxDegree?: number;\n\n  /**\n   * @description 最小旋转角度\n   * @default 90\n   */\n  minDegree?: number;\n\n  /**\n   * @description 图片的地址\n   */\n  src?: string;\n  /**\n   * @description 默认提示文本\n   */\n  defaultTip?: string;\n}\n\nexport interface SliderTranslateCaptchaProps {\n  /**\n   * @description 拼图的宽度\n   * @default 420\n   */\n  canvasWidth?: number;\n  /**\n   * @description 拼图的高度\n   * @default 280\n   */\n  canvasHeight?: number;\n  /**\n   * @description 切块上正方形的长度\n   * @default 42\n   */\n  squareLength?: number;\n  /**\n   * @description 切块上圆形的半径\n   * @default 10\n   */\n  circleRadius?: number;\n  /**\n   * @description 图片的地址\n   */\n  src?: string;\n  /**\n   * @description 允许的最大差距\n   * @default 3\n   */\n  diffDistance?: number;\n  /**\n   * @description 默认提示文本\n   */\n  defaultTip?: string;\n}\n\nexport interface CaptchaVerifyPassingData {\n  isPassing: boolean;\n  time: number | string;\n}\n\nexport interface SliderCaptchaActionType {\n  resume: () => void;\n}\n\nexport interface SliderRotateVerifyPassingData {\n  event: MouseEvent | TouchEvent;\n  moveDistance: number;\n  moveX: number;\n}\n"
  },
  {
    "path": "packages/effects/common-ui/src/components/code-mirror/code-mirror.vue",
    "content": "<script setup lang=\"ts\">\nimport { computed, nextTick, ref, useTemplateRef, watch } from 'vue';\nimport CodeMirror from 'vue-codemirror6';\n\nimport { usePreferences } from '@vben/preferences';\n\nimport { javascript } from '@codemirror/lang-javascript';\nimport { oneDark } from '@codemirror/theme-one-dark';\n\nimport { type LanguageSupport, languageSupportMap } from './data';\n\nconst props = withDefaults(\n  defineProps<{\n    /**\n     * 语言\n     */\n    language: LanguageSupport;\n    /**\n     * 只读\n     */\n    readonly?: boolean;\n  }>(),\n  {\n    language: 'js',\n    readonly: false,\n  },\n);\n\nconst codeMirrorRef =\n  useTemplateRef<InstanceType<typeof CodeMirror>>('codeMirrorRef');\n\nconst { isDark } = usePreferences();\n\nconst modelValue = defineModel({ default: '', type: String });\n\nconst lang = computed(() => languageSupportMap[props.language] ?? javascript());\n\n// 通过v-if 卸载挂载达到更新语言的目的\nconst langChanged = ref(true);\nwatch(\n  () => props.language,\n  () => {\n    langChanged.value = false;\n    nextTick(() => (langChanged.value = true));\n  },\n);\n/** 插件 */\nconst extensions = [oneDark];\n</script>\n\n<template>\n  <CodeMirror\n    v-if=\"langChanged\"\n    v-bind=\"$attrs\"\n    ref=\"codeMirrorRef\"\n    v-model=\"modelValue\"\n    :dark=\"isDark\"\n    :extensions=\"extensions\"\n    :lang=\"lang\"\n    :readonly=\"props.readonly\"\n    basic\n    wrap\n  >\n    <template v-for=\"slotName in Object.keys($slots)\" #[slotName]=\"scope\">\n      <slot :name=\"slotName\" v-bind=\"scope ?? {}\"></slot>\n    </template>\n  </CodeMirror>\n</template>\n"
  },
  {
    "path": "packages/effects/common-ui/src/components/code-mirror/data.ts",
    "content": "import { html } from '@codemirror/lang-html';\nimport { java } from '@codemirror/lang-java';\nimport { javascript } from '@codemirror/lang-javascript';\nimport { sql } from '@codemirror/lang-sql';\nimport { vue } from '@codemirror/lang-vue';\nimport { xml } from '@codemirror/lang-xml';\n\n/**\n * 可自行安装依赖并按格式配置 函数形参为配置项\n * @see https://github.com/logue/vue-codemirror6?tab=readme-ov-file#supported-languages Language Support项\n */\nexport const languageSupportMap = {\n  html: html(),\n  java: java(),\n  js: javascript(),\n  jsx: javascript({ jsx: true }),\n  sql: sql(),\n  ts: javascript({ typescript: true }),\n  tsx: javascript({ jsx: true, typescript: true }),\n  vue: vue(),\n  xml: xml(),\n};\n\nexport type LanguageSupport = keyof typeof languageSupportMap;\n"
  },
  {
    "path": "packages/effects/common-ui/src/components/code-mirror/index.ts",
    "content": "export { default as CodeMirror } from './code-mirror.vue';\nexport type { LanguageSupport } from './data';\n"
  },
  {
    "path": "packages/effects/common-ui/src/components/col-page/col-page.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { ColPageProps } from './types';\n\nimport { computed, ref, useSlots } from 'vue';\n\nimport {\n  ResizableHandle,\n  ResizablePanel,\n  ResizablePanelGroup,\n} from '@vben-core/shadcn-ui';\n\nimport Page from '../page/page.vue';\n\ndefineOptions({\n  name: 'ColPage',\n  inheritAttrs: false,\n});\n\nconst props = withDefaults(defineProps<ColPageProps>(), {\n  leftWidth: 30,\n  rightWidth: 70,\n  resizable: true,\n});\n\nconst delegatedProps = computed(() => {\n  const { leftWidth: _, ...delegated } = props;\n  return delegated;\n});\n\nconst slots = useSlots();\n\nconst delegatedSlots = computed(() => {\n  const resultSlots: string[] = [];\n\n  for (const key of Object.keys(slots)) {\n    if (!['default', 'left'].includes(key)) {\n      resultSlots.push(key);\n    }\n  }\n  return resultSlots;\n});\n\nconst leftPanelRef = ref<InstanceType<typeof ResizablePanel>>();\n\nfunction expandLeft() {\n  leftPanelRef.value?.expand();\n}\n\nfunction collapseLeft() {\n  leftPanelRef.value?.collapse();\n}\n\ndefineExpose({\n  expandLeft,\n  collapseLeft,\n});\n</script>\n<template>\n  <Page v-bind=\"delegatedProps\">\n    <!-- 继承默认的slot -->\n    <template\n      v-for=\"slotName in delegatedSlots\"\n      :key=\"slotName\"\n      #[slotName]=\"slotProps\"\n    >\n      <slot :name=\"slotName\" v-bind=\"slotProps\"></slot>\n    </template>\n\n    <ResizablePanelGroup class=\"w-full\" direction=\"horizontal\">\n      <ResizablePanel\n        ref=\"leftPanelRef\"\n        :collapsed-size=\"leftCollapsedWidth\"\n        :collapsible=\"leftCollapsible\"\n        :default-size=\"leftWidth\"\n        :max-size=\"leftMaxWidth\"\n        :min-size=\"leftMinWidth\"\n      >\n        <template #default=\"slotProps\">\n          <slot\n            name=\"left\"\n            v-bind=\"{\n              ...slotProps,\n              expand: expandLeft,\n              collapse: collapseLeft,\n            }\"\n          ></slot>\n        </template>\n      </ResizablePanel>\n      <ResizableHandle\n        v-if=\"resizable\"\n        :style=\"{ backgroundColor: splitLine ? undefined : 'transparent' }\"\n        :with-handle=\"splitHandle\"\n      />\n      <ResizablePanel\n        :collapsed-size=\"rightCollapsedWidth\"\n        :collapsible=\"rightCollapsible\"\n        :default-size=\"rightWidth\"\n        :max-size=\"rightMaxWidth\"\n        :min-size=\"rightMinWidth\"\n      >\n        <template #default>\n          <slot></slot>\n        </template>\n      </ResizablePanel>\n    </ResizablePanelGroup>\n  </Page>\n</template>\n"
  },
  {
    "path": "packages/effects/common-ui/src/components/col-page/index.ts",
    "content": "export { default as ColPage } from './col-page.vue';\nexport * from './types';\n"
  },
  {
    "path": "packages/effects/common-ui/src/components/col-page/types.ts",
    "content": "import type { PageProps } from '../page/types';\n\nexport interface ColPageProps extends PageProps {\n  /**\n   * 左侧宽度\n   * @default 30\n   */\n  leftWidth?: number;\n  leftMinWidth?: number;\n  leftMaxWidth?: number;\n  leftCollapsedWidth?: number;\n  leftCollapsible?: boolean;\n  /**\n   * 右侧宽度\n   * @default 70\n   */\n  rightWidth?: number;\n  rightMinWidth?: number;\n  rightCollapsedWidth?: number;\n  rightMaxWidth?: number;\n  rightCollapsible?: boolean;\n\n  resizable?: boolean;\n  splitLine?: boolean;\n  splitHandle?: boolean;\n}\n"
  },
  {
    "path": "packages/effects/common-ui/src/components/count-to/count-to.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { CountToProps } from './types';\n\nimport { computed, onMounted, ref, watch } from 'vue';\n\nimport { isString } from '@vben-core/shared/utils';\n\nimport { TransitionPresets, useTransition } from '@vueuse/core';\n\nconst props = withDefaults(defineProps<CountToProps>(), {\n  startVal: 0,\n  duration: 2000,\n  separator: ',',\n  decimal: '.',\n  decimals: 0,\n  delay: 0,\n  transition: () => TransitionPresets.easeOutExpo,\n});\n\nconst emit = defineEmits(['started', 'finished']);\n\nconst lastValue = ref(props.startVal);\n\nonMounted(() => {\n  lastValue.value = props.endVal;\n});\n\nwatch(\n  () => props.endVal,\n  (val) => {\n    lastValue.value = val;\n  },\n);\n\nconst currentValue = useTransition(lastValue, {\n  delay: computed(() => props.delay),\n  duration: computed(() => props.duration),\n  disabled: computed(() => props.disabled),\n  transition: computed(() => {\n    return isString(props.transition)\n      ? TransitionPresets[props.transition]\n      : props.transition;\n  }),\n  onStarted() {\n    emit('started');\n  },\n  onFinished() {\n    emit('finished');\n  },\n});\n\nconst numMain = computed(() => {\n  const result = currentValue.value\n    .toFixed(props.decimals)\n    .split('.')[0]\n    ?.replaceAll(/\\B(?=(\\d{3})+(?!\\d))/g, props.separator);\n  return result;\n});\n\nconst numDec = computed(() => {\n  return (\n    props.decimal + currentValue.value.toFixed(props.decimals).split('.')[1]\n  );\n});\n</script>\n<template>\n  <div class=\"count-to\" v-bind=\"$attrs\">\n    <slot name=\"prefix\">\n      <div\n        class=\"count-to-prefix\"\n        :style=\"prefixStyle\"\n        :class=\"prefixClass\"\n        v-if=\"prefix\"\n      >\n        {{ prefix }}\n      </div>\n    </slot>\n    <div class=\"count-to-main\" :class=\"mainClass\" :style=\"mainStyle\">\n      <span>{{ numMain }}</span>\n      <span\n        class=\"count-to-main-decimal\"\n        v-if=\"decimals > 0\"\n        :class=\"decimalClass\"\n        :style=\"decimalStyle\"\n      >\n        {{ numDec }}\n      </span>\n    </div>\n    <slot name=\"suffix\">\n      <div\n        class=\"count-to-suffix\"\n        :style=\"suffixStyle\"\n        :class=\"suffixClass\"\n        v-if=\"suffix\"\n      >\n        {{ suffix }}\n      </div>\n    </slot>\n  </div>\n</template>\n<style lang=\"scss\" scoped>\n.count-to {\n  display: flex;\n  align-items: baseline;\n\n  &-prefix {\n    // font-size: 1rem;\n  }\n\n  &-suffix {\n    // font-size: 1rem;\n  }\n\n  &-main {\n    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;\n    // font-size: 1.5rem;\n\n    &-decimal {\n      // font-size: 0.8rem;\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "packages/effects/common-ui/src/components/count-to/index.ts",
    "content": "export { default as CountTo } from './count-to.vue';\nexport * from './types';\n"
  },
  {
    "path": "packages/effects/common-ui/src/components/count-to/types.ts",
    "content": "import type { CubicBezierPoints, EasingFunction } from '@vueuse/core';\n\nimport type { StyleValue } from 'vue';\n\nimport { TransitionPresets as TransitionPresetsData } from '@vueuse/core';\n\nexport type TransitionPresets = keyof typeof TransitionPresetsData;\n\nexport const TransitionPresetsKeys = Object.keys(\n  TransitionPresetsData,\n) as TransitionPresets[];\n\nexport interface CountToProps {\n  /** 初始值 */\n  startVal?: number;\n  /** 当前值 */\n  endVal: number;\n  /** 是否禁用动画 */\n  disabled?: boolean;\n  /** 延迟动画开始的时间 */\n  delay?: number;\n  /** 持续时间  */\n  duration?: number;\n  /** 小数位数  */\n  decimals?: number;\n  /** 小数点  */\n  decimal?: string;\n  /** 分隔符  */\n  separator?: string;\n  /** 前缀  */\n  prefix?: string;\n  /** 后缀  */\n  suffix?: string;\n  /** 过渡效果  */\n  transition?: CubicBezierPoints | EasingFunction | TransitionPresets;\n  /** 整数部分的类名 */\n  mainClass?: string;\n  /** 小数部分的类名 */\n  decimalClass?: string;\n  /** 前缀部分的类名 */\n  prefixClass?: string;\n  /** 后缀部分的类名 */\n  suffixClass?: string;\n\n  /** 整数部分的样式 */\n  mainStyle?: StyleValue;\n  /** 小数部分的样式 */\n  decimalStyle?: StyleValue;\n  /** 前缀部分的样式 */\n  prefixStyle?: StyleValue;\n  /** 后缀部分的样式 */\n  suffixStyle?: StyleValue;\n}\n"
  },
  {
    "path": "packages/effects/common-ui/src/components/ellipsis-text/ellipsis-text.vue",
    "content": "<script setup lang=\"ts\">\nimport type { CSSProperties } from 'vue';\n\nimport {\n  computed,\n  onBeforeUnmount,\n  onMounted,\n  onUpdated,\n  ref,\n  watchEffect,\n} from 'vue';\n\nimport { VbenTooltip } from '@vben-core/shadcn-ui';\n\nimport { useElementSize } from '@vueuse/core';\n\ninterface Props {\n  /**\n   * 是否启用点击文本展开全部\n   * @default false\n   */\n  expand?: boolean;\n  /**\n   * 文本最大行数\n   * @default 1\n   */\n  line?: number;\n  /**\n   * 文本最大宽度\n   * @default '100%'\n   */\n  maxWidth?: number | string;\n  /**\n   * 提示框位置\n   * @default 'top'\n   */\n  placement?: 'bottom' | 'left' | 'right' | 'top';\n  /**\n   * 是否启用文本提示框\n   * @default true\n   */\n  tooltip?: boolean;\n  /**\n   * 是否只在文本被截断时显示提示框\n   * @default false\n   */\n  tooltipWhenEllipsis?: boolean;\n  /**\n   * 文本截断检测的像素差异阈值，越大则判断越严格\n   * @default 3\n   */\n  ellipsisThreshold?: number;\n  /**\n   * 提示框背景颜色，优先级高于 overlayStyle\n   */\n  tooltipBackgroundColor?: string;\n  /**\n   * 提示文本字体颜色，优先级高于 overlayStyle\n   */\n  tooltipColor?: string;\n  /**\n   * 提示文本字体大小，单位px，优先级高于 overlayStyle\n   */\n  tooltipFontSize?: number;\n  /**\n   * 提示框内容最大宽度，单位px，默认不设置时，提示文本内容自动与展示文本宽度保持一致\n   */\n  tooltipMaxWidth?: number;\n  /**\n   * 提示框内容区域样式\n   * @default { textAlign: 'justify' }\n   */\n  tooltipOverlayStyle?: CSSProperties;\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n  expand: false,\n  line: 1,\n  maxWidth: '100%',\n  placement: 'top',\n  tooltip: true,\n  tooltipWhenEllipsis: false,\n  ellipsisThreshold: 3,\n  tooltipBackgroundColor: '',\n  tooltipColor: '',\n  tooltipFontSize: 14,\n  tooltipMaxWidth: undefined,\n  tooltipOverlayStyle: () => ({ textAlign: 'justify' }),\n});\n\nconst emit = defineEmits<{ expandChange: [boolean] }>();\n\nconst textMaxWidth = computed(() => {\n  if (typeof props.maxWidth === 'number') {\n    return `${props.maxWidth}px`;\n  }\n  return props.maxWidth;\n});\nconst ellipsis = ref();\nconst isExpand = ref(false);\nconst defaultTooltipMaxWidth = ref();\nconst isEllipsis = ref(false);\n\nconst { width: eleWidth } = useElementSize(ellipsis);\n\n// 检测文本是否被截断\nconst checkEllipsis = () => {\n  if (!ellipsis.value || !props.tooltipWhenEllipsis) return;\n\n  const element = ellipsis.value;\n\n  const originalText = element.textContent || '';\n  const originalTrimmed = originalText.trim();\n\n  // 对于空文本直接返回 false\n  if (!originalTrimmed) {\n    isEllipsis.value = false;\n    return;\n  }\n\n  const widthDiff = element.scrollWidth - element.clientWidth;\n  const heightDiff = element.scrollHeight - element.clientHeight;\n\n  // 使用足够大的差异阈值确保只有真正被截断的文本才会显示 tooltip\n  isEllipsis.value =\n    props.line === 1\n      ? widthDiff > props.ellipsisThreshold\n      : heightDiff > props.ellipsisThreshold;\n};\n\n// 使用 ResizeObserver 监听尺寸变化\nlet resizeObserver: null | ResizeObserver = null;\n\nonMounted(() => {\n  if (typeof ResizeObserver !== 'undefined' && props.tooltipWhenEllipsis) {\n    resizeObserver = new ResizeObserver(() => {\n      checkEllipsis();\n    });\n\n    if (ellipsis.value) {\n      resizeObserver.observe(ellipsis.value);\n    }\n  }\n\n  // 初始检测\n  checkEllipsis();\n});\n\n// 使用onUpdated钩子检测内容变化\nonUpdated(() => {\n  if (props.tooltipWhenEllipsis) {\n    checkEllipsis();\n  }\n});\n\nonBeforeUnmount(() => {\n  if (resizeObserver) {\n    resizeObserver.disconnect();\n    resizeObserver = null;\n  }\n});\n\nwatchEffect(\n  () => {\n    if (props.tooltip && eleWidth.value) {\n      defaultTooltipMaxWidth.value =\n        props.tooltipMaxWidth ?? eleWidth.value + 24;\n    }\n  },\n  { flush: 'post' },\n);\n\nfunction onExpand() {\n  isExpand.value = !isExpand.value;\n  emit('expandChange', isExpand.value);\n  if (props.tooltipWhenEllipsis) {\n    checkEllipsis();\n  }\n}\n\nfunction handleExpand() {\n  props.expand && onExpand();\n}\n</script>\n<template>\n  <div>\n    <VbenTooltip\n      :content-style=\"{\n        ...tooltipOverlayStyle,\n        maxWidth: `${defaultTooltipMaxWidth}px`,\n        fontSize: `${tooltipFontSize}px`,\n        color: tooltipColor,\n        backgroundColor: tooltipBackgroundColor,\n      }\"\n      :disabled=\"\n        !props.tooltip || isExpand || (props.tooltipWhenEllipsis && !isEllipsis)\n      \"\n      :side=\"placement\"\n    >\n      <slot name=\"tooltip\">\n        <slot></slot>\n      </slot>\n\n      <template #trigger>\n        <div\n          ref=\"ellipsis\"\n          :class=\"{\n            '!cursor-pointer': expand,\n            ['block truncate']: line === 1,\n            [$style.ellipsisMultiLine]: line > 1,\n          }\"\n          :style=\"{\n            '-webkit-line-clamp': isExpand ? '' : line,\n            'max-width': textMaxWidth,\n          }\"\n          class=\"cursor-text overflow-hidden\"\n          @click=\"handleExpand\"\n          v-bind=\"$attrs\"\n        >\n          <slot></slot>\n        </div>\n      </template>\n    </VbenTooltip>\n  </div>\n</template>\n\n<style module>\n.ellipsisMultiLine {\n  display: -webkit-box;\n  -webkit-box-orient: vertical;\n}\n</style>\n"
  },
  {
    "path": "packages/effects/common-ui/src/components/ellipsis-text/index.ts",
    "content": "export { default as EllipsisText } from './ellipsis-text.vue';\n"
  },
  {
    "path": "packages/effects/common-ui/src/components/icon-picker/icon-picker.vue",
    "content": "<script setup lang=\"ts\">\nimport type { VNode } from 'vue';\n\nimport { computed, ref, useAttrs, watch, watchEffect } from 'vue';\n\nimport { usePagination } from '@vben/hooks';\nimport { EmptyIcon, Grip, listIcons } from '@vben/icons';\nimport { $t } from '@vben/locales';\n\nimport {\n  Button,\n  Input,\n  Pagination,\n  PaginationEllipsis,\n  PaginationFirst,\n  PaginationLast,\n  PaginationList,\n  PaginationListItem,\n  PaginationNext,\n  PaginationPrev,\n  VbenIcon,\n  VbenIconButton,\n  VbenPopover,\n} from '@vben-core/shadcn-ui';\nimport { isFunction } from '@vben-core/shared/utils';\n\nimport { objectOmit, refDebounced, watchDebounced } from '@vueuse/core';\n\nimport { fetchIconsData } from './icons';\n\ninterface Props {\n  pageSize?: number;\n  /** 图标集的名字 */\n  prefix?: string;\n  /** 是否自动请求API以获得图标集的数据.提供prefix时有效 */\n  autoFetchApi?: boolean;\n  /**\n   * 图标列表\n   */\n  icons?: string[];\n  /** Input组件 */\n  inputComponent?: VNode;\n  /** 图标插槽名，预览图标将被渲染到此插槽中 */\n  iconSlot?: string;\n  /** input组件的值属性名称 */\n  modelValueProp?: string;\n  /** 图标样式 */\n  iconClass?: string;\n  type?: 'icon' | 'input';\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n  prefix: 'ant-design',\n  pageSize: 36,\n  icons: () => [],\n  iconSlot: 'default',\n  iconClass: 'size-4',\n  autoFetchApi: true,\n  modelValueProp: 'modelValue',\n  inputComponent: undefined,\n  type: 'input',\n});\n\nconst emit = defineEmits<{\n  change: [string];\n}>();\n\nconst attrs = useAttrs();\n\nconst modelValue = defineModel({ default: '', type: String });\n\nconst visible = ref(false);\nconst currentSelect = ref('');\nconst currentPage = ref(1);\nconst keyword = ref('');\nconst keywordDebounce = refDebounced(keyword, 300);\nconst innerIcons = ref<string[]>([]);\n\n/* 当检索关键词变化时，重置分页 */\nwatch(keywordDebounce, () => {\n  currentPage.value = 1;\n  setCurrentPage(1);\n});\n\nwatchDebounced(\n  () => props.prefix,\n  async (prefix) => {\n    if (prefix && prefix !== 'svg' && props.autoFetchApi) {\n      innerIcons.value = await fetchIconsData(prefix);\n    }\n  },\n  { immediate: true, debounce: 500, maxWait: 1000 },\n);\n\nconst currentList = computed(() => {\n  try {\n    if (props.prefix) {\n      if (\n        props.prefix !== 'svg' &&\n        props.autoFetchApi &&\n        props.icons.length === 0\n      ) {\n        return innerIcons.value;\n      }\n      const icons = listIcons('', props.prefix);\n      if (icons.length === 0) {\n        console.warn(`No icons found for prefix: ${props.prefix}`);\n      }\n      return icons;\n    } else {\n      return props.icons;\n    }\n  } catch (error) {\n    console.error('Failed to load icons:', error);\n    return [];\n  }\n});\n\nconst showList = computed(() => {\n  return currentList.value.filter((item) =>\n    item.includes(keywordDebounce.value),\n  );\n});\n\nconst { paginationList, total, setCurrentPage } = usePagination(\n  showList,\n  props.pageSize,\n);\n\nwatchEffect(() => {\n  currentSelect.value = modelValue.value;\n});\n\nwatch(\n  () => currentSelect.value,\n  (v) => {\n    emit('change', v);\n  },\n);\n\nconst handleClick = (icon: string) => {\n  currentSelect.value = icon;\n  modelValue.value = icon;\n  close();\n};\n\nconst handlePageChange = (page: number) => {\n  currentPage.value = page;\n  setCurrentPage(page);\n};\n\nfunction toggleOpenState() {\n  visible.value = !visible.value;\n}\n\nfunction open() {\n  visible.value = true;\n}\n\nfunction close() {\n  visible.value = false;\n}\n\nfunction onKeywordChange(v: string) {\n  keyword.value = v;\n}\n\nconst searchInputProps = computed(() => {\n  return {\n    placeholder: $t('ui.iconPicker.search'),\n    [props.modelValueProp]: keyword.value,\n    [`onUpdate:${props.modelValueProp}`]: onKeywordChange,\n    class: 'mx-2',\n  };\n});\n\nfunction updateCurrentSelect(v: string) {\n  currentSelect.value = v;\n  const eventKey = `onUpdate:${props.modelValueProp}`;\n  if (attrs[eventKey] && isFunction(attrs[eventKey])) {\n    attrs[eventKey](v);\n  }\n}\nconst getBindAttrs = computed(() => {\n  return objectOmit(attrs, [`onUpdate:${props.modelValueProp}`]);\n});\n\ndefineExpose({ toggleOpenState, open, close });\n</script>\n<template>\n  <VbenPopover\n    v-model:open=\"visible\"\n    :content-props=\"{ align: 'end', alignOffset: -11, sideOffset: 8 }\"\n    content-class=\"p-0 pt-3 w-full\"\n    trigger-class=\"w-full\"\n  >\n    <template #trigger>\n      <template v-if=\"props.type === 'input'\">\n        <component\n          v-if=\"props.inputComponent\"\n          :is=\"inputComponent\"\n          :[modelValueProp]=\"currentSelect\"\n          :placeholder=\"$t('ui.iconPicker.placeholder')\"\n          role=\"combobox\"\n          :aria-label=\"$t('ui.iconPicker.placeholder')\"\n          aria-expanded=\"visible\"\n          :[`onUpdate:${modelValueProp}`]=\"updateCurrentSelect\"\n          v-bind=\"getBindAttrs\"\n        >\n          <template #[iconSlot]>\n            <VbenIcon\n              :icon=\"currentSelect || Grip\"\n              class=\"size-4\"\n              aria-hidden=\"true\"\n            />\n          </template>\n        </component>\n        <div class=\"relative w-full\" v-else>\n          <Input\n            v-bind=\"$attrs\"\n            v-model=\"currentSelect\"\n            :placeholder=\"$t('ui.iconPicker.placeholder')\"\n            class=\"h-8 w-full pr-8\"\n            role=\"combobox\"\n            :aria-label=\"$t('ui.iconPicker.placeholder')\"\n            aria-expanded=\"visible\"\n          />\n          <VbenIcon\n            :icon=\"currentSelect || Grip\"\n            class=\"absolute right-1 top-1 size-6\"\n            aria-hidden=\"true\"\n          />\n        </div>\n      </template>\n      <VbenIcon\n        :icon=\"currentSelect || Grip\"\n        v-else\n        class=\"size-4\"\n        v-bind=\"$attrs\"\n      />\n    </template>\n    <div class=\"mb-2 flex w-full\">\n      <component\n        v-if=\"inputComponent\"\n        :is=\"inputComponent\"\n        v-bind=\"searchInputProps\"\n      />\n      <Input\n        v-else\n        class=\"mx-2 h-8 w-full\"\n        :placeholder=\"$t('ui.iconPicker.search')\"\n        v-model=\"keyword\"\n      />\n    </div>\n\n    <template v-if=\"paginationList.length > 0\">\n      <div class=\"grid max-h-[360px] w-full grid-cols-6 justify-items-center\">\n        <VbenIconButton\n          v-for=\"(item, index) in paginationList\"\n          :key=\"index\"\n          :tooltip=\"item\"\n          tooltip-side=\"top\"\n          @click=\"handleClick(item)\"\n        >\n          <VbenIcon\n            :class=\"{\n              'text-primary transition-all': currentSelect === item,\n            }\"\n            :icon=\"item\"\n          />\n        </VbenIconButton>\n      </div>\n      <div\n        v-if=\"total >= pageSize\"\n        class=\"flex-center flex justify-end overflow-hidden border-t py-2 pr-3\"\n      >\n        <Pagination\n          :items-per-page=\"36\"\n          :sibling-count=\"1\"\n          :total=\"total\"\n          show-edges\n          size=\"small\"\n          @update:page=\"handlePageChange\"\n        >\n          <PaginationList\n            v-slot=\"{ items }\"\n            class=\"flex w-full items-center gap-1\"\n          >\n            <PaginationFirst class=\"size-5\" />\n            <PaginationPrev class=\"size-5\" />\n            <template v-for=\"(item, index) in items\">\n              <PaginationListItem\n                v-if=\"item.type === 'page'\"\n                :key=\"index\"\n                :value=\"item.value\"\n                as-child\n              >\n                <Button\n                  :variant=\"item.value === currentPage ? 'default' : 'outline'\"\n                  class=\"size-5 p-0 text-sm\"\n                >\n                  {{ item.value }}\n                </Button>\n              </PaginationListItem>\n              <PaginationEllipsis\n                v-else\n                :key=\"item.type\"\n                :index=\"index\"\n                class=\"size-5\"\n              />\n            </template>\n            <PaginationNext class=\"size-5\" />\n            <PaginationLast class=\"size-5\" />\n          </PaginationList>\n        </Pagination>\n      </div>\n    </template>\n\n    <template v-else>\n      <div class=\"flex-col-center text-muted-foreground min-h-[150px] w-full\">\n        <EmptyIcon class=\"size-10\" />\n        <div class=\"mt-1 text-sm\">{{ $t('common.noData') }}</div>\n      </div>\n    </template>\n  </VbenPopover>\n</template>\n"
  },
  {
    "path": "packages/effects/common-ui/src/components/icon-picker/icons.ts",
    "content": "import type { Recordable } from '@vben/types';\n\n/**\n * 一个缓存对象，在不刷新页面时，无需重复请求远程接口\n */\nexport const ICONS_MAP: Recordable<string[]> = {};\n\ninterface IconifyResponse {\n  prefix: string;\n  total: number;\n  title: string;\n  uncategorized?: string[];\n  categories?: Recordable<string[]>;\n  aliases?: Recordable<string>;\n}\n\nconst PENDING_REQUESTS: Recordable<Promise<string[]>> = {};\n\n/**\n * 通过Iconify接口获取图标集数据。\n * 同一时间多个图标选择器同时请求同一个图标集时，实际上只会发起一次请求（所有请求共享同一份结果）。\n * 请求结果会被缓存，刷新页面前同一个图标集不会再次请求\n * @param prefix 图标集名称\n * @returns 图标集中包含的所有图标名称\n */\nexport async function fetchIconsData(prefix: string): Promise<string[]> {\n  if (Reflect.has(ICONS_MAP, prefix) && ICONS_MAP[prefix]) {\n    return ICONS_MAP[prefix];\n  }\n  if (Reflect.has(PENDING_REQUESTS, prefix) && PENDING_REQUESTS[prefix]) {\n    return PENDING_REQUESTS[prefix];\n  }\n  PENDING_REQUESTS[prefix] = (async () => {\n    try {\n      const controller = new AbortController();\n      const timeoutId = setTimeout(() => controller.abort(), 1000 * 10);\n      const response: IconifyResponse = await fetch(\n        `https://api.iconify.design/collection?prefix=${prefix}`,\n        { signal: controller.signal },\n      ).then((res) => res.json());\n      clearTimeout(timeoutId);\n      const list = response.uncategorized || [];\n      if (response.categories) {\n        for (const category in response.categories) {\n          list.push(...(response.categories[category] || []));\n        }\n      }\n      ICONS_MAP[prefix] = list.map((v) => `${prefix}:${v}`);\n    } catch (error) {\n      console.error(`Failed to fetch icons for prefix ${prefix}:`, error);\n      return [] as string[];\n    }\n    return ICONS_MAP[prefix];\n  })();\n  return PENDING_REQUESTS[prefix];\n}\n"
  },
  {
    "path": "packages/effects/common-ui/src/components/icon-picker/index.ts",
    "content": "export { default as IconPicker } from './icon-picker.vue';\n"
  },
  {
    "path": "packages/effects/common-ui/src/components/index.ts",
    "content": "export * from './api-component';\nexport * from './captcha';\nexport * from './code-mirror';\nexport * from './col-page';\nexport * from './count-to';\nexport * from './ellipsis-text';\nexport * from './icon-picker';\nexport * from './json-preview';\nexport * from './json-viewer';\nexport * from './loading';\nexport * from './markdown';\nexport * from './page';\nexport * from './resize';\nexport * from './tippy';\nexport * from './tree';\nexport * from '@vben-core/form-ui';\nexport * from '@vben-core/popup-ui';\n\n// 给文档用\nexport {\n  VbenAvatar,\n  VbenButton,\n  VbenButtonGroup,\n  VbenCheckbox,\n  VbenCheckButtonGroup,\n  VbenCountToAnimator,\n  VbenFullScreen,\n  VbenInputPassword,\n  VbenLoading,\n  VbenLogo,\n  VbenPinInput,\n  VbenSelect,\n  VbenSpinner,\n} from '@vben-core/shadcn-ui';\n\nexport type { FlattenedItem } from '@vben-core/shadcn-ui';\nexport { globalShareState } from '@vben-core/shared/global-state';\n"
  },
  {
    "path": "packages/effects/common-ui/src/components/json-preview/index.ts",
    "content": "export { default as JsonPreview } from './json-preview.vue';\n"
  },
  {
    "path": "packages/effects/common-ui/src/components/json-preview/json-preview.vue",
    "content": "<script lang=\"ts\" setup>\nimport VueJsonPretty from 'vue-json-pretty';\nimport 'vue-json-pretty/lib/styles.css';\n\ndefineProps<{ data: any }>();\n</script>\n\n<template>\n  <VueJsonPretty :data=\"data\" :deep=\"3\" :show-length=\"true\" path=\"res\" />\n</template>\n\n<style>\nhtml[class='dark'] {\n  .vjs-tree-node:hover {\n    background-color: #333;\n  }\n}\n</style>\n"
  },
  {
    "path": "packages/effects/common-ui/src/components/json-viewer/index.ts",
    "content": "export { default as JsonViewer } from './index.vue';\n\nexport * from './types';\n"
  },
  {
    "path": "packages/effects/common-ui/src/components/json-viewer/index.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { SetupContext } from 'vue';\n\nimport type { Recordable } from '@vben/types';\n\nimport type {\n  JsonViewerAction,\n  JsonViewerProps,\n  JsonViewerToggle,\n  JsonViewerValue,\n} from './types';\n\nimport { computed, useAttrs } from 'vue';\n// @ts-ignore\nimport VueJsonViewer from 'vue-json-viewer';\n\nimport { $t } from '@vben/locales';\n\nimport { isBoolean } from '@vben-core/shared/utils';\n\n// @ts-ignore\nimport JsonBigint from 'json-bigint';\n\ndefineOptions({ name: 'JsonViewer' });\n\nconst props = withDefaults(defineProps<JsonViewerProps>(), {\n  expandDepth: 1,\n  copyable: false,\n  sort: false,\n  boxed: false,\n  theme: 'default-json-theme',\n  expanded: false,\n  previewMode: false,\n  showArrayIndex: true,\n  showDoubleQuotes: false,\n});\n\nconst emit = defineEmits<{\n  click: [event: MouseEvent];\n  copied: [event: JsonViewerAction];\n  keyClick: [key: string];\n  toggle: [param: JsonViewerToggle];\n  valueClick: [value: JsonViewerValue];\n}>();\n\nconst attrs: SetupContext['attrs'] = useAttrs();\n\nfunction handleClick(event: MouseEvent) {\n  if (\n    event.target instanceof HTMLElement &&\n    event.target.classList.contains('jv-item')\n  ) {\n    const pathNode = event.target.closest('.jv-push');\n    if (!pathNode || !pathNode.hasAttribute('path')) {\n      return;\n    }\n    const param: JsonViewerValue = {\n      path: '',\n      value: '',\n      depth: 0,\n      el: event.target,\n    };\n\n    param.path = pathNode.getAttribute('path') || '';\n    param.depth = Number(pathNode.getAttribute('depth')) || 0;\n\n    param.value = event.target.textContent || undefined;\n    param.value = JSON.parse(param.value);\n    emit('valueClick', param);\n  }\n  emit('click', event);\n}\n\n// 支持显示 bigint 数据，如较长的订单号\nconst jsonData = computed<Record<string, any>>(() => {\n  if (typeof props.value !== 'string') {\n    return props.value || {};\n  }\n\n  try {\n    return JsonBigint({ storeAsString: true }).parse(props.value);\n  } catch (error) {\n    console.error('JSON parse error:', error);\n    return {};\n  }\n});\n\nconst bindProps = computed<Recordable<any>>(() => {\n  const copyable = {\n    copyText: $t('ui.jsonViewer.copy'),\n    copiedText: $t('ui.jsonViewer.copied'),\n    timeout: 2000,\n    ...(isBoolean(props.copyable) ? {} : props.copyable),\n  };\n\n  return {\n    ...props,\n    ...attrs,\n    value: jsonData.value,\n    onCopied: (event: JsonViewerAction) => emit('copied', event),\n    onKeyclick: (key: string) => emit('keyClick', key),\n    onClick: (event: MouseEvent) => handleClick(event),\n    copyable: props.copyable ? copyable : false,\n  };\n});\n</script>\n<template>\n  <VueJsonViewer v-bind=\"bindProps\">\n    <template #copy=\"slotProps\">\n      <slot name=\"copy\" v-bind=\"slotProps\"></slot>\n    </template>\n  </VueJsonViewer>\n</template>\n<style lang=\"scss\">\n@use './style.scss';\n</style>\n"
  },
  {
    "path": "packages/effects/common-ui/src/components/json-viewer/style.scss",
    "content": ".default-json-theme {\n  font-family: Consolas, Menlo, Courier, monospace;\n  font-size: 14px;\n  color: hsl(var(--foreground));\n  white-space: nowrap;\n  background: hsl(var(--background));\n\n  &.jv-container.boxed {\n    border: 1px solid hsl(var(--border));\n  }\n\n  .jv-ellipsis {\n    display: inline-block;\n    padding: 0 4px 2px;\n    font-size: 0.9em;\n    line-height: 0.9;\n    color: hsl(var(--secondary-foreground));\n    vertical-align: 2px;\n    cursor: pointer;\n    user-select: none;\n    background-color: hsl(var(--secondary));\n    border-radius: 3px;\n  }\n\n  .jv-button {\n    color: hsl(var(--primary));\n  }\n\n  .jv-key {\n    color: hsl(var(--heavy-foreground));\n  }\n\n  .jv-item {\n    &.jv-array {\n      color: hsl(var(--heavy-foreground));\n    }\n\n    &.jv-boolean {\n      color: hsl(var(--red-400));\n    }\n\n    &.jv-function {\n      color: hsl(var(--destructive-foreground));\n    }\n\n    &.jv-number {\n      color: hsl(var(--info-foreground));\n    }\n\n    &.jv-number-float {\n      color: hsl(var(--info-foreground));\n    }\n\n    &.jv-number-integer {\n      color: hsl(var(--info-foreground));\n    }\n\n    &.jv-object {\n      color: hsl(var(--accent-darker));\n    }\n\n    &.jv-undefined {\n      color: hsl(var(--secondary-foreground));\n    }\n\n    &.jv-string {\n      color: hsl(var(--primary));\n      overflow-wrap: break-word;\n      white-space: normal;\n    }\n  }\n\n  &.jv-container .jv-code {\n    padding: 10px;\n\n    &.boxed:not(.open) {\n      padding-bottom: 20px;\n      margin-bottom: 10px;\n    }\n\n    &.open {\n      padding-bottom: 10px;\n    }\n\n    .jv-toggle {\n      &::before {\n        padding: 0 2px;\n        border-radius: 2px;\n      }\n\n      &:hover {\n        &::before {\n          background: hsl(var(--accent-foreground));\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "packages/effects/common-ui/src/components/json-viewer/types.ts",
    "content": "export interface JsonViewerProps {\n  /** 要展示的结构数据 */\n  value: any;\n  /** 展开深度 */\n  expandDepth?: number;\n  /** 是否可复制 */\n  copyable?: boolean;\n  /** 是否排序 */\n  sort?: boolean;\n  /** 显示边框 */\n  boxed?: boolean;\n  /** 主题 */\n  theme?: string;\n  /** 是否展开 */\n  expanded?: boolean;\n  /** 时间格式化函数 */\n  timeformat?: (time: Date | number | string) => string;\n  /** 预览模式 */\n  previewMode?: boolean;\n  /** 显示数组索引 */\n  showArrayIndex?: boolean;\n  /** 显示双引号 */\n  showDoubleQuotes?: boolean;\n}\n\nexport interface JsonViewerAction {\n  action: string;\n  text: string;\n  trigger: HTMLElement;\n}\n\nexport interface JsonViewerValue {\n  value: any;\n  path: string;\n  depth: number;\n  el: HTMLElement;\n}\n\nexport interface JsonViewerToggle {\n  /** 鼠标事件 */\n  event: MouseEvent;\n  /** 当前展开状态 */\n  open: boolean;\n}\n"
  },
  {
    "path": "packages/effects/common-ui/src/components/loading/directive.ts",
    "content": "import type { App, Directive, DirectiveBinding } from 'vue';\n\nimport { h, render } from 'vue';\n\nimport { VbenLoading, VbenSpinner } from '@vben-core/shadcn-ui';\nimport { isString } from '@vben-core/shared/utils';\n\nconst LOADING_INSTANCE_KEY = Symbol('loading');\nconst SPINNER_INSTANCE_KEY = Symbol('spinner');\n\nconst CLASS_NAME_RELATIVE = 'spinner-parent--relative';\n\nconst loadingDirective: Directive = {\n  mounted(el, binding) {\n    const instance = h(VbenLoading, getOptions(binding));\n    render(instance, el);\n\n    el.classList.add(CLASS_NAME_RELATIVE);\n    el[LOADING_INSTANCE_KEY] = instance;\n  },\n  unmounted(el) {\n    const instance = el[LOADING_INSTANCE_KEY];\n    el.classList.remove(CLASS_NAME_RELATIVE);\n    render(null, el);\n    instance.el.remove();\n\n    el[LOADING_INSTANCE_KEY] = null;\n  },\n\n  updated(el, binding) {\n    const instance = el[LOADING_INSTANCE_KEY];\n    const options = getOptions(binding);\n    if (options && instance?.component) {\n      try {\n        Object.keys(options).forEach((key) => {\n          instance.component.props[key] = options[key];\n        });\n        instance.component.update();\n      } catch (error) {\n        console.error(\n          'Failed to update loading component in directive:',\n          error,\n        );\n      }\n    }\n  },\n};\n\nfunction getOptions(binding: DirectiveBinding) {\n  if (binding.value === undefined) {\n    return { spinning: true };\n  } else if (typeof binding.value === 'boolean') {\n    return { spinning: binding.value };\n  } else {\n    return { ...binding.value };\n  }\n}\n\nconst spinningDirective: Directive = {\n  mounted(el, binding) {\n    const instance = h(VbenSpinner, getOptions(binding));\n    render(instance, el);\n\n    el.classList.add(CLASS_NAME_RELATIVE);\n    el[SPINNER_INSTANCE_KEY] = instance;\n  },\n  unmounted(el) {\n    const instance = el[SPINNER_INSTANCE_KEY];\n    el.classList.remove(CLASS_NAME_RELATIVE);\n    render(null, el);\n    instance.el.remove();\n\n    el[SPINNER_INSTANCE_KEY] = null;\n  },\n\n  updated(el, binding) {\n    const instance = el[SPINNER_INSTANCE_KEY];\n    const options = getOptions(binding);\n    if (options && instance?.component) {\n      try {\n        Object.keys(options).forEach((key) => {\n          instance.component.props[key] = options[key];\n        });\n        instance.component.update();\n      } catch (error) {\n        console.error(\n          'Failed to update spinner component in directive:',\n          error,\n        );\n      }\n    }\n  },\n};\n\ntype loadingDirectiveParams = {\n  /** 是否注册loading指令。如果提供一个string，则将指令注册为指定的名称 */\n  loading?: boolean | string;\n  /** 是否注册spinning指令。如果提供一个string，则将指令注册为指定的名称 */\n  spinning?: boolean | string;\n};\n\n/**\n * 注册loading指令\n * @param app\n * @param params\n */\nexport function registerLoadingDirective(\n  app: App,\n  params?: loadingDirectiveParams,\n) {\n  // 注入一个样式供指令使用，确保容器是相对定位\n  const style = document.createElement('style');\n  style.id = CLASS_NAME_RELATIVE;\n  style.innerHTML = `\n    .${CLASS_NAME_RELATIVE} {\n      position: relative !important;\n    }\n  `;\n  document.head.append(style);\n  if (params?.loading !== false) {\n    app.directive(\n      isString(params?.loading) ? params.loading : 'loading',\n      loadingDirective,\n    );\n  }\n  if (params?.spinning !== false) {\n    app.directive(\n      isString(params?.spinning) ? params.spinning : 'spinning',\n      spinningDirective,\n    );\n  }\n}\n"
  },
  {
    "path": "packages/effects/common-ui/src/components/loading/index.ts",
    "content": "export * from './directive';\nexport { default as Loading } from './loading.vue';\nexport { default as Spinner } from './spinner.vue';\n"
  },
  {
    "path": "packages/effects/common-ui/src/components/loading/loading.vue",
    "content": "<script lang=\"ts\" setup>\nimport { VbenLoading } from '@vben-core/shadcn-ui';\nimport { cn } from '@vben-core/shared/utils';\n\ninterface LoadingProps {\n  class?: string;\n  /**\n   * @zh_CN 最小加载时间\n   * @en_US Minimum loading time\n   */\n  minLoadingTime?: number;\n\n  /**\n   * @zh_CN loading状态开启\n   */\n  spinning?: boolean;\n  /**\n   * @zh_CN 文字\n   */\n  text?: string;\n}\n\ndefineOptions({ name: 'Loading' });\nconst props = defineProps<LoadingProps>();\n</script>\n<template>\n  <div :class=\"cn('relative min-h-20', props.class)\">\n    <slot></slot>\n    <VbenLoading\n      :min-loading-time=\"props.minLoadingTime\"\n      :spinning=\"props.spinning\"\n      :text=\"props.text\"\n    >\n      <template v-if=\"$slots.icon\" #icon>\n        <slot name=\"icon\"></slot>\n      </template>\n    </VbenLoading>\n  </div>\n</template>\n"
  },
  {
    "path": "packages/effects/common-ui/src/components/loading/spinner.vue",
    "content": "<script lang=\"ts\" setup>\nimport { VbenSpinner } from '@vben-core/shadcn-ui';\nimport { cn } from '@vben-core/shared/utils';\n\ninterface SpinnerProps {\n  class?: string;\n  /**\n   * @zh_CN 最小加载时间\n   * @en_US Minimum loading time\n   */\n  minLoadingTime?: number;\n  /**\n   * @zh_CN loading状态开启\n   */\n  spinning?: boolean;\n}\ndefineOptions({ name: 'Spinner' });\nconst props = defineProps<SpinnerProps>();\n</script>\n<template>\n  <div :class=\"cn('relative min-h-20', props.class)\">\n    <slot></slot>\n    <VbenSpinner\n      :min-loading-time=\"props.minLoadingTime\"\n      :spinning=\"props.spinning\"\n    />\n  </div>\n</template>\n"
  },
  {
    "path": "packages/effects/common-ui/src/components/markdown/editor.vue",
    "content": "<script setup lang=\"ts\">\nimport {\n  onBeforeUnmount,\n  onMounted,\n  type PropType,\n  shallowRef,\n  useTemplateRef,\n  watch,\n} from 'vue';\n\nimport { usePreferences } from '@vben/preferences';\n\nimport Vditor from 'vditor';\n\nimport 'vditor/dist/index.css';\n\nconst props = defineProps({\n  // 编辑器高度\n  height: {\n    // string或者number类型\n    type: [String, Number],\n    default: 500,\n  },\n  /**\n   * 编辑模式。默认值: 'wysiwyg'\n   * wysiwyg: 所见即所得\n   * ir: 即时渲染\n   * sv: 分屏预览\n   */\n  mode: {\n    type: String as PropType<'ir' | 'sv' | 'wysiwyg'>,\n    default: 'wysiwyg',\n  },\n  // 编辑器唯一ID 缓存使用 可记录上次输入\n  id: {\n    type: String,\n    required: false,\n    default: '',\n    validator(value, props) {\n      if (!value && props.enableCache) {\n        console.warn('The id is required when enableCache is true');\n        return false;\n      }\n      return true;\n    },\n  },\n  enableCache: {\n    type: Boolean,\n    default: false,\n  },\n  // 禁用编辑器\n  disabled: {\n    type: Boolean,\n    default: false,\n  },\n  // 其他配置项\n  options: {\n    type: Object as PropType<IOptions>,\n    default: () => ({}),\n  },\n});\n\nconst emit = defineEmits<{\n  // 初始化 cdn加载完成\n  mounted: [];\n}>();\n\n// 挂载节点\nconst vditorRef = useTemplateRef('vditorRef');\n// 编辑器实例\nconst vditorInstance = shallowRef<null | Vditor>(null);\n\n// 监听主题切换x\nconst { isDark, locale } = usePreferences();\nwatch(isDark, (dark) => {\n  const theme = dark ? 'dark' : 'light';\n  vditorInstance.value?.setTheme(dark ? 'dark' : 'classic', theme, theme);\n});\n\n// 双向绑定\nconst content = defineModel('value', {\n  type: String,\n  default: '',\n});\n/**\n * 为了保持外部直接(v-model)与编辑器内部的同步\n * 注意: 下面的input事件也会触发watch\n */\nwatch(content, (value) => {\n  vditorInstance.value?.setValue(value);\n});\n\n// 监听禁用\nfunction changeDisabled(disabled: boolean) {\n  if (disabled) {\n    vditorInstance.value?.disabled();\n  } else {\n    vditorInstance.value?.enable();\n  }\n}\nwatch(() => props.disabled, changeDisabled);\n\nonMounted(() => {\n  vditorInstance.value = new Vditor(vditorRef.value!, {\n    mode: props.mode,\n    value: content.value,\n    height: props.height,\n    lang: locale.value.replace('-', '_') as any,\n    cache: {\n      enable: props.enableCache,\n      id: props.id,\n    },\n    theme: isDark.value ? 'dark' : 'classic',\n    // 手动响应式\n    input(value) {\n      content.value = value;\n    },\n    // 加载完成的事件\n    after() {\n      // 需要初始化就禁用的情况\n      changeDisabled(props.disabled);\n      emit('mounted');\n    },\n    ...props.options,\n  });\n});\n\nonBeforeUnmount(() => {\n  vditorInstance.value?.destroy();\n  vditorInstance.value = null;\n});\n</script>\n\n<template>\n  <div ref=\"vditorRef\"></div>\n</template>\n"
  },
  {
    "path": "packages/effects/common-ui/src/components/markdown/index.ts",
    "content": "export { default as MarkdownEditor } from './editor.vue';\nexport { default as MarkdownPreviewer } from './preview.vue';\n"
  },
  {
    "path": "packages/effects/common-ui/src/components/markdown/preview.vue",
    "content": "<script setup lang=\"ts\">\nimport {\n  onBeforeUnmount,\n  onMounted,\n  shallowRef,\n  useTemplateRef,\n  watch,\n} from 'vue';\n\nimport { usePreferences } from '@vben/preferences';\n\nimport Vditor from 'vditor';\n\nimport 'vditor/dist/index.css';\n\ninterface Props {\n  height?: number | string;\n  options?: IOptions;\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n  height: 500,\n  options: () => ({}),\n});\n\nconst emit = defineEmits<{\n  // 初始化 cdn加载完成\n  mounted: [];\n}>();\n\n// 挂载节点\nconst vditorRef = useTemplateRef('vditorRef');\n// 编辑器实例\nconst vditorInstance = shallowRef<null | Vditor>(null);\n\n// 监听主题切换x\nconst { isDark, locale } = usePreferences();\nwatch(isDark, (dark) => {\n  const theme = dark ? 'dark' : 'light';\n  vditorInstance.value?.setTheme(dark ? 'dark' : 'classic', theme, theme);\n});\n\n// 双向绑定\nconst content = defineModel('value', {\n  type: String,\n  default: '',\n});\n\n/**\n * 由于不能输入 需要使用watch监听\n */\nwatch(content, (value) => {\n  vditorInstance.value?.setValue(value);\n});\n\nonMounted(() => {\n  vditorInstance.value = new Vditor(vditorRef.value!, {\n    mode: 'wysiwyg',\n    value: content.value,\n    height: props.height,\n    // 开启打字机模式\n    // typewriterMode: true,\n    lang: locale.value.replace('-', '_') as any,\n    cache: {\n      enable: false,\n    },\n    theme: isDark.value ? 'dark' : 'classic',\n    // 预览(只读模式) 不显示工具栏\n    toolbar: [],\n    // 加载完成的事件\n    after() {\n      emit('mounted');\n      // 禁用编辑器\n      vditorInstance.value?.disabled();\n    },\n    ...props.options,\n  });\n});\n\nonBeforeUnmount(() => {\n  vditorInstance.value?.destroy();\n  vditorInstance.value = null;\n});\n</script>\n\n<template>\n  <div ref=\"vditorRef\"></div>\n</template>\n\n<style>\n.vditor-wysiwyg pre.vditor-reset[contenteditable='false'] {\n  cursor: unset;\n  opacity: 1;\n}\n\n/**\ndark模式样式需要重置\n*/\n.vditor--dark .vditor-reset {\n  color: #d1d5da;\n}\n</style>\n"
  },
  {
    "path": "packages/effects/common-ui/src/components/page/__tests__/page.test.ts",
    "content": "import { mount } from '@vue/test-utils';\nimport { describe, expect, it } from 'vitest';\n\nimport { Page } from '..';\n\ndescribe('page.vue', () => {\n  it('renders title when passed', () => {\n    const wrapper = mount(Page, {\n      props: {\n        title: 'Test Title',\n      },\n    });\n\n    expect(wrapper.text()).toContain('Test Title');\n  });\n\n  it('renders description when passed', () => {\n    const wrapper = mount(Page, {\n      props: {\n        description: 'Test Description',\n      },\n    });\n\n    expect(wrapper.text()).toContain('Test Description');\n  });\n\n  it('renders default slot content', () => {\n    const wrapper = mount(Page, {\n      slots: {\n        default: '<p>Default Slot Content</p>',\n      },\n    });\n\n    expect(wrapper.html()).toContain('<p>Default Slot Content</p>');\n  });\n\n  it('renders footer slot when showFooter is true', () => {\n    const wrapper = mount(Page, {\n      props: {\n        showFooter: true,\n      },\n      slots: {\n        footer: '<p>Footer Slot Content</p>',\n      },\n    });\n\n    expect(wrapper.html()).toContain('<p>Footer Slot Content</p>');\n  });\n\n  it('applies the custom contentClass', () => {\n    const wrapper = mount(Page, {\n      props: {\n        contentClass: 'custom-class',\n      },\n    });\n\n    const contentDiv = wrapper.find('.p-4');\n    expect(contentDiv.classes()).toContain('custom-class');\n  });\n\n  it('does not render title slot if title prop is provided', () => {\n    const wrapper = mount(Page, {\n      props: {\n        title: 'Test Title',\n      },\n      slots: {\n        title: '<p>Title Slot Content</p>',\n      },\n    });\n\n    expect(wrapper.text()).toContain('Title Slot Content');\n    expect(wrapper.html()).not.toContain('Test Title');\n  });\n\n  it('does not render description slot if description prop is provided', () => {\n    const wrapper = mount(Page, {\n      props: {\n        description: 'Test Description',\n      },\n      slots: {\n        description: '<p>Description Slot Content</p>',\n      },\n    });\n\n    expect(wrapper.text()).toContain('Description Slot Content');\n    expect(wrapper.html()).not.toContain('Test Description');\n  });\n});\n"
  },
  {
    "path": "packages/effects/common-ui/src/components/page/index.ts",
    "content": "export { default as Page } from './page.vue';\nexport * from './types';\n"
  },
  {
    "path": "packages/effects/common-ui/src/components/page/page.vue",
    "content": "<script setup lang=\"ts\">\nimport type { StyleValue } from 'vue';\n\nimport type { PageProps } from './types';\n\nimport { computed, nextTick, onMounted, ref, useTemplateRef } from 'vue';\n\nimport { CSS_VARIABLE_LAYOUT_CONTENT_HEIGHT } from '@vben-core/shared/constants';\nimport { cn } from '@vben-core/shared/utils';\n\ndefineOptions({\n  name: 'Page',\n});\n\nconst { autoContentHeight = false, heightOffset = 0 } =\n  defineProps<PageProps>();\n\nconst headerHeight = ref(0);\nconst footerHeight = ref(0);\nconst shouldAutoHeight = ref(false);\n\nconst headerRef = useTemplateRef<HTMLDivElement>('headerRef');\nconst footerRef = useTemplateRef<HTMLDivElement>('footerRef');\n\nconst contentStyle = computed<StyleValue>(() => {\n  if (autoContentHeight) {\n    return {\n      height: `calc(var(${CSS_VARIABLE_LAYOUT_CONTENT_HEIGHT}) - ${headerHeight.value}px - ${footerHeight.value}px - ${typeof heightOffset === 'number' ? `${heightOffset}px` : heightOffset})`,\n      overflowY: shouldAutoHeight.value ? 'auto' : 'unset',\n    };\n  }\n  return {};\n});\n\nasync function calcContentHeight() {\n  if (!autoContentHeight) {\n    return;\n  }\n  await nextTick();\n  headerHeight.value = headerRef.value?.offsetHeight || 0;\n  footerHeight.value = footerRef.value?.offsetHeight || 0;\n  setTimeout(() => {\n    shouldAutoHeight.value = true;\n  }, 30);\n}\n\nonMounted(() => {\n  calcContentHeight();\n});\n</script>\n\n<template>\n  <div class=\"relative flex min-h-full flex-col\">\n    <div\n      v-if=\"\n        description ||\n        $slots.description ||\n        title ||\n        $slots.title ||\n        $slots.extra\n      \"\n      ref=\"headerRef\"\n      :class=\"\n        cn(\n          'bg-card border-border relative flex items-end border-b px-6 py-4',\n          headerClass,\n        )\n      \"\n    >\n      <div class=\"flex-auto\">\n        <slot name=\"title\">\n          <div v-if=\"title\" class=\"mb-2 flex text-lg font-semibold\">\n            {{ title }}\n          </div>\n        </slot>\n\n        <slot name=\"description\">\n          <p v-if=\"description\" class=\"text-muted-foreground\">\n            {{ description }}\n          </p>\n        </slot>\n      </div>\n\n      <div v-if=\"$slots.extra\">\n        <slot name=\"extra\"></slot>\n      </div>\n    </div>\n\n    <div :class=\"cn('h-full p-4', contentClass)\" :style=\"contentStyle\">\n      <slot></slot>\n    </div>\n    <div\n      v-if=\"$slots.footer\"\n      ref=\"footerRef\"\n      :class=\"cn('bg-card align-center flex px-6 py-4', footerClass)\"\n    >\n      <slot name=\"footer\"></slot>\n    </div>\n  </div>\n</template>\n"
  },
  {
    "path": "packages/effects/common-ui/src/components/page/types.ts",
    "content": "export interface PageProps {\n  title?: string;\n  description?: string;\n  contentClass?: string;\n  /**\n   * 根据content可见高度自适应\n   */\n  autoContentHeight?: boolean;\n  headerClass?: string;\n  footerClass?: string;\n  /**\n   * Custom height offset value (in pixels) to adjust content area sizing\n   * when used with autoContentHeight\n   * @default 0\n   */\n  heightOffset?: number;\n}\n"
  },
  {
    "path": "packages/effects/common-ui/src/components/resize/index.ts",
    "content": "export { default as VResize } from './resize.vue';\n"
  },
  {
    "path": "packages/effects/common-ui/src/components/resize/resize.vue",
    "content": "<script lang=\"ts\" setup>\n/**\n * This components is refactored from vue-drag-resize: https://github.com/kirillmurashov/vue-drag-resize\n */\n\nimport {\n  computed,\n  getCurrentInstance,\n  nextTick,\n  onBeforeUnmount,\n  onMounted,\n  ref,\n  toRefs,\n  watch,\n} from 'vue';\n\nconst props = defineProps({\n  stickSize: {\n    type: Number,\n    default: 8,\n  },\n  parentScaleX: {\n    type: Number,\n    default: 1,\n  },\n  parentScaleY: {\n    type: Number,\n    default: 1,\n  },\n  isActive: {\n    type: Boolean,\n    default: false,\n  },\n  preventActiveBehavior: {\n    type: Boolean,\n    default: false,\n  },\n  isDraggable: {\n    type: Boolean,\n    default: true,\n  },\n  isResizable: {\n    type: Boolean,\n    default: true,\n  },\n  aspectRatio: {\n    type: Boolean,\n    default: false,\n  },\n  parentLimitation: {\n    type: Boolean,\n    default: false,\n  },\n  snapToGrid: {\n    type: Boolean,\n    default: false,\n  },\n  gridX: {\n    type: Number,\n    default: 50,\n    validator(val: number) {\n      return val >= 0;\n    },\n  },\n  gridY: {\n    type: Number,\n    default: 50,\n    validator(val: number) {\n      return val >= 0;\n    },\n  },\n  parentW: {\n    type: Number,\n    default: 0,\n    validator(val: number) {\n      return val >= 0;\n    },\n  },\n  parentH: {\n    type: Number,\n    default: 0,\n    validator(val: number) {\n      return val >= 0;\n    },\n  },\n  w: {\n    type: [String, Number],\n    default: 200,\n    validator(val: number) {\n      return typeof val === 'string' ? val === 'auto' : val >= 0;\n    },\n  },\n  h: {\n    type: [String, Number],\n    default: 200,\n    validator(val: number) {\n      return typeof val === 'string' ? val === 'auto' : val >= 0;\n    },\n  },\n  minw: {\n    type: Number,\n    default: 50,\n    validator(val: number) {\n      return val >= 0;\n    },\n  },\n  minh: {\n    type: Number,\n    default: 50,\n    validator(val: number) {\n      return val >= 0;\n    },\n  },\n  x: {\n    type: Number,\n    default: 0,\n    validator(val: number) {\n      return typeof val === 'number';\n    },\n  },\n  y: {\n    type: Number,\n    default: 0,\n    validator(val: number) {\n      return typeof val === 'number';\n    },\n  },\n  z: {\n    type: [String, Number],\n    default: 'auto',\n    validator(val: number) {\n      return typeof val === 'string' ? val === 'auto' : val >= 0;\n    },\n  },\n  dragHandle: {\n    type: String,\n    default: null,\n  },\n  dragCancel: {\n    type: String,\n    default: null,\n  },\n  sticks: {\n    type: Array<'bl' | 'bm' | 'br' | 'ml' | 'mr' | 'tl' | 'tm' | 'tr'>,\n    default() {\n      return ['tl', 'tm', 'tr', 'mr', 'br', 'bm', 'bl', 'ml'];\n    },\n  },\n  axis: {\n    type: String,\n    default: 'both',\n    validator(val: string) {\n      return ['both', 'none', 'x', 'y'].includes(val);\n    },\n  },\n  contentClass: {\n    type: String,\n    required: false,\n    default: '',\n  },\n});\n\nconst emit = defineEmits([\n  'clicked',\n  'dragging',\n  'dragstop',\n  'resizing',\n  'resizestop',\n  'activated',\n  'deactivated',\n]);\n\nconst styleMapping = {\n  y: {\n    t: 'top',\n    m: 'marginTop',\n    b: 'bottom',\n  },\n  x: {\n    l: 'left',\n    m: 'marginLeft',\n    r: 'right',\n  },\n};\n\nfunction addEvents(events: Map<string, (...args: any[]) => void>) {\n  events.forEach((cb, eventName) => {\n    document.documentElement.addEventListener(eventName, cb);\n  });\n}\n\nfunction removeEvents(events: Map<string, (...args: any[]) => void>) {\n  events.forEach((cb, eventName) => {\n    document.documentElement.removeEventListener(eventName, cb);\n  });\n}\n\nconst {\n  stickSize,\n  parentScaleX,\n  parentScaleY,\n  isActive,\n  preventActiveBehavior,\n  isDraggable,\n  isResizable,\n  aspectRatio,\n  parentLimitation,\n  snapToGrid,\n  gridX,\n  gridY,\n  parentW,\n  parentH,\n  w,\n  h,\n  minw,\n  minh,\n  x,\n  y,\n  z,\n  dragHandle,\n  dragCancel,\n  sticks,\n  axis,\n  contentClass,\n} = toRefs(props);\n\n// states\nconst active = ref(false);\nconst zIndex = ref<null | number>(null);\nconst parentWidth = ref<null | number>(null);\nconst parentHeight = ref<null | number>(null);\nconst left = ref<null | number>(null);\nconst top = ref<null | number>(null);\nconst right = ref<null | number>(null);\nconst bottom = ref<null | number>(null);\n\nconst aspectFactor = ref<null | number>(null);\n\n// state end\n\nconst stickDrag = ref(false);\nconst bodyDrag = ref(false);\nconst dimensionsBeforeMove = ref({\n  pointerX: 0,\n  pointerY: 0,\n  x: 0,\n  y: 0,\n  w: 0,\n  h: 0,\n  top: 0,\n  right: 0,\n  bottom: 0,\n  left: 0,\n  width: 0,\n  height: 0,\n});\nconst limits = ref({\n  left: { min: null as null | number, max: null as null | number },\n  right: { min: null as null | number, max: null as null | number },\n  top: { min: null as null | number, max: null as null | number },\n  bottom: { min: null as null | number, max: null as null | number },\n});\nconst currentStick = ref<null | string>(null);\n\nconst parentElement = ref<HTMLElement | null>(null);\n\nconst width = computed(() => parentWidth.value! - left.value! - right.value!);\n\nconst height = computed(() => parentHeight.value! - top.value! - bottom.value!);\n\nconst rect = computed(() => ({\n  left: Math.round(left.value!),\n  top: Math.round(top.value!),\n  width: Math.round(width.value),\n  height: Math.round(height.value),\n}));\n\nconst saveDimensionsBeforeMove = ({\n  pointerX,\n  pointerY,\n}: {\n  pointerX: number;\n  pointerY: number;\n}) => {\n  dimensionsBeforeMove.value.pointerX = pointerX;\n  dimensionsBeforeMove.value.pointerY = pointerY;\n\n  dimensionsBeforeMove.value.left = left.value as number;\n  dimensionsBeforeMove.value.right = right.value as number;\n  dimensionsBeforeMove.value.top = top.value as number;\n  dimensionsBeforeMove.value.bottom = bottom.value as number;\n\n  dimensionsBeforeMove.value.width = width.value as number;\n  dimensionsBeforeMove.value.height = height.value as number;\n\n  aspectFactor.value = width.value / height.value;\n};\n\nconst sideCorrectionByLimit = (\n  limit: { max: number; min: number },\n  current: number,\n) => {\n  let value = current;\n\n  if (limit.min !== null && current < limit.min) {\n    value = limit.min;\n  } else if (limit.max !== null && limit.max < current) {\n    value = limit.max;\n  }\n\n  return value;\n};\n\nconst rectCorrectionByLimit = (rect: {\n  newBottom: number;\n  newLeft: number;\n  newRight: number;\n  newTop: number;\n}) => {\n  // const { limits } = this;\n  let { newRight, newLeft, newBottom, newTop } = rect;\n\n  type RectRange = {\n    max: number;\n    min: number;\n  };\n\n  newLeft = sideCorrectionByLimit(limits.value.left as RectRange, newLeft);\n  newRight = sideCorrectionByLimit(limits.value.right as RectRange, newRight);\n  newTop = sideCorrectionByLimit(limits.value.top as RectRange, newTop);\n  newBottom = sideCorrectionByLimit(\n    limits.value.bottom as RectRange,\n    newBottom,\n  );\n\n  return {\n    newLeft,\n    newRight,\n    newTop,\n    newBottom,\n  };\n};\n\nconst rectCorrectionByAspectRatio = (rect: {\n  newBottom: number;\n  newLeft: number;\n  newRight: number;\n  newTop: number;\n}) => {\n  let { newLeft, newRight, newTop, newBottom } = rect;\n  // const { parentWidth, parentHeight, currentStick, aspectFactor, dimensionsBeforeMove } = this;\n\n  let newWidth = parentWidth.value! - newLeft - newRight;\n  let newHeight = parentHeight.value! - newTop - newBottom;\n\n  if (currentStick.value![1] === 'm') {\n    const deltaHeight = newHeight - dimensionsBeforeMove.value.height;\n\n    newLeft -= (deltaHeight * aspectFactor.value!) / 2;\n    newRight -= (deltaHeight * aspectFactor.value!) / 2;\n  } else if (currentStick.value![0] === 'm') {\n    const deltaWidth = newWidth - dimensionsBeforeMove.value.width;\n\n    newTop -= deltaWidth / aspectFactor.value! / 2;\n    newBottom -= deltaWidth / aspectFactor.value! / 2;\n  } else if (newWidth / newHeight > aspectFactor.value!) {\n    newWidth = aspectFactor.value! * newHeight;\n\n    if (currentStick.value![1] === 'l') {\n      newLeft = parentWidth.value! - newRight - newWidth;\n    } else {\n      newRight = parentWidth.value! - newLeft - newWidth;\n    }\n  } else {\n    newHeight = newWidth / aspectFactor.value!;\n\n    if (currentStick.value![0] === 't') {\n      newTop = parentHeight.value! - newBottom - newHeight;\n    } else {\n      newBottom = parentHeight.value! - newTop - newHeight;\n    }\n  }\n\n  return { newLeft, newRight, newTop, newBottom };\n};\n\nconst stickMove = (delta: { x: number; y: number }) => {\n  let newTop = dimensionsBeforeMove.value.top;\n  let newBottom = dimensionsBeforeMove.value.bottom;\n  let newLeft = dimensionsBeforeMove.value.left;\n  let newRight = dimensionsBeforeMove.value.right;\n  switch (currentStick.value![0]) {\n    case 'b': {\n      newBottom = dimensionsBeforeMove.value.bottom + delta.y;\n\n      if (snapToGrid.value) {\n        newBottom =\n          (parentHeight.value as number) -\n          Math.round(\n            ((parentHeight.value as number) - newBottom) / gridY.value,\n          ) *\n            gridY.value;\n      }\n\n      break;\n    }\n\n    case 't': {\n      newTop = dimensionsBeforeMove.value.top - delta.y;\n\n      if (snapToGrid.value) {\n        newTop = Math.round(newTop / gridY.value) * gridY.value;\n      }\n\n      break;\n    }\n    default: {\n      break;\n    }\n  }\n\n  switch (currentStick.value![1]) {\n    case 'l': {\n      newLeft = dimensionsBeforeMove.value.left - delta.x;\n\n      if (snapToGrid.value) {\n        newLeft = Math.round(newLeft / gridX.value) * gridX.value;\n      }\n\n      break;\n    }\n\n    case 'r': {\n      newRight = dimensionsBeforeMove.value.right + delta.x;\n\n      if (snapToGrid.value) {\n        newRight =\n          (parentWidth.value as number) -\n          Math.round(((parentWidth.value as number) - newRight) / gridX.value) *\n            gridX.value;\n      }\n\n      break;\n    }\n    default: {\n      break;\n    }\n  }\n\n  ({ newLeft, newRight, newTop, newBottom } = rectCorrectionByLimit({\n    newLeft,\n    newRight,\n    newTop,\n    newBottom,\n  }));\n\n  if (aspectRatio.value) {\n    ({ newLeft, newRight, newTop, newBottom } = rectCorrectionByAspectRatio({\n      newLeft,\n      newRight,\n      newTop,\n      newBottom,\n    }));\n  }\n\n  left.value = newLeft;\n  right.value = newRight;\n  top.value = newTop;\n  bottom.value = newBottom;\n\n  emit('resizing', rect.value);\n};\n\nconst stickUp = () => {\n  stickDrag.value = false;\n  // dimensionsBeforeMove.value = {\n  //   pointerX: 0,\n  //   pointerY: 0,\n  //   x: 0,\n  //   y: 0,\n  //   w: 0,\n  //   h: 0,\n  // };\n\n  Object.assign(dimensionsBeforeMove.value, {\n    pointerX: 0,\n    pointerY: 0,\n    x: 0,\n    y: 0,\n    w: 0,\n    h: 0,\n  });\n\n  limits.value = {\n    left: { min: null, max: null },\n    right: { min: null, max: null },\n    top: { min: null, max: null },\n    bottom: { min: null, max: null },\n  };\n\n  emit('resizing', rect.value);\n  emit('resizestop', rect.value);\n};\n\nconst calcDragLimitation = () => {\n  return {\n    left: { min: 0, max: (parentWidth.value as number) - width.value },\n    right: { min: 0, max: (parentWidth.value as number) - width.value },\n    top: { min: 0, max: (parentHeight.value as number) - height.value },\n    bottom: { min: 0, max: (parentHeight.value as number) - height.value },\n  };\n};\n\nconst calcResizeLimits = () => {\n  // const { aspectFactor, width, height, bottom, top, left, right } = this;\n\n  const parentLim = parentLimitation.value ? 0 : null;\n\n  if (aspectRatio.value) {\n    if (minw.value / minh.value > (aspectFactor.value as number)) {\n      minh.value = minw.value / (aspectFactor.value as number);\n    } else {\n      minw.value = ((aspectFactor.value as number) * minh.value) as number;\n    }\n  }\n\n  const limits = {\n    left: {\n      min: parentLim,\n      max: (left.value as number) + (width.value - minw.value),\n    },\n    right: {\n      min: parentLim,\n      max: (right.value as number) + (width.value - minw.value),\n    },\n    top: {\n      min: parentLim,\n      max: (top.value as number) + (height.value - minh.value),\n    },\n    bottom: {\n      min: parentLim,\n      max: (bottom.value as number) + (height.value - minh.value),\n    },\n  };\n\n  if (aspectRatio.value) {\n    const aspectLimits = {\n      left: {\n        min:\n          left.value! -\n          Math.min(top.value!, bottom.value!) * aspectFactor.value! * 2,\n        max:\n          left.value! +\n          ((height.value - minh.value!) / 2) * aspectFactor.value! * 2,\n      },\n      right: {\n        min:\n          right.value! -\n          Math.min(top.value!, bottom.value!) * aspectFactor.value! * 2,\n        max:\n          right.value! +\n          ((height.value - minh.value!) / 2) * aspectFactor.value! * 2,\n      },\n      top: {\n        min:\n          top.value! -\n          (Math.min(left.value!, right.value!) / aspectFactor.value!) * 2,\n        max:\n          top.value! +\n          ((width.value - minw.value) / 2 / aspectFactor.value!) * 2,\n      },\n      bottom: {\n        min:\n          bottom.value! -\n          (Math.min(left.value!, right.value!) / aspectFactor.value!) * 2,\n        max:\n          bottom.value! +\n          ((width.value - minw.value) / 2 / aspectFactor.value!) * 2,\n      },\n    };\n\n    if (currentStick.value![0] === 'm') {\n      limits.left = {\n        min: Math.max(limits.left.min!, aspectLimits.left.min),\n        max: Math.min(limits.left.max, aspectLimits.left.max),\n      };\n      limits.right = {\n        min: Math.max(limits.right.min!, aspectLimits.right.min),\n        max: Math.min(limits.right.max, aspectLimits.right.max),\n      };\n    } else if (currentStick.value![1] === 'm') {\n      limits.top = {\n        min: Math.max(limits.top.min!, aspectLimits.top.min),\n        max: Math.min(limits.top.max, aspectLimits.top.max),\n      };\n      limits.bottom = {\n        min: Math.max(limits.bottom.min!, aspectLimits.bottom.min),\n        max: Math.min(limits.bottom.max, aspectLimits.bottom.max),\n      };\n    }\n  }\n\n  return limits;\n};\n\nconst positionStyle = computed(() => ({\n  top: `${top.value}px`,\n  left: `${left.value}px`,\n  zIndex: zIndex.value!,\n}));\n\nconst sizeStyle = computed(() => ({\n  width: w.value === 'auto' ? 'auto' : `${width.value}px`,\n  height: h.value === 'auto' ? 'auto' : `${height.value}px`,\n}));\n\nconst stickStyles = computed(() => (stick: string) => {\n  const stickStyle = {\n    width: `${stickSize.value / parentScaleX.value}px`,\n    height: `${stickSize.value / parentScaleY.value}px`,\n  };\n  stickStyle[\n    styleMapping.y[stick[0] as 'b' | 'm' | 't'] as 'height' | 'width'\n  ] = `${stickSize.value / parentScaleX.value / -2}px`;\n  stickStyle[\n    styleMapping.x[stick[1] as 'l' | 'm' | 'r'] as 'height' | 'width'\n  ] = `${stickSize.value / parentScaleX.value / -2}px`;\n  return stickStyle;\n});\n\nconst bodyMove = (delta: { x: number; y: number }) => {\n  let newTop = dimensionsBeforeMove.value.top - delta.y;\n  let newBottom = dimensionsBeforeMove.value.bottom + delta.y;\n  let newLeft = dimensionsBeforeMove.value.left - delta.x;\n  let newRight = dimensionsBeforeMove.value.right + delta.x;\n\n  if (snapToGrid.value) {\n    let alignTop = true;\n    let alignLeft = true;\n\n    let diffT = newTop - Math.floor(newTop / gridY.value) * gridY.value;\n    let diffB =\n      (parentHeight.value as number) -\n      newBottom -\n      Math.floor(((parentHeight.value as number) - newBottom) / gridY.value) *\n        gridY.value;\n    let diffL = newLeft - Math.floor(newLeft / gridX.value) * gridX.value;\n    let diffR =\n      (parentWidth.value as number) -\n      newRight -\n      Math.floor(((parentWidth.value as number) - newRight) / gridX.value) *\n        gridX.value;\n\n    if (diffT > gridY.value / 2) {\n      diffT -= gridY.value;\n    }\n    if (diffB > gridY.value / 2) {\n      diffB -= gridY.value;\n    }\n    if (diffL > gridX.value / 2) {\n      diffL -= gridX.value;\n    }\n    if (diffR > gridX.value / 2) {\n      diffR -= gridX.value;\n    }\n\n    if (Math.abs(diffB) < Math.abs(diffT)) {\n      alignTop = false;\n    }\n    if (Math.abs(diffR) < Math.abs(diffL)) {\n      alignLeft = false;\n    }\n\n    newTop -= alignTop ? diffT : diffB;\n    newBottom = (parentHeight.value as number) - height.value - newTop;\n    newLeft -= alignLeft ? diffL : diffR;\n    newRight = (parentWidth.value as number) - width.value - newLeft;\n  }\n\n  ({\n    newLeft: left.value,\n    newRight: right.value,\n    newTop: top.value,\n    newBottom: bottom.value,\n  } = rectCorrectionByLimit({ newLeft, newRight, newTop, newBottom }));\n\n  emit('dragging', rect.value);\n};\n\nconst bodyUp = () => {\n  bodyDrag.value = false;\n  emit('dragging', rect.value);\n  emit('dragstop', rect.value);\n\n  // dimensionsBeforeMove.value = { pointerX: 0, pointerY: 0, x: 0, y: 0, w: 0, h: 0 };\n  Object.assign(dimensionsBeforeMove.value, {\n    pointerX: 0,\n    pointerY: 0,\n    x: 0,\n    y: 0,\n    w: 0,\n    h: 0,\n  });\n\n  limits.value = {\n    left: { min: null, max: null },\n    right: { min: null, max: null },\n    top: { min: null, max: null },\n    bottom: { min: null, max: null },\n  };\n};\n\nconst stickDown = (\n  stick: string,\n  ev: { pageX: any; pageY: any; touches?: any },\n  force = false,\n) => {\n  if ((!isResizable.value || !active.value) && !force) {\n    return;\n  }\n\n  stickDrag.value = true;\n\n  const pointerX = ev.pageX === undefined ? ev.touches[0].pageX : ev.pageX;\n  const pointerY = ev.pageY === undefined ? ev.touches[0].pageY : ev.pageY;\n\n  saveDimensionsBeforeMove({ pointerX, pointerY });\n\n  currentStick.value = stick;\n\n  limits.value = calcResizeLimits();\n};\n\nconst move = (ev: MouseEvent & TouchEvent) => {\n  if (!stickDrag.value && !bodyDrag.value) {\n    return;\n  }\n\n  ev.stopPropagation();\n\n  // touches 兼容性代码\n  const pageX = ev.pageX === undefined ? ev.touches![0]!.pageX : ev.pageX;\n  const pageY = ev.pageY === undefined ? ev.touches![0]!.pageY : ev.pageY;\n\n  const delta = {\n    x: (dimensionsBeforeMove.value.pointerX - pageX) / parentScaleX.value,\n    y: (dimensionsBeforeMove.value.pointerY - pageY) / parentScaleY.value,\n  };\n\n  if (stickDrag.value) {\n    stickMove(delta);\n  }\n\n  if (bodyDrag.value) {\n    switch (axis.value) {\n      case 'none': {\n        return;\n      }\n      case 'x': {\n        delta.y = 0;\n\n        break;\n      }\n      case 'y': {\n        delta.x = 0;\n\n        break;\n      }\n      // No default\n    }\n    bodyMove(delta);\n  }\n};\n\nconst up = () => {\n  if (stickDrag.value) {\n    stickUp();\n  } else if (bodyDrag.value) {\n    bodyUp();\n  }\n};\n\nconst deselect = () => {\n  if (preventActiveBehavior.value) {\n    return;\n  }\n  active.value = false;\n};\n\nconst domEvents = ref(\n  new Map([\n    ['mousedown', deselect],\n    ['mouseleave', up],\n    ['mousemove', move],\n    ['mouseup', up],\n    ['touchcancel', up],\n    ['touchend', up],\n    ['touchmove', move],\n    ['touchstart', up],\n  ]),\n);\n\nconst container = ref<HTMLDivElement>();\n\nonMounted(() => {\n  const currentInstance = getCurrentInstance();\n  const $el = currentInstance?.vnode.el as HTMLElement;\n\n  parentElement.value = $el?.parentNode as HTMLElement;\n  parentWidth.value = parentW.value ?? parentElement.value?.clientWidth;\n  parentHeight.value = parentH.value ?? parentElement.value?.clientHeight;\n\n  left.value = x.value;\n  top.value = y.value;\n  right.value = (parentWidth.value -\n    (w.value === 'auto' ? container.value!.scrollWidth : (w.value as number)) -\n    left.value) as number;\n  bottom.value = (parentHeight.value -\n    (h.value === 'auto' ? container.value!.scrollHeight : (h.value as number)) -\n    top.value) as number;\n\n  addEvents(domEvents.value);\n\n  if (dragHandle.value) {\n    [...($el?.querySelectorAll(dragHandle.value) || [])].forEach(\n      (dragHandle) => {\n        (dragHandle as HTMLElement).dataset.dragHandle = String(\n          currentInstance?.uid,\n        );\n      },\n    );\n  }\n\n  if (dragCancel.value) {\n    [...($el?.querySelectorAll(dragCancel.value) || [])].forEach(\n      (cancelHandle) => {\n        (cancelHandle as HTMLElement).dataset.dragCancel = String(\n          currentInstance?.uid,\n        );\n      },\n    );\n  }\n});\n\nonBeforeUnmount(() => {\n  removeEvents(domEvents.value);\n});\n\nconst bodyDown = (ev: MouseEvent & TouchEvent) => {\n  const { target, button } = ev;\n\n  if (!preventActiveBehavior.value) {\n    active.value = true;\n  }\n\n  if (button && button !== 0) {\n    return;\n  }\n\n  emit('clicked', ev);\n\n  if (!active.value) {\n    return;\n  }\n\n  if (\n    dragHandle.value &&\n    (target! as HTMLElement).dataset.dragHandle !==\n      getCurrentInstance()?.uid.toString()\n  ) {\n    return;\n  }\n\n  if (\n    dragCancel.value &&\n    (target! as HTMLElement).dataset.dragCancel ===\n      getCurrentInstance()?.uid.toString()\n  ) {\n    return;\n  }\n\n  if (ev.stopPropagation !== undefined) {\n    ev.stopPropagation();\n  }\n\n  if (ev.preventDefault !== undefined) {\n    ev.preventDefault();\n  }\n\n  if (isDraggable.value) {\n    bodyDrag.value = true;\n  }\n\n  const pointerX = ev.pageX === undefined ? ev.touches[0]!.pageX : ev.pageX;\n  const pointerY = ev.pageY === undefined ? ev.touches[0]!.pageY : ev.pageY;\n\n  saveDimensionsBeforeMove({ pointerX, pointerY });\n\n  if (parentLimitation.value) {\n    limits.value = calcDragLimitation();\n  }\n};\n\nwatch(\n  () => active.value,\n  (isActive) => {\n    if (isActive) {\n      emit('activated');\n    } else {\n      emit('deactivated');\n    }\n  },\n);\n\nwatch(\n  () => isActive.value,\n  (val) => {\n    active.value = val;\n  },\n  { immediate: true },\n);\n\nwatch(\n  () => z.value,\n  (val) => {\n    if ((val as number) >= 0 || val === 'auto') {\n      zIndex.value = val as number;\n    }\n  },\n  { immediate: true },\n);\n\nwatch(\n  () => x.value,\n  (newVal, oldVal) => {\n    if (stickDrag.value || bodyDrag.value || newVal === left.value) {\n      return;\n    }\n\n    const delta = oldVal - newVal;\n\n    bodyDown({ pageX: left.value!, pageY: top.value! } as MouseEvent &\n      TouchEvent);\n    bodyMove({ x: delta, y: 0 });\n\n    nextTick(() => {\n      bodyUp();\n    });\n  },\n);\n\nwatch(\n  () => y.value,\n  (newVal, oldVal) => {\n    if (stickDrag.value || bodyDrag.value || newVal === top.value) {\n      return;\n    }\n\n    const delta = oldVal - newVal;\n\n    bodyDown({ pageX: left.value, pageY: top.value } as MouseEvent &\n      TouchEvent);\n    bodyMove({ x: 0, y: delta });\n\n    nextTick(() => {\n      bodyUp();\n    });\n  },\n);\n\nwatch(\n  () => w.value,\n  (newVal, oldVal) => {\n    if (stickDrag.value || bodyDrag.value || newVal === width.value) {\n      return;\n    }\n\n    const stick = 'mr';\n    const delta = (oldVal as number) - (newVal as number);\n\n    stickDown(\n      stick,\n      { pageX: right.value, pageY: top.value! + height.value / 2 },\n      true,\n    );\n    stickMove({ x: delta, y: 0 });\n\n    nextTick(() => {\n      stickUp();\n    });\n  },\n);\n\nwatch(\n  () => h.value,\n  (newVal, oldVal) => {\n    if (stickDrag.value || bodyDrag.value || newVal === height.value) {\n      return;\n    }\n\n    const stick = 'bm';\n    const delta = (oldVal as number) - (newVal as number);\n\n    stickDown(\n      stick,\n      { pageX: left.value! + width.value / 2, pageY: bottom.value },\n      true,\n    );\n    stickMove({ x: 0, y: delta });\n\n    nextTick(() => {\n      stickUp();\n    });\n  },\n);\n\nwatch(\n  () => parentW.value,\n  (val) => {\n    right.value = val - width.value - left.value!;\n    parentWidth.value = val;\n  },\n);\n\nwatch(\n  () => parentH.value,\n  (val) => {\n    bottom.value = val - height.value - top.value!;\n    parentHeight.value = val;\n  },\n);\n</script>\n\n<template>\n  <div\n    :class=\"`${active || isActive ? 'active' : 'inactive'} ${contentClass ? contentClass : ''}`\"\n    :style=\"positionStyle\"\n    class=\"resize\"\n    @mousedown=\"bodyDown($event as TouchEvent & MouseEvent)\"\n    @touchend=\"up\"\n    @touchstart=\"bodyDown($event as TouchEvent & MouseEvent)\"\n  >\n    <div ref=\"container\" :style=\"sizeStyle\" class=\"content-container\">\n      <slot></slot>\n    </div>\n    <div\n      v-for=\"(stick, index) of sticks\"\n      :key=\"index\"\n      :class=\"[`resize-stick-${stick}`, isResizable ? '' : 'not-resizable']\"\n      :style=\"stickStyles(stick)\"\n      class=\"resize-stick\"\n      @mousedown.stop.prevent=\"\n        stickDown(stick, $event as TouchEvent & MouseEvent)\n      \"\n      @touchstart.stop.prevent=\"\n        stickDown(stick, $event as TouchEvent & MouseEvent)\n      \"\n    ></div>\n  </div>\n</template>\n\n<style lang=\"css\" scoped>\n.resize {\n  position: absolute;\n  box-sizing: border-box;\n}\n\n.resize.active::before {\n  position: absolute;\n  top: 0;\n  left: 0;\n  box-sizing: border-box;\n  width: 100%;\n  height: 100%;\n  content: '';\n  outline: 1px dashed #d6d6d6;\n}\n\n.resize-stick {\n  position: absolute;\n  box-sizing: border-box;\n  font-size: 1px;\n  background: #fff;\n  border: 1px solid #6c6c6c;\n  box-shadow: 0 0 2px #bbb;\n}\n\n.inactive .resize-stick {\n  display: none;\n}\n\n.resize-stick-tl,\n.resize-stick-br {\n  cursor: nwse-resize;\n}\n\n.resize-stick-tm,\n.resize-stick-bm {\n  left: 50%;\n  cursor: ns-resize;\n}\n\n.resize-stick-tr,\n.resize-stick-bl {\n  cursor: nesw-resize;\n}\n\n.resize-stick-ml,\n.resize-stick-mr {\n  top: 50%;\n  cursor: ew-resize;\n}\n\n.resize-stick.not-resizable {\n  display: none;\n}\n\n.content-container {\n  position: relative;\n  display: block;\n}\n</style>\n"
  },
  {
    "path": "packages/effects/common-ui/src/components/tippy/directive.ts",
    "content": "import type { ComputedRef, Directive } from 'vue';\n\nimport { useTippy } from 'vue-tippy';\n\nexport default function useTippyDirective(isDark: ComputedRef<boolean>) {\n  const directive: Directive = {\n    mounted(el, binding, vnode) {\n      const opts =\n        typeof binding.value === 'string'\n          ? { content: binding.value }\n          : binding.value || {};\n\n      const modifiers = Object.keys(binding.modifiers || {});\n      const placement = modifiers.find((modifier) => modifier !== 'arrow');\n      const withArrow = modifiers.includes('arrow');\n\n      if (placement) {\n        opts.placement = opts.placement || placement;\n      }\n\n      if (withArrow) {\n        opts.arrow = opts.arrow === undefined ? true : opts.arrow;\n      }\n\n      if (vnode.props && vnode.props.onTippyShow) {\n        opts.onShow = function (...args: any[]) {\n          return vnode.props?.onTippyShow(...args);\n        };\n      }\n\n      if (vnode.props && vnode.props.onTippyShown) {\n        opts.onShown = function (...args: any[]) {\n          return vnode.props?.onTippyShown(...args);\n        };\n      }\n\n      if (vnode.props && vnode.props.onTippyHidden) {\n        opts.onHidden = function (...args: any[]) {\n          return vnode.props?.onTippyHidden(...args);\n        };\n      }\n\n      if (vnode.props && vnode.props.onTippyHide) {\n        opts.onHide = function (...args: any[]) {\n          return vnode.props?.onTippyHide(...args);\n        };\n      }\n\n      if (vnode.props && vnode.props.onTippyMount) {\n        opts.onMount = function (...args: any[]) {\n          return vnode.props?.onTippyMount(...args);\n        };\n      }\n\n      if (el.getAttribute('title') && !opts.content) {\n        opts.content = el.getAttribute('title');\n        el.removeAttribute('title');\n      }\n\n      if (el.getAttribute('content') && !opts.content) {\n        opts.content = el.getAttribute('content');\n      }\n\n      useTippy(el, opts);\n    },\n    unmounted(el) {\n      if (el.$tippy) {\n        el.$tippy.destroy();\n      } else if (el._tippy) {\n        el._tippy.destroy();\n      }\n    },\n\n    updated(el, binding) {\n      const opts =\n        typeof binding.value === 'string'\n          ? { content: binding.value, theme: isDark.value ? '' : 'light' }\n          : Object.assign(\n              { theme: isDark.value ? '' : 'light' },\n              binding.value,\n            );\n\n      if (el.getAttribute('title') && !opts.content) {\n        opts.content = el.getAttribute('title');\n        el.removeAttribute('title');\n      }\n\n      if (el.getAttribute('content') && !opts.content) {\n        opts.content = el.getAttribute('content');\n      }\n\n      if (el.$tippy) {\n        el.$tippy.setProps(opts || {});\n      } else if (el._tippy) {\n        el._tippy.setProps(opts || {});\n      }\n    },\n  };\n  return directive;\n}\n"
  },
  {
    "path": "packages/effects/common-ui/src/components/tippy/index.ts",
    "content": "import type { DefaultProps, Props } from 'tippy.js';\n\nimport type { App, SetupContext } from 'vue';\n\nimport { h, watchEffect } from 'vue';\nimport { setDefaultProps, Tippy as TippyComponent } from 'vue-tippy';\n\nimport { usePreferences } from '@vben-core/preferences';\n\nimport useTippyDirective from './directive';\n\nimport 'tippy.js/dist/tippy.css';\nimport 'tippy.js/dist/backdrop.css';\nimport 'tippy.js/themes/light.css';\nimport 'tippy.js/animations/scale.css';\nimport 'tippy.js/animations/shift-toward.css';\nimport 'tippy.js/animations/shift-away.css';\nimport 'tippy.js/animations/perspective.css';\n\nconst { isDark } = usePreferences();\nexport type TippyProps = Partial<\n  Props & {\n    animation?:\n      | 'fade'\n      | 'perspective'\n      | 'scale'\n      | 'shift-away'\n      | 'shift-toward'\n      | boolean;\n    theme?: 'auto' | 'dark' | 'light';\n  }\n>;\n\nexport function initTippy(app: App<Element>, options?: DefaultProps) {\n  setDefaultProps({\n    allowHTML: true,\n    delay: [500, 200],\n    theme: isDark.value ? '' : 'light',\n    ...options,\n  });\n  if (!options || !Reflect.has(options, 'theme') || options.theme === 'auto') {\n    watchEffect(() => {\n      setDefaultProps({ theme: isDark.value ? '' : 'light' });\n    });\n  }\n\n  app.directive('tippy', useTippyDirective(isDark));\n}\n\nexport const Tippy = (props: any, { attrs, slots }: SetupContext) => {\n  let theme: string = (attrs.theme as string) ?? 'auto';\n  if (theme === 'auto') {\n    theme = isDark.value ? '' : 'light';\n  }\n  if (theme === 'dark') {\n    theme = '';\n  }\n  return h(\n    TippyComponent,\n    {\n      ...props,\n      ...attrs,\n      theme,\n    },\n    slots,\n  );\n};\n"
  },
  {
    "path": "packages/effects/common-ui/src/components/tree/index.ts",
    "content": "export { default as Tree } from './tree.vue';\n"
  },
  {
    "path": "packages/effects/common-ui/src/components/tree/tree.vue",
    "content": "<script setup lang=\"ts\">\nimport type { TreeProps } from '@vben-core/shadcn-ui';\n\nimport { Inbox } from '@vben/icons';\nimport { $t } from '@vben/locales';\n\nimport { treePropsDefaults, VbenTree } from '@vben-core/shadcn-ui';\n\nconst props = withDefaults(defineProps<TreeProps>(), treePropsDefaults());\n</script>\n\n<template>\n  <VbenTree v-if=\"props.treeData?.length > 0\" v-bind=\"props\">\n    <template v-for=\"(_, key) in $slots\" :key=\"key\" #[key]=\"slotProps\">\n      <slot :name=\"key\" v-bind=\"slotProps\"> </slot>\n    </template>\n  </VbenTree>\n  <div\n    v-else\n    class=\"flex-col-center text-muted-foreground cursor-pointer rounded-lg border p-10 text-sm font-medium\"\n  >\n    <Inbox class=\"size-10\" />\n    <div class=\"mt-1\">{{ $t('common.noData') }}</div>\n  </div>\n</template>\n"
  },
  {
    "path": "packages/effects/common-ui/src/index.ts",
    "content": "export * from './components';\nexport * from './ui';\nexport { VbenAvatar } from '@vben-core/shadcn-ui';\n"
  },
  {
    "path": "packages/effects/common-ui/src/ui/about/about.ts",
    "content": "import type { Component } from 'vue';\n\ninterface AboutProps {\n  description?: string;\n  name?: string;\n  title?: string;\n}\n\ninterface DescriptionItem {\n  content: Component | string;\n  title: string;\n}\n\nexport type { AboutProps, DescriptionItem };\n"
  },
  {
    "path": "packages/effects/common-ui/src/ui/about/about.vue",
    "content": "<script setup lang=\"ts\">\nimport type { AboutProps, DescriptionItem } from './about';\n\nimport {\n  VBEN_DOC_URL,\n  VBEN_GITHUB_URL,\n  VBEN_PREVIEW_URL,\n} from '@vben/constants';\nimport { VbenRenderContent } from '@vben-core/shadcn-ui';\nimport { h } from 'vue';\n\nimport { Page } from '../../components';\n\ninterface Props extends AboutProps {}\n\ndefineOptions({\n  name: 'AboutUI',\n});\n\nwithDefaults(defineProps<Props>(), {\n  description:\n    '是一个现代化开箱即用的中后台解决方案，采用最新的技术栈，包括 Vue 3.0、Vite、TailwindCSS 和 TypeScript 等前沿技术，代码规范严谨，提供丰富的配置选项，旨在为中大型项目的开发提供现成的开箱即用解决方案及丰富的示例，同时，它也是学习和深入前端技术的一个极佳示例。',\n  name: 'Vben Admin',\n  title: '关于项目',\n});\n\ndeclare global {\n  const __VBEN_ADMIN_METADATA__: {\n    authorEmail: string;\n    authorName: string;\n    authorUrl: string;\n    buildTime: string;\n    dependencies: Record<string, string>;\n    description: string;\n    devDependencies: Record<string, string>;\n    homepage: string;\n    license: string;\n    repositoryUrl: string;\n    version: string;\n  };\n}\n\nconst renderLink = (href: string, text: string) =>\n  h(\n    'a',\n    { href, target: '_blank', class: 'vben-link' },\n    { default: () => text },\n  );\n\nconst {\n  authorEmail,\n  authorName,\n  authorUrl,\n  buildTime,\n  dependencies = {},\n  devDependencies = {},\n  homepage,\n  license,\n  version,\n  // vite inject-metadata 插件注入的全局变量\n} = __VBEN_ADMIN_METADATA__ || {};\n\nconst vbenDescriptionItems: DescriptionItem[] = [\n  {\n    content: version,\n    title: '版本号',\n  },\n  {\n    content: license,\n    title: '开源许可协议',\n  },\n  {\n    content: buildTime,\n    title: '最后构建时间',\n  },\n  {\n    content: renderLink(homepage, '点击查看'),\n    title: '主页',\n  },\n  {\n    content: renderLink(VBEN_DOC_URL, '点击查看'),\n    title: '文档地址',\n  },\n  {\n    content: renderLink(VBEN_PREVIEW_URL, '点击查看'),\n    title: '预览地址',\n  },\n  {\n    content: renderLink(VBEN_GITHUB_URL, '点击查看'),\n    title: 'Github',\n  },\n  {\n    content: h('div', [\n      renderLink(authorUrl, `${authorName}  `),\n      renderLink(`mailto:${authorEmail}`, authorEmail),\n    ]),\n    title: '作者',\n  },\n];\n\nconst dependenciesItems = Object.keys(dependencies).map((key) => ({\n  content: dependencies[key],\n  title: key,\n}));\n\nconst devDependenciesItems = Object.keys(devDependencies).map((key) => ({\n  content: devDependencies[key],\n  title: key,\n}));\n</script>\n\n<template>\n  <Page :title=\"title\">\n    <template #description>\n      <p class=\"text-foreground mt-3 text-sm leading-6\">\n        <a :href=\"VBEN_GITHUB_URL\" class=\"vben-link\" target=\"_blank\">\n          {{ name }}\n        </a>\n        {{ description }}\n      </p>\n    </template>\n    <div class=\"card-box p-5\">\n      <div>\n        <h5 class=\"text-foreground text-lg\">基本信息</h5>\n      </div>\n      <div class=\"mt-4\">\n        <dl class=\"grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4\">\n          <template v-for=\"item in vbenDescriptionItems\" :key=\"item.title\">\n            <div class=\"border-border border-t px-4 py-6 sm:col-span-1 sm:px-0\">\n              <dt class=\"text-foreground text-sm font-medium leading-6\">\n                {{ item.title }}\n              </dt>\n              <dd class=\"text-foreground mt-1 text-sm leading-6 sm:mt-2\">\n                <VbenRenderContent :content=\"item.content\" />\n              </dd>\n            </div>\n          </template>\n        </dl>\n      </div>\n    </div>\n\n    <div class=\"card-box mt-6 p-5\">\n      <div>\n        <h5 class=\"text-foreground text-lg\">生产环境依赖</h5>\n      </div>\n      <div class=\"mt-4\">\n        <dl class=\"grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4\">\n          <template v-for=\"item in dependenciesItems\" :key=\"item.title\">\n            <div class=\"border-border border-t px-4 py-3 sm:col-span-1 sm:px-0\">\n              <dt class=\"text-foreground text-sm\">\n                {{ item.title }}\n              </dt>\n              <dd class=\"text-foreground/80 mt-1 text-sm sm:mt-2\">\n                <VbenRenderContent :content=\"item.content\" />\n              </dd>\n            </div>\n          </template>\n        </dl>\n      </div>\n    </div>\n    <div class=\"card-box mt-6 p-5\">\n      <div>\n        <h5 class=\"text-foreground text-lg\">开发环境依赖</h5>\n      </div>\n      <div class=\"mt-4\">\n        <dl class=\"grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4\">\n          <template v-for=\"item in devDependenciesItems\" :key=\"item.title\">\n            <div class=\"border-border border-t px-4 py-3 sm:col-span-1 sm:px-0\">\n              <dt class=\"text-foreground text-sm\">\n                {{ item.title }}\n              </dt>\n              <dd class=\"text-foreground/80 mt-1 text-sm sm:mt-2\">\n                <VbenRenderContent :content=\"item.content\" />\n              </dd>\n            </div>\n          </template>\n        </dl>\n      </div>\n    </div>\n  </Page>\n</template>\n"
  },
  {
    "path": "packages/effects/common-ui/src/ui/about/index.ts",
    "content": "export { default as About } from './about.vue';\n"
  },
  {
    "path": "packages/effects/common-ui/src/ui/authentication/auth-title.vue",
    "content": "<template>\n  <div class=\"mb-7 sm:mx-auto sm:w-full sm:max-w-md\">\n    <h2\n      class=\"text-foreground mb-3 text-3xl font-bold leading-9 tracking-tight lg:text-4xl\"\n    >\n      <slot></slot>\n    </h2>\n\n    <p class=\"text-muted-foreground lg:text-md text-sm\">\n      <slot name=\"desc\"></slot>\n    </p>\n  </div>\n</template>\n"
  },
  {
    "path": "packages/effects/common-ui/src/ui/authentication/code-login.vue",
    "content": "<script setup lang=\"ts\">\nimport type { VbenFormSchema } from '@vben-core/form-ui';\n\nimport { computed, reactive } from 'vue';\nimport { useRouter } from 'vue-router';\n\nimport { $t } from '@vben/locales';\n\nimport { useVbenForm } from '@vben-core/form-ui';\nimport { VbenButton } from '@vben-core/shadcn-ui';\n\nimport Title from './auth-title.vue';\n\ninterface Props {\n  formSchema: VbenFormSchema[];\n  /**\n   * @zh_CN 是否处于加载处理状态\n   */\n  loading?: boolean;\n  /**\n   * @zh_CN 登录路径\n   */\n  loginPath?: string;\n  /**\n   * @zh_CN 标题\n   */\n  title?: string;\n  /**\n   * @zh_CN 描述\n   */\n  subTitle?: string;\n  /**\n   * @zh_CN 按钮文本\n   */\n  submitButtonText?: string;\n  /**\n   * @zh_CN 是否显示返回按钮\n   */\n  showBack?: boolean;\n}\n\ndefineOptions({\n  name: 'AuthenticationCodeLogin',\n});\n\nconst props = withDefaults(defineProps<Props>(), {\n  loading: false,\n  showBack: true,\n  loginPath: '/auth/login',\n  submitButtonText: '',\n  subTitle: '',\n  title: '',\n});\n\nconst emit = defineEmits<{\n  submit: [{ code: string; phoneNumber: string; tenantId: string }];\n}>();\n\nconst router = useRouter();\n\nconst [Form, formApi] = useVbenForm(\n  reactive({\n    commonConfig: {\n      hideLabel: true,\n      hideRequiredMark: true,\n    },\n    schema: computed(() => props.formSchema),\n    showDefaultActions: false,\n  }),\n);\n\nasync function handleSubmit() {\n  const { valid } = await formApi.validate();\n  const values = await formApi.getValues();\n  if (valid) {\n    emit('submit', {\n      tenantId: values?.tenantId,\n      code: values?.code,\n      phoneNumber: values?.phoneNumber,\n    });\n  }\n}\n\nfunction goToLogin() {\n  router.push(props.loginPath);\n}\n\ndefineExpose({\n  getFormApi: () => formApi,\n});\n</script>\n\n<template>\n  <div>\n    <Title>\n      <slot name=\"title\">\n        {{ title || $t('authentication.welcomeBack') }} 📲\n      </slot>\n      <template #desc>\n        <span class=\"text-muted-foreground\">\n          <slot name=\"subTitle\">\n            {{ subTitle || $t('authentication.codeSubtitle') }}\n          </slot>\n        </span>\n      </template>\n    </Title>\n    <Form />\n    <VbenButton\n      :class=\"{\n        'cursor-wait': loading,\n      }\"\n      :loading=\"loading\"\n      class=\"w-full\"\n      @click=\"handleSubmit\"\n    >\n      <slot name=\"submitButtonText\">\n        {{ submitButtonText || $t('common.login') }}\n      </slot>\n    </VbenButton>\n    <VbenButton\n      v-if=\"showBack\"\n      class=\"mt-4 w-full\"\n      variant=\"outline\"\n      @click=\"goToLogin()\"\n    >\n      {{ $t('common.back') }}\n    </VbenButton>\n  </div>\n</template>\n"
  },
  {
    "path": "packages/effects/common-ui/src/ui/authentication/dingding-login.vue",
    "content": "<script setup lang=\"ts\">\nimport { useRoute } from 'vue-router';\n\nimport { SvgDingDingIcon } from '@vben/icons';\nimport { $t } from '@vben/locales';\n\nimport { alert, useVbenModal } from '@vben-core/popup-ui';\nimport { VbenIconButton } from '@vben-core/shadcn-ui';\nimport { loadScript } from '@vben-core/shared/utils';\n\ninterface Props {\n  clientId: string;\n  corpId: string;\n  // 登录回调地址\n  redirectUri?: string;\n  // 是否内嵌二维码登录\n  isQrCode?: boolean;\n}\n\nconst props = defineProps<Props>();\n\nconst route = useRoute();\n\nconst [Modal, modalApi] = useVbenModal({\n  header: false,\n  footer: false,\n  fullscreenButton: false,\n  class: 'w-[302px] h-[302px] dingding-qrcode-login-modal',\n  onOpened() {\n    handleQrCodeLogin();\n  },\n});\n\nconst getRedirectUri = () => {\n  const { redirectUri } = props;\n  if (redirectUri) {\n    return redirectUri;\n  }\n  return window.location.origin + route.fullPath;\n};\n\n/**\n * 内嵌二维码登录\n */\nconst handleQrCodeLogin = async () => {\n  const { clientId, corpId } = props;\n  if (!(window as any).DTFrameLogin) {\n    // 二维码登录 加载资源\n    await loadScript(\n      'https://g.alicdn.com/dingding/h5-dingtalk-login/0.21.0/ddlogin.js',\n    );\n  }\n  (window as any).DTFrameLogin(\n    {\n      id: 'dingding_qrcode_login_element',\n      width: 300,\n      height: 300,\n    },\n    {\n      // 注意：redirect_uri 需为完整URL，扫码后钉钉会带code跳转到这里\n      redirect_uri: encodeURIComponent(getRedirectUri()),\n      client_id: clientId,\n      scope: 'openid corpid',\n      response_type: 'code',\n      state: '1',\n      prompt: 'consent',\n      corpId,\n    },\n    (loginResult: any) => {\n      const { redirectUrl } = loginResult;\n      // 这里可以直接进行重定向\n      window.location.href = redirectUrl;\n    },\n    (errorMsg: string) => {\n      // 这里一般需要展示登录失败的具体原因\n      alert(`Login Error: ${errorMsg}`);\n    },\n  );\n};\n\nconst handleLogin = () => {\n  const { clientId, corpId, isQrCode } = props;\n  if (isQrCode) {\n    // 内嵌二维码登录\n    modalApi.open();\n  } else {\n    window.location.href = `https://login.dingtalk.com/oauth2/auth?redirect_uri=${encodeURIComponent(getRedirectUri())}&response_type=code&client_id=${clientId}&scope=openid&corpid=${corpId}&prompt=consent`;\n  }\n};\n</script>\n\n<template>\n  <div>\n    <VbenIconButton\n      @click=\"handleLogin\"\n      :tooltip=\"$t('authentication.dingdingLogin')\"\n      tooltip-side=\"top\"\n    >\n      <SvgDingDingIcon />\n    </VbenIconButton>\n    <Modal>\n      <div id=\"dingding_qrcode_login_element\"></div>\n    </Modal>\n  </div>\n</template>\n\n<style>\n.dingding-qrcode-login-modal {\n  .relative {\n    padding: 0 !important;\n  }\n}\n</style>\n"
  },
  {
    "path": "packages/effects/common-ui/src/ui/authentication/forget-password.vue",
    "content": "<script setup lang=\"ts\">\nimport type { VbenFormSchema } from '@vben-core/form-ui';\n\nimport { $t } from '@vben/locales';\nimport { useVbenForm } from '@vben-core/form-ui';\nimport { VbenButton } from '@vben-core/shadcn-ui';\nimport { computed, reactive } from 'vue';\nimport { useRouter } from 'vue-router';\n\nimport Title from './auth-title.vue';\n\ninterface Props {\n  formSchema: VbenFormSchema[];\n  /**\n   * @zh_CN 是否处于加载处理状态\n   */\n  loading?: boolean;\n  /**\n   * @zh_CN 登录路径\n   */\n  loginPath?: string;\n  /**\n   * @zh_CN 标题\n   */\n  title?: string;\n  /**\n   * @zh_CN 描述\n   */\n  subTitle?: string;\n  /**\n   * @zh_CN 按钮文本\n   */\n  submitButtonText?: string;\n}\n\ndefineOptions({\n  name: 'ForgetPassword',\n});\n\nconst props = withDefaults(defineProps<Props>(), {\n  loading: false,\n  loginPath: '/auth/login',\n  submitButtonText: '',\n  subTitle: '',\n  title: '',\n});\n\nconst emit = defineEmits<{\n  submit: [Record<string, any>];\n}>();\n\nconst [Form, formApi] = useVbenForm(\n  reactive({\n    commonConfig: {\n      hideLabel: true,\n      hideRequiredMark: true,\n    },\n    schema: computed(() => props.formSchema),\n    showDefaultActions: false,\n  }),\n);\n\nconst router = useRouter();\n\nasync function handleSubmit() {\n  const { valid } = await formApi.validate();\n  const values = await formApi.getValues();\n  if (valid) {\n    emit('submit', values);\n  }\n}\n\nfunction goToLogin() {\n  router.push(props.loginPath);\n}\n\ndefineExpose({\n  getFormApi: () => formApi,\n});\n</script>\n\n<template>\n  <div>\n    <Title>\n      <slot name=\"title\">\n        {{ title || $t('authentication.forgetPassword') }} 🤦🏻‍♂️\n      </slot>\n      <template #desc>\n        <slot name=\"subTitle\">\n          {{ subTitle || $t('authentication.forgetPasswordSubtitle') }}\n        </slot>\n      </template>\n    </Title>\n    <Form />\n\n    <div>\n      <VbenButton\n        :class=\"{\n          'cursor-wait': loading,\n        }\"\n        aria-label=\"submit\"\n        class=\"mt-2 w-full\"\n        @click=\"handleSubmit\"\n      >\n        <slot name=\"submitButtonText\">\n          {{ submitButtonText || $t('authentication.sendResetLink') }}\n        </slot>\n      </VbenButton>\n      <VbenButton class=\"mt-4 w-full\" variant=\"outline\" @click=\"goToLogin()\">\n        {{ $t('common.back') }}\n      </VbenButton>\n    </div>\n  </div>\n</template>\n"
  },
  {
    "path": "packages/effects/common-ui/src/ui/authentication/index.ts",
    "content": "export { default as AuthenticationCodeLogin } from './code-login.vue';\nexport { default as AuthenticationForgetPassword } from './forget-password.vue';\nexport { default as AuthenticationLogin } from './login.vue';\nexport { default as AuthenticationLoginExpiredModal } from './login-expired-modal.vue';\nexport { default as AuthenticationQrCodeLogin } from './qrcode-login.vue';\nexport { default as AuthenticationRegister } from './register.vue';\nexport type {\n  AuthenticationProps,\n  GrantType,\n  LoginAndRegisterParams,\n  LoginCodeParams,\n} from './types';\n"
  },
  {
    "path": "packages/effects/common-ui/src/ui/authentication/login-expired-modal.vue",
    "content": "<script setup lang=\"ts\">\nimport type { AuthenticationProps } from './types';\n\nimport { computed, watch } from 'vue';\n\nimport { $t } from '@vben/locales';\n\nimport { useVbenModal } from '@vben-core/popup-ui';\nimport { Slot, VbenAvatar } from '@vben-core/shadcn-ui';\n\ninterface Props extends AuthenticationProps {\n  avatar?: string;\n  zIndex?: number;\n}\n\ndefineOptions({\n  name: 'LoginExpiredModal',\n});\n\nconst props = withDefaults(defineProps<Props>(), {\n  avatar: '',\n  zIndex: 0,\n});\n\nconst open = defineModel<boolean>('open');\n\nconst [Modal, modalApi] = useVbenModal();\n\nwatch(\n  () => open.value,\n  (val) => {\n    modalApi.setState({ isOpen: val });\n  },\n);\n\nconst getZIndex = computed(() => {\n  return props.zIndex || calcZIndex();\n});\n\n/**\n * 排除ant-message和loading:9999的z-index\n */\nconst zIndexExcludeClass = ['ant-message', 'loading'];\nfunction isZIndexExcludeClass(element: Element) {\n  return zIndexExcludeClass.some((className) =>\n    element.classList.contains(className),\n  );\n}\n\n/**\n * 获取最大的zIndex值\n */\nfunction calcZIndex() {\n  let maxZ = 0;\n  const elements = document.querySelectorAll('*');\n  [...elements].forEach((element) => {\n    const style = window.getComputedStyle(element);\n    const zIndex = style.getPropertyValue('z-index');\n    if (\n      zIndex &&\n      !Number.isNaN(Number.parseInt(zIndex)) &&\n      !isZIndexExcludeClass(element)\n    ) {\n      maxZ = Math.max(maxZ, Number.parseInt(zIndex));\n    }\n  });\n  return maxZ + 1;\n}\n</script>\n\n<template>\n  <div>\n    <Modal\n      :closable=\"false\"\n      :close-on-click-modal=\"false\"\n      :close-on-press-escape=\"false\"\n      :footer=\"false\"\n      :fullscreen-button=\"false\"\n      :header=\"false\"\n      :z-index=\"getZIndex\"\n      class=\"border-none px-10 py-6 text-center shadow-xl sm:w-[600px] sm:rounded-2xl md:h-[unset]\"\n    >\n      <VbenAvatar :src=\"avatar\" class=\"mx-auto mb-6 size-20\" />\n      <Slot\n        :show-forget-password=\"false\"\n        :show-register=\"false\"\n        :show-remember-me=\"false\"\n        :sub-title=\"$t('authentication.loginAgainSubTitle')\"\n        :title=\"$t('authentication.loginAgainTitle')\"\n      >\n        <slot> </slot>\n      </Slot>\n    </Modal>\n  </div>\n</template>\n"
  },
  {
    "path": "packages/effects/common-ui/src/ui/authentication/login.vue",
    "content": "<script setup lang=\"ts\">\nimport type { VbenFormSchema } from '@vben-core/form-ui';\n\nimport type { AuthenticationProps, LoginAndRegisterParams } from './types';\n\nimport { computed, onMounted, reactive, ref } from 'vue';\nimport { useRouter } from 'vue-router';\n\nimport { $t } from '@vben/locales';\n\nimport { useVbenForm } from '@vben-core/form-ui';\nimport { VbenButton, VbenCheckbox } from '@vben-core/shadcn-ui';\nimport { cloneDeep } from '@vben-core/shared/utils';\n\nimport Title from './auth-title.vue';\nimport ThirdPartyLogin from './third-party-login.vue';\n\ninterface Props extends AuthenticationProps {\n  formSchema?: VbenFormSchema[];\n}\n\ndefineOptions({\n  name: 'AuthenticationLogin',\n});\n\nconst props = withDefaults(defineProps<Props>(), {\n  codeLoginPath: '/auth/code-login',\n  forgetPasswordPath: '/auth/forget-password',\n  formSchema: () => [],\n  loading: false,\n  qrCodeLoginPath: '/auth/qrcode-login',\n  registerPath: '/auth/register',\n  showCodeLogin: true,\n  showForgetPassword: true,\n  showQrcodeLogin: true,\n  showRegister: true,\n  showRememberMe: true,\n  showThirdPartyLogin: true,\n  submitButtonText: '',\n  subTitle: '',\n  title: '',\n});\n\nconst emit = defineEmits<{\n  submit: [LoginAndRegisterParams];\n}>();\n\nconst [Form, formApi] = useVbenForm(\n  reactive({\n    commonConfig: {\n      hideLabel: true,\n      hideRequiredMark: true,\n    },\n    schema: computed(() => props.formSchema),\n    showDefaultActions: false,\n  }),\n);\nconst router = useRouter();\n\nconst REMEMBER_ME_KEY = `REMEMBER_ME_USERNAME_${location.hostname}`;\n\nconst localUsername = localStorage.getItem(REMEMBER_ME_KEY) || '';\n\nconst rememberMe = ref(!!localUsername);\n\nasync function handleSubmit() {\n  const { valid } = await formApi.validate();\n  if (valid) {\n    const values = cloneDeep(await formApi.getValues());\n    localStorage.setItem(\n      REMEMBER_ME_KEY,\n      rememberMe.value ? values?.username : '',\n    );\n    // 加上认证类型\n    (values as any).grantType = 'password';\n    emit('submit', values as LoginAndRegisterParams);\n  }\n}\n\nfunction handleGo(path: string) {\n  router.push(path);\n}\n\nonMounted(() => {\n  if (localUsername) {\n    formApi.setFieldValue('username', localUsername);\n  }\n});\n\ndefineExpose({\n  getFormApi: () => formApi,\n});\n</script>\n\n<template>\n  <div @keydown.enter.prevent=\"handleSubmit\">\n    <slot name=\"title\">\n      <Title>\n        <slot name=\"title\">\n          {{ title || `${$t('authentication.welcomeBack')} 👋🏻` }}\n        </slot>\n        <template #desc>\n          <span class=\"text-muted-foreground\">\n            <slot name=\"subTitle\">\n              {{ subTitle || $t('authentication.loginSubtitle') }}\n            </slot>\n          </span>\n        </template>\n      </Title>\n    </slot>\n\n    <Form />\n\n    <div\n      v-if=\"showRememberMe || showForgetPassword\"\n      class=\"mb-6 flex justify-between\"\n    >\n      <div class=\"flex-center\">\n        <VbenCheckbox\n          v-if=\"showRememberMe\"\n          v-model:checked=\"rememberMe\"\n          name=\"rememberMe\"\n        >\n          {{ $t('authentication.rememberMe') }}\n        </VbenCheckbox>\n      </div>\n\n      <span\n        v-if=\"showForgetPassword\"\n        class=\"vben-link text-sm font-normal\"\n        @click=\"handleGo(forgetPasswordPath)\"\n      >\n        {{ $t('authentication.forgetPassword') }}\n      </span>\n    </div>\n    <VbenButton\n      :class=\"{\n        'cursor-wait': loading,\n      }\"\n      :loading=\"loading\"\n      aria-label=\"login\"\n      class=\"w-full\"\n      @click=\"handleSubmit\"\n    >\n      {{ submitButtonText || $t('common.login') }}\n    </VbenButton>\n\n    <div\n      v-if=\"showCodeLogin || showQrcodeLogin\"\n      class=\"mb-2 mt-4 flex items-center justify-between\"\n    >\n      <VbenButton\n        v-if=\"showCodeLogin\"\n        class=\"w-1/2\"\n        variant=\"outline\"\n        @click=\"handleGo(codeLoginPath)\"\n      >\n        {{ $t('authentication.mobileLogin') }}\n      </VbenButton>\n      <VbenButton\n        v-if=\"showQrcodeLogin\"\n        class=\"ml-4 w-1/2\"\n        variant=\"outline\"\n        @click=\"handleGo(qrCodeLoginPath)\"\n      >\n        {{ $t('authentication.qrcodeLogin') }}\n      </VbenButton>\n    </div>\n\n    <!-- 第三方登录 -->\n    <slot v-if=\"showThirdPartyLogin\" name=\"third-party-login\">\n      <ThirdPartyLogin />\n    </slot>\n\n    <slot name=\"to-register\">\n      <div v-if=\"showRegister\" class=\"mt-3 text-center text-sm\">\n        {{ $t('authentication.accountTip') }}\n        <span\n          class=\"vben-link text-sm font-normal\"\n          @click=\"handleGo(registerPath)\"\n        >\n          {{ $t('authentication.createAccount') }}\n        </span>\n      </div>\n    </slot>\n  </div>\n</template>\n"
  },
  {
    "path": "packages/effects/common-ui/src/ui/authentication/qrcode-login.vue",
    "content": "<script setup lang=\"ts\">\nimport { $t } from '@vben/locales';\nimport { VbenButton } from '@vben-core/shadcn-ui';\nimport { useQRCode } from '@vueuse/integrations/useQRCode';\nimport { ref } from 'vue';\nimport { useRouter } from 'vue-router';\n\nimport Title from './auth-title.vue';\n\ninterface Props {\n  /**\n   * @zh_CN 是否处于加载处理状态\n   */\n  loading?: boolean;\n  /**\n   * @zh_CN 登录路径\n   */\n  loginPath?: string;\n  /**\n   * @zh_CN 标题\n   */\n  title?: string;\n  /**\n   * @zh_CN 描述\n   */\n  subTitle?: string;\n  /**\n   * @zh_CN 按钮文本\n   */\n  submitButtonText?: string;\n  /**\n   * @zh_CN 描述\n   */\n  description?: string;\n  /**\n   * @zh_CN 是否显示返回按钮\n   */\n  showBack?: boolean;\n}\n\ndefineOptions({\n  name: 'AuthenticationQrCodeLogin',\n});\n\nconst props = withDefaults(defineProps<Props>(), {\n  description: '',\n  loading: false,\n  showBack: true,\n  loginPath: '/auth/login',\n  submitButtonText: '',\n  subTitle: '',\n  title: '',\n});\n\nconst router = useRouter();\n\nconst text = ref('https://vben.vvbin.cn');\n\nconst qrcode = useQRCode(text, {\n  errorCorrectionLevel: 'H',\n  margin: 4,\n});\n\nfunction goToLogin() {\n  router.push(props.loginPath);\n}\n</script>\n\n<template>\n  <div>\n    <Title>\n      <slot name=\"title\">\n        {{ title || $t('authentication.welcomeBack') }} 📱\n      </slot>\n      <template #desc>\n        <span class=\"text-muted-foreground\">\n          <slot name=\"subTitle\">\n            {{ subTitle || $t('authentication.qrcodeSubtitle') }}\n          </slot>\n        </span>\n      </template>\n    </Title>\n\n    <div class=\"flex-col-center mt-6\">\n      <img :src=\"qrcode\" alt=\"qrcode\" class=\"w-1/2\" />\n      <p class=\"text-muted-foreground mt-4 text-sm\">\n        <slot name=\"description\">\n          {{ description || $t('authentication.qrcodePrompt') }}\n        </slot>\n      </p>\n    </div>\n\n    <VbenButton\n      v-if=\"showBack\"\n      class=\"mt-4 w-full\"\n      variant=\"outline\"\n      @click=\"goToLogin()\"\n    >\n      {{ $t('common.back') }}\n    </VbenButton>\n  </div>\n</template>\n"
  },
  {
    "path": "packages/effects/common-ui/src/ui/authentication/register.vue",
    "content": "<script setup lang=\"ts\">\nimport type { Recordable } from '@vben/types';\n\nimport type { VbenFormSchema } from '@vben-core/form-ui';\n\nimport { computed, reactive } from 'vue';\nimport { useRouter } from 'vue-router';\n\nimport { $t } from '@vben/locales';\n\nimport { useVbenForm } from '@vben-core/form-ui';\nimport { VbenButton } from '@vben-core/shadcn-ui';\n\nimport Title from './auth-title.vue';\n\ninterface Props {\n  formSchema?: VbenFormSchema[];\n  /**\n   * @zh_CN 是否处于加载处理状态\n   */\n  loading?: boolean;\n  /**\n   * @zh_CN 登录路径\n   */\n  loginPath?: string;\n  /**\n   * @zh_CN 标题\n   */\n  title?: string;\n  /**\n   * @zh_CN 描述\n   */\n  subTitle?: string;\n  /**\n   * @zh_CN 按钮文本\n   */\n  submitButtonText?: string;\n}\n\ndefineOptions({\n  name: 'RegisterForm',\n});\n\nconst props = withDefaults(defineProps<Props>(), {\n  formSchema: () => [],\n  loading: false,\n  loginPath: '/auth/login',\n  submitButtonText: '',\n  subTitle: '',\n  title: '',\n});\n\nconst emit = defineEmits<{\n  submit: [Recordable<any>];\n}>();\n\nconst [Form, formApi] = useVbenForm(\n  reactive({\n    commonConfig: {\n      hideLabel: true,\n      hideRequiredMark: true,\n    },\n    schema: computed(() => props.formSchema),\n    showDefaultActions: false,\n  }),\n);\n\nconst router = useRouter();\n\nasync function handleSubmit() {\n  const { valid } = await formApi.validate();\n  const values = await formApi.getValues();\n  if (valid) {\n    emit('submit', values as { password: string; username: string });\n  }\n}\n\nfunction goToLogin() {\n  router.push(props.loginPath);\n}\n\ndefineExpose({\n  getFormApi: () => formApi,\n});\n</script>\n\n<template>\n  <div>\n    <Title>\n      <slot name=\"title\">\n        {{ title || $t('authentication.createAnAccount') }} 🚀\n      </slot>\n      <template #desc>\n        <slot name=\"subTitle\">\n          {{ subTitle || $t('authentication.signUpSubtitle') }}\n        </slot>\n      </template>\n    </Title>\n    <Form />\n\n    <VbenButton\n      :class=\"{\n        'cursor-wait': loading,\n      }\"\n      :loading=\"loading\"\n      aria-label=\"register\"\n      class=\"mt-2 w-full\"\n      @click=\"handleSubmit\"\n    >\n      <slot name=\"submitButtonText\">\n        {{ submitButtonText || $t('authentication.signUp') }}\n      </slot>\n    </VbenButton>\n    <div class=\"mt-4 text-center text-sm\">\n      {{ $t('authentication.alreadyHaveAccount') }}\n      <span class=\"vben-link text-sm font-normal\" @click=\"goToLogin()\">\n        {{ $t('authentication.goToLogin') }}\n      </span>\n    </div>\n  </div>\n</template>\n"
  },
  {
    "path": "packages/effects/common-ui/src/ui/authentication/third-party-login.vue",
    "content": "<script setup lang=\"ts\">\nimport { GiteeIcon, MdiGithub, MdiQqchat, MdiWechat } from '@vben/icons';\nimport { $t } from '@vben/locales';\n\nimport { VbenIconButton } from '@vben-core/shadcn-ui';\n\ndefineOptions({\n  name: 'ThirdPartyLogin',\n});\n\ndefineEmits<{\n  /**\n   * 第三方登录 platfrom 对应平台的string\n   */\n  oauthLogin: [plateform: string];\n}>();\n</script>\n\n<template>\n  <div class=\"w-full sm:mx-auto md:max-w-md\">\n    <div class=\"mt-4 flex items-center justify-between\">\n      <span class=\"border-input w-[35%] border-b dark:border-gray-600\"></span>\n      <span class=\"text-muted-foreground text-center text-xs uppercase\">\n        {{ $t('authentication.thirdPartyLogin') }}\n      </span>\n      <span class=\"border-input w-[35%] border-b dark:border-gray-600\"></span>\n    </div>\n\n    <div class=\"mt-4 flex flex-wrap justify-around\">\n      <VbenIconButton class=\"mb-3\" @click=\"$emit('oauthLogin', 'wechat')\">\n        <MdiWechat class=\"size-[24px] text-green-600\" />\n      </VbenIconButton>\n      <VbenIconButton class=\"mb-3\" @click=\"$emit('oauthLogin', 'qq')\">\n        <MdiQqchat class=\"size-[24px]\" />\n      </VbenIconButton>\n      <VbenIconButton class=\"mb-3\" @click=\"$emit('oauthLogin', 'github')\">\n        <MdiGithub class=\"size-[24px]\" />\n      </VbenIconButton>\n      <VbenIconButton class=\"mb-3\" @click=\"$emit('oauthLogin', 'gitee')\">\n        <GiteeIcon class=\"size-[24px] text-red-700\" />\n      </VbenIconButton>\n    </div>\n  </div>\n</template>\n"
  },
  {
    "path": "packages/effects/common-ui/src/ui/authentication/types.ts",
    "content": "interface AuthenticationProps {\n  /**\n   * @zh_CN 验证码登录路径\n   */\n  codeLoginPath?: string;\n\n  /**\n   * @zh_CN 忘记密码路径\n   */\n  forgetPasswordPath?: string;\n\n  /**\n   * @zh_CN 是否处于加载处理状态\n   */\n  loading?: boolean;\n\n  /**\n   * @zh_CN 二维码登录路径\n   */\n  qrCodeLoginPath?: string;\n\n  /**\n   * @zh_CN 注册路径\n   */\n  registerPath?: string;\n\n  /**\n   * @zh_CN 是否显示验证码登录\n   */\n  showCodeLogin?: boolean;\n\n  /**\n   * @zh_CN 是否显示忘记密码\n   */\n  showForgetPassword?: boolean;\n\n  /**\n   * @zh_CN 是否显示二维码登录\n   */\n  showQrcodeLogin?: boolean;\n\n  /**\n   * @zh_CN 是否显示注册按钮\n   */\n  showRegister?: boolean;\n\n  /**\n   * @zh_CN 是否显示记住账号\n   */\n  showRememberMe?: boolean;\n\n  /**\n   * @zh_CN 是否显示第三方登录\n   */\n  showThirdPartyLogin?: boolean;\n\n  /**\n   * @zh_CN 登录框子标题\n   */\n  subTitle?: string;\n\n  /**\n   * @zh_CN 登录框标题\n   */\n  title?: string;\n  /**\n   * @zh_CN 提交按钮文本\n   */\n  submitButtonText?: string;\n}\n\n/**\n * 登录类型\n * password 密码\n * sms 短信\n * social 第三方oauth\n * email 邮箱\n * xcx 小程序\n */\ntype GrantType = 'email' | 'password' | 'sms' | 'social' | 'xcx';\n\ninterface LoginAndRegisterParams {\n  code?: string;\n  grantType: GrantType;\n  password: string;\n  tenantId: string;\n  username: string;\n  uuid?: string;\n}\n\ninterface LoginCodeParams {\n  tenantId: string;\n  code: string;\n  phoneNumber: string;\n}\n\ninterface LoginEmits {\n  submit: [LoginAndRegisterParams];\n}\n\ninterface LoginCodeEmits {\n  submit: [LoginCodeParams];\n}\n\ninterface RegisterEmits {\n  submit: [LoginAndRegisterParams];\n}\n\nexport type {\n  AuthenticationProps,\n  GrantType,\n  LoginAndRegisterParams,\n  LoginCodeEmits,\n  LoginCodeParams,\n  LoginEmits,\n  RegisterEmits,\n};\n"
  },
  {
    "path": "packages/effects/common-ui/src/ui/dashboard/analysis/analysis-chart-card.vue",
    "content": "<script setup lang=\"ts\">\nimport { Card, CardContent, CardHeader, CardTitle } from '@vben-core/shadcn-ui';\n\ninterface Props {\n  title: string;\n}\n\ndefineOptions({\n  name: 'AnalysisChartCard',\n});\n\nwithDefaults(defineProps<Props>(), {});\n</script>\n\n<template>\n  <Card>\n    <CardHeader>\n      <CardTitle class=\"text-xl\">{{ title }}</CardTitle>\n    </CardHeader>\n    <CardContent>\n      <slot></slot>\n    </CardContent>\n  </Card>\n</template>\n"
  },
  {
    "path": "packages/effects/common-ui/src/ui/dashboard/analysis/analysis-charts-tabs.vue",
    "content": "<script setup lang=\"ts\">\nimport type { TabOption } from '@vben/types';\n\nimport { computed } from 'vue';\n\nimport { Tabs, TabsContent, TabsList, TabsTrigger } from '@vben-core/shadcn-ui';\n\ninterface Props {\n  tabs?: TabOption[];\n}\n\ndefineOptions({\n  name: 'AnalysisChartsTabs',\n});\n\nconst props = withDefaults(defineProps<Props>(), {\n  tabs: () => [],\n});\n\nconst defaultValue = computed(() => {\n  return props.tabs?.[0]?.value;\n});\n</script>\n\n<template>\n  <div class=\"card-box w-full px-4 pb-5 pt-3\">\n    <Tabs :default-value=\"defaultValue\">\n      <TabsList>\n        <template v-for=\"tab in tabs\" :key=\"tab.label\">\n          <TabsTrigger :value=\"tab.value\"> {{ tab.label }} </TabsTrigger>\n        </template>\n      </TabsList>\n      <template v-for=\"tab in tabs\" :key=\"tab.label\">\n        <TabsContent :value=\"tab.value\" class=\"pt-4\">\n          <slot :name=\"tab.value\"></slot>\n        </TabsContent>\n      </template>\n    </Tabs>\n  </div>\n</template>\n"
  },
  {
    "path": "packages/effects/common-ui/src/ui/dashboard/analysis/analysis-overview.vue",
    "content": "<script setup lang=\"ts\">\nimport type { AnalysisOverviewItem } from '../typing';\n\nimport {\n  Card,\n  CardContent,\n  CardFooter,\n  CardHeader,\n  CardTitle,\n  VbenCountToAnimator,\n  VbenIcon,\n} from '@vben-core/shadcn-ui';\n\ninterface Props {\n  items?: AnalysisOverviewItem[];\n}\n\ndefineOptions({\n  name: 'AnalysisOverview',\n});\n\nwithDefaults(defineProps<Props>(), {\n  items: () => [],\n});\n</script>\n\n<template>\n  <div class=\"grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-4\">\n    <template v-for=\"item in items\" :key=\"item.title\">\n      <Card :title=\"item.title\" class=\"w-full\">\n        <CardHeader>\n          <CardTitle class=\"text-xl\">{{ item.title }}</CardTitle>\n        </CardHeader>\n\n        <CardContent class=\"flex items-center justify-between\">\n          <VbenCountToAnimator\n            :end-val=\"item.value\"\n            :start-val=\"1\"\n            class=\"text-xl\"\n            prefix=\"\"\n          />\n          <VbenIcon :icon=\"item.icon\" class=\"size-8 flex-shrink-0\" />\n        </CardContent>\n        <CardFooter class=\"justify-between\">\n          <span>{{ item.totalTitle }}</span>\n          <VbenCountToAnimator\n            :end-val=\"item.totalValue\"\n            :start-val=\"1\"\n            prefix=\"\"\n          />\n        </CardFooter>\n      </Card>\n    </template>\n  </div>\n</template>\n"
  },
  {
    "path": "packages/effects/common-ui/src/ui/dashboard/analysis/index.ts",
    "content": "export { default as AnalysisChartCard } from './analysis-chart-card.vue';\nexport { default as AnalysisChartsTabs } from './analysis-charts-tabs.vue';\nexport { default as AnalysisOverview } from './analysis-overview.vue';\n"
  },
  {
    "path": "packages/effects/common-ui/src/ui/dashboard/index.ts",
    "content": "export * from './analysis';\nexport type * from './typing';\nexport * from './workbench';\n"
  },
  {
    "path": "packages/effects/common-ui/src/ui/dashboard/typing.ts",
    "content": "import type { Component } from 'vue';\n\ninterface AnalysisOverviewItem {\n  icon: Component | string;\n  title: string;\n  totalTitle: string;\n  totalValue: number;\n  value: number;\n}\n\ninterface WorkbenchProjectItem {\n  color?: string;\n  content: string;\n  date: string;\n  group: string;\n  icon: Component | string;\n  title: string;\n  url?: string;\n}\n\ninterface WorkbenchTrendItem {\n  avatar: string;\n  content: string;\n  date: string;\n  title: string;\n}\n\ninterface WorkbenchTodoItem {\n  completed: boolean;\n  content: string;\n  date: string;\n  title: string;\n}\n\ninterface WorkbenchQuickNavItem {\n  color?: string;\n  icon: Component | string;\n  title: string;\n  url?: string;\n}\n\nexport type {\n  AnalysisOverviewItem,\n  WorkbenchProjectItem,\n  WorkbenchQuickNavItem,\n  WorkbenchTodoItem,\n  WorkbenchTrendItem,\n};\n"
  },
  {
    "path": "packages/effects/common-ui/src/ui/dashboard/workbench/index.ts",
    "content": "export { default as WorkbenchHeader } from './workbench-header.vue';\nexport { default as WorkbenchProject } from './workbench-project.vue';\nexport { default as WorkbenchQuickNav } from './workbench-quick-nav.vue';\nexport { default as WorkbenchTodo } from './workbench-todo.vue';\nexport { default as WorkbenchTrends } from './workbench-trends.vue';\n"
  },
  {
    "path": "packages/effects/common-ui/src/ui/dashboard/workbench/workbench-header.vue",
    "content": "<script lang=\"ts\" setup>\nimport { VbenAvatar } from '@vben-core/shadcn-ui';\n\ninterface Props {\n  avatar?: string;\n}\n\ndefineOptions({\n  name: 'WorkbenchHeader',\n});\n\nwithDefaults(defineProps<Props>(), {\n  avatar: '',\n});\n</script>\n<template>\n  <div class=\"card-box p-4 py-6 lg:flex\">\n    <VbenAvatar :src=\"avatar\" class=\"size-20\" />\n    <div\n      v-if=\"$slots.title || $slots.description\"\n      class=\"flex flex-col justify-center md:ml-6 md:mt-0\"\n    >\n      <h1 v-if=\"$slots.title\" class=\"text-md font-semibold md:text-xl\">\n        <slot name=\"title\"></slot>\n      </h1>\n      <span v-if=\"$slots.description\" class=\"text-foreground/80 mt-1\">\n        <slot name=\"description\"></slot>\n      </span>\n    </div>\n    <div class=\"mt-4 flex flex-1 justify-end md:mt-0\">\n      <div class=\"flex flex-col justify-center text-right\">\n        <span class=\"text-foreground/80\"> 待办 </span>\n        <span class=\"text-2xl\">2/10</span>\n      </div>\n\n      <div class=\"mx-12 flex flex-col justify-center text-right md:mx-16\">\n        <span class=\"text-foreground/80\"> 项目 </span>\n        <span class=\"text-2xl\">8</span>\n      </div>\n      <div class=\"mr-4 flex flex-col justify-center text-right md:mr-10\">\n        <span class=\"text-foreground/80\"> 团队 </span>\n        <span class=\"text-2xl\">300</span>\n      </div>\n    </div>\n  </div>\n</template>\n"
  },
  {
    "path": "packages/effects/common-ui/src/ui/dashboard/workbench/workbench-project.vue",
    "content": "<script setup lang=\"ts\">\nimport type { WorkbenchProjectItem } from '../typing';\n\nimport {\n  Card,\n  CardContent,\n  CardHeader,\n  CardTitle,\n  VbenIcon,\n} from '@vben-core/shadcn-ui';\n\ninterface Props {\n  items?: WorkbenchProjectItem[];\n  title: string;\n}\n\ndefineOptions({\n  name: 'WorkbenchProject',\n});\n\nwithDefaults(defineProps<Props>(), {\n  items: () => [],\n});\n\ndefineEmits(['click']);\n</script>\n\n<template>\n  <Card>\n    <CardHeader class=\"py-4\">\n      <CardTitle class=\"text-lg\">{{ title }}</CardTitle>\n    </CardHeader>\n    <CardContent class=\"flex flex-wrap p-0\">\n      <template v-for=\"(item, index) in items\" :key=\"item.title\">\n        <div\n          :class=\"{\n            'border-r-0': index % 3 === 2,\n            'border-b-0': index < 3,\n            'pb-4': index > 2,\n            'rounded-bl-xl': index === items.length - 3,\n            'rounded-br-xl': index === items.length - 1,\n          }\"\n          class=\"border-border group w-full cursor-pointer border-r border-t p-4 transition-all hover:shadow-xl md:w-1/2 lg:w-1/3\"\n        >\n          <div class=\"flex items-center\">\n            <VbenIcon\n              :color=\"item.color\"\n              :icon=\"item.icon\"\n              class=\"size-8 transition-all duration-300 group-hover:scale-110\"\n              @click=\"$emit('click', item)\"\n            />\n            <span class=\"ml-4 text-lg font-medium\">{{ item.title }}</span>\n          </div>\n          <div class=\"text-foreground/80 mt-4 flex h-10\">\n            {{ item.content }}\n          </div>\n          <div class=\"text-foreground/80 flex justify-between\">\n            <span>{{ item.group }}</span>\n            <span>{{ item.date }}</span>\n          </div>\n        </div>\n      </template>\n    </CardContent>\n  </Card>\n</template>\n"
  },
  {
    "path": "packages/effects/common-ui/src/ui/dashboard/workbench/workbench-quick-nav.vue",
    "content": "<script setup lang=\"ts\">\nimport type { WorkbenchQuickNavItem } from '../typing';\n\nimport {\n  Card,\n  CardContent,\n  CardHeader,\n  CardTitle,\n  VbenIcon,\n} from '@vben-core/shadcn-ui';\n\ninterface Props {\n  items?: WorkbenchQuickNavItem[];\n  title: string;\n}\n\ndefineOptions({\n  name: 'WorkbenchQuickNav',\n});\n\nwithDefaults(defineProps<Props>(), {\n  items: () => [],\n});\n\ndefineEmits(['click']);\n</script>\n\n<template>\n  <Card>\n    <CardHeader class=\"py-4\">\n      <CardTitle class=\"text-lg\">{{ title }}</CardTitle>\n    </CardHeader>\n    <CardContent class=\"flex flex-wrap p-0\">\n      <template v-for=\"(item, index) in items\" :key=\"item.title\">\n        <div\n          :class=\"{\n            'border-r-0': index % 3 === 2,\n            'border-b-0': index < 3,\n            'pb-4': index > 2,\n            'rounded-bl-xl': index === items.length - 3,\n            'rounded-br-xl': index === items.length - 1,\n          }\"\n          class=\"flex-col-center border-border group w-1/3 cursor-pointer border-r border-t py-8 hover:shadow-xl\"\n          @click=\"$emit('click', item)\"\n        >\n          <VbenIcon\n            :color=\"item.color\"\n            :icon=\"item.icon\"\n            class=\"size-7 transition-all duration-300 group-hover:scale-125\"\n          />\n          <span class=\"text-md mt-2 truncate\">{{ item.title }}</span>\n        </div>\n      </template>\n    </CardContent>\n  </Card>\n</template>\n"
  },
  {
    "path": "packages/effects/common-ui/src/ui/dashboard/workbench/workbench-todo.vue",
    "content": "<script setup lang=\"ts\">\nimport type { WorkbenchTodoItem } from '../typing';\n\nimport {\n  Card,\n  CardContent,\n  CardHeader,\n  CardTitle,\n  VbenCheckbox,\n} from '@vben-core/shadcn-ui';\n\ninterface Props {\n  items?: WorkbenchTodoItem[];\n  title: string;\n}\n\ndefineOptions({\n  name: 'WorkbenchTodo',\n});\n\nwithDefaults(defineProps<Props>(), {\n  items: () => [],\n});\n</script>\n\n<template>\n  <Card>\n    <CardHeader class=\"py-4\">\n      <CardTitle class=\"text-lg\">{{ title }}</CardTitle>\n    </CardHeader>\n    <CardContent class=\"flex flex-wrap p-5 pt-0\">\n      <ul class=\"divide-border w-full divide-y\" role=\"list\">\n        <li\n          v-for=\"item in items\"\n          :key=\"item.title\"\n          :class=\"{\n            'select-none line-through opacity-60': item.completed,\n          }\"\n          class=\"flex cursor-pointer justify-between gap-x-6 py-5\"\n        >\n          <div class=\"flex min-w-0 items-center gap-x-4\">\n            <VbenCheckbox v-model:checked=\"item.completed\" name=\"completed\" />\n            <div class=\"min-w-0 flex-auto\">\n              <p class=\"text-foreground text-sm font-semibold leading-6\">\n                {{ item.title }}\n              </p>\n              <!-- eslint-disable vue/no-v-html -->\n              <p\n                class=\"text-foreground/80 *:text-primary mt-1 truncate text-xs leading-5\"\n                v-html=\"item.content\"\n              ></p>\n            </div>\n          </div>\n          <div class=\"hidden h-full shrink-0 sm:flex sm:flex-col sm:items-end\">\n            <span class=\"text-foreground/80 mt-6 text-xs leading-6\">\n              {{ item.date }}\n            </span>\n          </div>\n        </li>\n      </ul>\n    </CardContent>\n  </Card>\n</template>\n"
  },
  {
    "path": "packages/effects/common-ui/src/ui/dashboard/workbench/workbench-trends.vue",
    "content": "<script setup lang=\"ts\">\nimport type { WorkbenchTrendItem } from '../typing';\n\nimport {\n  Card,\n  CardContent,\n  CardHeader,\n  CardTitle,\n  VbenIcon,\n} from '@vben-core/shadcn-ui';\n\ninterface Props {\n  items?: WorkbenchTrendItem[];\n  title: string;\n}\n\ndefineOptions({\n  name: 'WorkbenchTrends',\n});\n\nwithDefaults(defineProps<Props>(), {\n  items: () => [],\n});\n</script>\n\n<template>\n  <Card>\n    <CardHeader class=\"py-4\">\n      <CardTitle class=\"text-lg\">{{ title }}</CardTitle>\n    </CardHeader>\n    <CardContent class=\"flex flex-wrap p-5 pt-0\">\n      <ul class=\"divide-border w-full divide-y\" role=\"list\">\n        <li\n          v-for=\"item in items\"\n          :key=\"item.title\"\n          class=\"flex justify-between gap-x-6 py-5\"\n        >\n          <div class=\"flex min-w-0 items-center gap-x-4\">\n            <VbenIcon\n              :icon=\"item.avatar\"\n              alt=\"\"\n              class=\"size-10 flex-none rounded-full\"\n            />\n            <div class=\"min-w-0 flex-auto\">\n              <p class=\"text-foreground text-sm font-semibold leading-6\">\n                {{ item.title }}\n              </p>\n              <!-- eslint-disable vue/no-v-html -->\n              <p\n                class=\"text-foreground/80 *:text-primary mt-1 truncate text-xs leading-5\"\n                v-html=\"item.content\"\n              ></p>\n            </div>\n          </div>\n          <div class=\"hidden h-full shrink-0 sm:flex sm:flex-col sm:items-end\">\n            <span class=\"text-foreground/80 mt-6 text-xs leading-6\">\n              {{ item.date }}\n            </span>\n          </div>\n        </li>\n      </ul>\n    </CardContent>\n  </Card>\n</template>\n"
  },
  {
    "path": "packages/effects/common-ui/src/ui/fallback/fallback.ts",
    "content": "interface FallbackProps {\n  /**\n   * 描述\n   */\n  description?: string;\n  /**\n   *  @zh_CN 首页路由地址\n   *  @default /\n   */\n  homePath?: string;\n  /**\n   * @zh_CN 默认显示的图片\n   * @default pageNotFoundSvg\n   */\n  image?: string;\n  /**\n   *  @zh_CN 内置类型\n   */\n  status?: '403' | '404' | '500' | 'coming-soon' | 'offline';\n  /**\n   *  @zh_CN 页面提示语\n   */\n  title?: string;\n}\nexport type { FallbackProps };\n"
  },
  {
    "path": "packages/effects/common-ui/src/ui/fallback/fallback.vue",
    "content": "<script setup lang=\"ts\">\nimport type { FallbackProps } from './fallback';\n\nimport { ArrowLeft, RotateCw } from '@vben/icons';\nimport { $t } from '@vben/locales';\nimport { VbenButton } from '@vben-core/shadcn-ui';\nimport { computed, defineAsyncComponent } from 'vue';\nimport { useRouter } from 'vue-router';\n\ninterface Props extends FallbackProps {}\n\ndefineOptions({\n  name: 'Fallback',\n});\n\nconst props = withDefaults(defineProps<Props>(), {\n  description: '',\n  homePath: '/',\n  image: '',\n  showBack: true,\n  status: 'coming-soon',\n  title: '',\n});\n\nconst Icon403 = defineAsyncComponent(() => import('./icons/icon-403.vue'));\nconst Icon404 = defineAsyncComponent(() => import('./icons/icon-404.vue'));\nconst Icon500 = defineAsyncComponent(() => import('./icons/icon-500.vue'));\nconst IconHello = defineAsyncComponent(\n  () => import('./icons/icon-coming-soon.vue'),\n);\nconst IconOffline = defineAsyncComponent(\n  () => import('./icons/icon-offline.vue'),\n);\n\nconst titleText = computed(() => {\n  if (props.title) {\n    return props.title;\n  }\n\n  switch (props.status) {\n    case '403': {\n      return $t('ui.fallback.forbidden');\n    }\n    case '404': {\n      return $t('ui.fallback.pageNotFound');\n    }\n    case '500': {\n      return $t('ui.fallback.internalError');\n    }\n    case 'coming-soon': {\n      return $t('ui.fallback.comingSoon');\n    }\n    case 'offline': {\n      return $t('ui.fallback.offlineError');\n    }\n    default: {\n      return '';\n    }\n  }\n});\n\nconst descText = computed(() => {\n  if (props.description) {\n    return props.description;\n  }\n  switch (props.status) {\n    case '403': {\n      return $t('ui.fallback.forbiddenDesc');\n    }\n    case '404': {\n      return $t('ui.fallback.pageNotFoundDesc');\n    }\n    case '500': {\n      return $t('ui.fallback.internalErrorDesc');\n    }\n    case 'offline': {\n      return $t('ui.fallback.offlineErrorDesc');\n    }\n    default: {\n      return '';\n    }\n  }\n});\n\nconst fallbackIcon = computed(() => {\n  switch (props.status) {\n    case '403': {\n      return Icon403;\n    }\n    case '404': {\n      return Icon404;\n    }\n    case '500': {\n      return Icon500;\n    }\n    case 'coming-soon': {\n      return IconHello;\n    }\n    case 'offline': {\n      return IconOffline;\n    }\n    default: {\n      return null;\n    }\n  }\n});\n\nconst showBack = computed(() => {\n  return props.status === '403' || props.status === '404';\n});\n\nconst showRefresh = computed(() => {\n  return props.status === '500' || props.status === 'offline';\n});\n\nconst { push } = useRouter();\n\n// 返回首页\nfunction back() {\n  push(props.homePath);\n}\n\nfunction refresh() {\n  location.reload();\n}\n</script>\n\n<template>\n  <div class=\"flex size-full flex-col items-center justify-center duration-300\">\n    <img v-if=\"image\" :src=\"image\" class=\"md:1/3 w-1/2 lg:w-1/4\" />\n    <component\n      :is=\"fallbackIcon\"\n      v-else-if=\"fallbackIcon\"\n      class=\"md:1/3 h-1/3 w-1/2 lg:w-1/4\"\n    />\n    <div class=\"flex-col-center\">\n      <slot v-if=\"$slots.title\" name=\"title\"></slot>\n      <p\n        v-else-if=\"titleText\"\n        class=\"text-foreground mt-8 text-2xl md:text-3xl lg:text-4xl\"\n      >\n        {{ titleText }}\n      </p>\n      <slot v-if=\"$slots.describe\" name=\"describe\"></slot>\n      <p\n        v-else-if=\"descText\"\n        class=\"text-muted-foreground md:text-md my-4 lg:text-lg\"\n      >\n        {{ descText }}\n      </p>\n      <slot v-if=\"$slots.action\" name=\"action\"></slot>\n      <VbenButton v-else-if=\"showBack\" size=\"lg\" @click=\"back\">\n        <ArrowLeft class=\"mr-2 size-4\" />\n        {{ $t('common.backToHome') }}\n      </VbenButton>\n      <VbenButton v-else-if=\"showRefresh\" size=\"lg\" @click=\"refresh\">\n        <RotateCw class=\"mr-2 size-4\" />\n        {{ $t('common.refresh') }}\n      </VbenButton>\n    </div>\n  </div>\n</template>\n"
  },
  {
    "path": "packages/effects/common-ui/src/ui/fallback/icons/icon-403.vue",
    "content": "<template>\n  <svg\n    height=\"659.29778\"\n    viewBox=\"0 0 586 659.29778\"\n    width=\"586\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n    xmlns:xlink=\"http://www.w3.org/1999/xlink\"\n  >\n    <circle cx=\"332.47856\" cy=\"254\" fill=\"#f2f2f2\" r=\"254.00001\" />\n    <path\n      d=\"M498.46363,113.58835H33.17063c-.99774-.02133-1.78931-.84746-1.76797-1.84521,.02069-.96771,.80026-1.74727,1.76797-1.76796H498.46363c.99774,.02133,1.78931,.84746,1.76794,1.84521-.02069,.96771-.80023,1.74727-1.76794,1.76796Z\"\n      fill=\"#cacaca\"\n    />\n    <rect\n      fill=\"#fff\"\n      height=\"34.98639\"\n      rx=\"17.49318\"\n      ry=\"17.49318\"\n      width=\"163.61147\"\n      x=\"193.77441\"\n      y=\"174.47256\"\n    />\n    <path\n      d=\"M128.17493,244.44534H422.98542c9.66122,0,17.49316,7.83197,17.49316,17.49319h0c0,9.66122-7.83194,17.49319-17.49316,17.49319H128.17493c-9.66122,0-17.49318-7.83197-17.49318-17.49319h0c0-9.66122,7.83196-17.49319,17.49318-17.49319Z\"\n      fill=\"#fff\"\n    />\n    <path\n      d=\"M128.17493,314.41812H422.98542c9.66122,0,17.49316,7.83197,17.49316,17.49319h0c0,9.66122-7.83194,17.49319-17.49316,17.49319H128.17493c-9.66122,0-17.49318-7.83197-17.49318-17.49319h0c0-9.66122,7.83196-17.49319,17.49318-17.49319Z\"\n      fill=\"#fff\"\n    />\n    <path\n      d=\"M91.64085,657.75932l-.69385-.06793c-23.54068-2.42871-44.82135-15.08929-58.18845-34.61835-3.66138-5.44159-6.62299-11.32251-8.815-17.50409l-.21069-.58966,.62375-.05048c7.44699-.59924,15.09732-1.86292,18.49585-2.46417l-21.91473-7.42511-.1355-.65033c-1.29926-6.10406,1.24612-12.38458,6.4285-15.86176,5.19641-3.64447,12.08731-3.76111,17.40405-.29449,2.38599,1.52399,4.88162,3.03339,7.29489,4.49359,8.29321,5.01636,16.8688,10.20337,23.29828,17.30121,9.74951,10.97778,14.02298,25.76984,11.63,40.25562l4.7829,17.47595Z\"\n      fill=\"#f2f2f2\"\n    />\n    <polygon\n      fill=\"#a0616a\"\n      points=\"171.30016 646.86102 182.10017 646.85999 187.23916 605.198 171.29716 605.19897 171.30016 646.86102\"\n    />\n    <path\n      d=\"M170.9192,658.12816l33.21436-.00122v-.41998c-.00049-7.13965-5.78833-12.92737-12.92798-12.92773h-.00079l-6.06702-4.60278-11.3197,4.60345-2.89941,.00012,.00055,13.34814Z\"\n      fill=\"#2f2e41\"\n    />\n    <polygon\n      fill=\"#a0616a\"\n      points=\"84.74116 616.94501 93.38016 623.42603 122.49316 593.185 109.74116 583.61902 84.74116 616.94501\"\n    />\n    <path\n      d=\"M77.67448,625.72966l26.569,19.93188,.25208-.336c4.2843-5.71136,3.12799-13.81433-2.58279-18.09937l-.00064-.00049-2.09079-7.32275-11.81735-3.11102-2.31931-1.73993-8.01019,10.67767Z\"\n      fill=\"#2f2e41\"\n    />\n    <path\n      d=\"M120.64463,451.35271s.59625,16.26422,1.3483,29.30737c.12335,2.13916-4.88821,4.46301-4.75842,6.7901,.08609,1.54395,1.02808,3.04486,1.1156,4.65472,.09235,1.69897-1.20822,3.20282-1.1156,4.95984,.09052,1.71667,1.57422,3.6853,1.66373,5.44244,.96317,18.9093,4.45459,41.54633,.9584,47.87439-1.72299,3.11871-23.68533,46.32446-23.68533,46.32446,0,0,12.23666,18.35498,15.73285,12.23663,4.61771-8.08099,40.20615-45.88745,40.20615-53.10712,0-7.21088,8.23346-61.25323,8.23346-61.25323l5.74103,31.98169,2.63239,6.33655-.82715,3.71997,1.70117,5.02045,.09192,4.96838,1.65619,9.22614s-4.98199,71.88159-2.17633,73.88312c2.81439,2.01038,16.44086,5.62018,18.04901,2.01038,1.59955-3.6098,12.0108-75.01947,12.0108-75.01947,0,0,1.6781-32.72424,3.49622-63.14111,.1048-1.76556,1.34607-3.89825,1.4422-5.63763,.11365-2.01898-.67297-4.64111-.56818-6.599,.11365-2.24628,1.11005-3.82831,1.20618-5.97852,.74292-16.6156-3.42761-36.84912-4.7561-38.84192-4.01202-6.01343-7.62177-10.82074-7.62177-10.82074,0,0-54.03558-17.75403-68.47485,.28625l-3.30185,25.37585Z\"\n      fill=\"#2f2e41\"\n    />\n    <path\n      d=\"M174.53779,284.10378l-21.4209-4.28418-9.9964,13.56656h0c-18.65262,18.34058-18.93359,34.52753-15.60379,60.47382v36.41553l-2.41,24.41187s-8.53156,17.84521,.26788,22.00006,66.59857,3.80066,72.117,2.14209,.73517-3.69482-.71399-11.4245c-2.72211-14.51929-.90131-7.51562-.71399-12.13849,2.68585-66.31363-3.57013-93.5379-4.20544-100.69376l-10.89398-19.75858-6.42639-10.71042Z\"\n      fill=\"#3f3d56\"\n    />\n    <path\n      d=\"M287.43909,337.57097c-2.23248,4.23007-7.47144,5.84943-11.70148,3.61694-.45099-.23804-.88013-.51541-1.28229-.82895l-46.26044,29.37308,.13336-15.9924,44.93842-26.07846c3.20093-3.58887,8.70514-3.90332,12.29401-.70239,3.00305,2.67844,3.7796,7.0657,1.87842,10.61218Z\"\n      fill=\"#a0616a\"\n    />\n    <path\n      d=\"M157.62488,302.62425l-5.26666-.55807c-4.86633-.50473-9.64093,1.57941-12.57947,5.491-1.12549,1.48346-1.9339,3.18253-2.37491,4.99164l-.00317,.01447c-1.32108,5.44534,.75095,11.15201,5.25803,14.48117l18.19031,13.41101c12.76544,17.24899,36.75653,28.69272,64.89832,37.98978l43.74274-27.16666-15.47186-18.73843-30.00336,16.0798-44.59833-34.52374-.0257-.02075-16.97424-10.936-4.79169-.5152Z\"\n      fill=\"#3f3d56\"\n    />\n    <circle cx=\"167.29993\" cy=\"248.60526\" fill=\"#a0616a\" r=\"24.9798\" />\n    <path\n      d=\"M167.8769,273.59047c-.20135,.00662-.4032,.01108-.6048,.01657-.0863,.22388-.17938,.44583-.2868,.66357l.8916-.68015Z\"\n      fill=\"#2f2e41\"\n    />\n    <path\n      d=\"M174.73243,249.29823c.03918,.24612,.09912,.48846,.17914,.72449-.03302-.24731-.09308-.49026-.17914-.72449Z\"\n      fill=\"#2f2e41\"\n    />\n    <path\n      d=\"M192.59852,224.6942c-1.0282,3.19272-1.94586-.85715-5.32825-.12869-4.06885,.87625-8.80377,.57532-12.13586-1.91879-4.96478-3.64273-11.39874-4.62335-17.22333-2.62509-5.70154,2.01706-15.25348,3.43933-16.73907,9.30179-.51642,2.03781-.7215,4.24933-1.97321,5.9382-1.09436,1.47662-2.82166,2.31854-4.26608,3.45499-4.87726,3.83743-1.14954,14.73981,1.15881,20.50046,2.30838,5.76065,7.60355,9.95721,13.42526,12.10678,5.63281,2.07977,11.7464,2.44662,17.75531,2.28317,1.04517-2.7106,.59363-5.84137-.26874-8.65134-.93359-3.04199-2.31592-5.97791-2.70593-9.13599s.46643-6.74527,3.11444-8.50986c2.4339-1.62192,6.39465-.63388,7.32062,1.98843-.54028-3.27841,2.7807-6.4509,6.20508-7.00882,3.67651-.599,7.35291,.72833,11.01886,1.38901s2.36475-14.77301,.64209-18.98425Z\"\n      fill=\"#2f2e41\"\n    />\n    <circle\n      cx=\"281.3585\"\n      cy=\"285.71051\"\n      fill=\"hsl(var(--primary))\"\n      r=\"51.12006\"\n      transform=\"translate(-26.58509 542.54478) rotate(-85.26884)\"\n    />\n    <path\n      d=\"M294.78675,264.41051l-13.42828,13.42828-13.42828-13.42828c-2.17371-2.17374-5.69806-2.17374-7.87177,0s-2.17371,5.69803,0,7.87177l13.42828,13.42828-13.42828,13.42828c-2.17169,2.17575-2.1684,5.70007,.00739,7.87177,2.17285,2.16879,5.69153,2.16879,7.86438-.00003l13.42828-13.42828,13.42828,13.42828c2.17578,2.17169,5.70007,2.1684,7.87177-.00735,2.16882-2.17288,2.16882-5.6915,0-7.86438l-13.42828-13.42828,13.42828-13.42828c2.17371-2.17374,2.17371-5.69803,0-7.87177s-5.69806-2.17374-7.87177,0h0Z\"\n      fill=\"#fff\"\n    />\n    <path\n      d=\"M261.21387,242.74385c1.5069,4.53946-.95154,9.44101-5.49097,10.94791-.48401,.16064-.9812,.27823-1.4859,.35141l-10.83051,53.71692-11.44788-11.16785,12.29266-50.48209c-.37366-4.7944,3.21008-8.98395,8.00452-9.3576,4.01166-.31265,7.71509,2.16425,8.95807,5.9913Z\"\n      fill=\"#a0616a\"\n    />\n    <path\n      d=\"M146.12519,312.22478l-4.04883,3.41412c-3.73322,3.16214-5.53476,8.05035-4.74649,12.87888,.29129,1.83917,.95773,3.59879,1.95786,5.16949l.00824,.0123c3.01477,4.72311,8.5672,7.17865,14.08978,6.23117l22.27075-3.84171c21.28461,2.72995,46.15155-6.65967,72.34302-20.53055l10.67969-50.37274-24.23297-1.80811-9.16821,32.78271-55.78815,8.28149-.03278,.00415-19.64294,4.67767-3.68896,3.1011Z\"\n      fill=\"#3f3d56\"\n    />\n    <path\n      d=\"M272.93684,658.99046l-271.75,.30731c-.65759-.00214-1.18896-.53693-1.18683-1.19452,.00211-.6546,.53223-1.18469,1.18683-1.18683l271.75-.30731c.65759,.00214,1.18896,.53693,1.18683,1.19452-.00208,.6546-.53223,1.18469-1.18683,1.18683Z\"\n      fill=\"#cacaca\"\n    />\n    <g>\n      <ellipse\n        cx=\"56.77685\"\n        cy=\"82.05834\"\n        fill=\"#3f3d56\"\n        rx=\"8.45661\"\n        ry=\"8.64507\"\n      />\n      <ellipse\n        cx=\"85.9906\"\n        cy=\"82.05834\"\n        fill=\"#3f3d56\"\n        rx=\"8.45661\"\n        ry=\"8.64507\"\n      />\n      <ellipse\n        cx=\"115.20435\"\n        cy=\"82.05834\"\n        fill=\"#3f3d56\"\n        rx=\"8.45661\"\n        ry=\"8.64507\"\n      />\n      <path\n        d=\"M148.51577,88.89113c-.25977,0-.51904-.10059-.71484-.30078l-5.70605-5.83301c-.38037-.38867-.38037-1.00977,0-1.39844l5.70605-5.83252c.38721-.39453,1.021-.40088,1.41406-.01562,.39502,.38623,.40186,1.01953,.01562,1.41406l-5.02197,5.1333,5.02197,5.13379c.38623,.39453,.37939,1.02783-.01562,1.41406-.19434,.19043-.44678,.28516-.69922,.28516Z\"\n        fill=\"#3f3d56\"\n      />\n      <path\n        d=\"M158.10415,88.89113c-.25244,0-.50488-.09473-.69922-.28516-.39502-.38623-.40186-1.01904-.01562-1.41406l5.02148-5.13379-5.02148-5.1333c-.38623-.39453-.37939-1.02783,.01562-1.41406,.39404-.38672,1.02783-.37939,1.41406,.01562l5.70557,5.83252c.38037,.38867,.38037,1.00977,0,1.39844l-5.70557,5.83301c-.1958,.2002-.45508,.30078-.71484,.30078Z\"\n        fill=\"#3f3d56\"\n      />\n      <path\n        d=\"M456.61398,74.41416h-10.60999c-1.21002,0-2.19,.97998-2.19,2.19v10.62c0,1.21002,.97998,2.19,2.19,2.19h10.60999c1.21002,0,2.20001-.97998,2.20001-2.19v-10.62c0-1.21002-.98999-2.19-2.20001-2.19Z\"\n        fill=\"#3f3d56\"\n      />\n      <path\n        d=\"M430.61398,74.41416h-10.60999c-1.21002,0-2.19,.97998-2.19,2.19v10.62c0,1.21002,.97998,2.19,2.19,2.19h10.60999c1.21002,0,2.20001-.97998,2.20001-2.19v-10.62c0-1.21002-.98999-2.19-2.20001-2.19Z\"\n        fill=\"#3f3d56\"\n      />\n      <path\n        d=\"M481.11398,74.91416h-10.60999c-1.21002,0-2.19,.97998-2.19,2.19v10.62c0,1.21002,.97998,2.19,2.19,2.19h10.60999c1.21002,0,2.20001-.97998,2.20001-2.19v-10.62c0-1.21002-.98999-2.19-2.20001-2.19Z\"\n        fill=\"#3f3d56\"\n      />\n      <path\n        d=\"M321.19229,78.95414h-84.81c-1.48004,0-2.67004,1.20001-2.67004,2.67004s1.19,2.66998,2.67004,2.66998h84.81c1.46997,0,2.66998-1.20001,2.66998-2.66998s-1.20001-2.67004-2.66998-2.67004Z\"\n        fill=\"#3f3d56\"\n      />\n    </g>\n  </svg>\n</template>\n"
  },
  {
    "path": "packages/effects/common-ui/src/ui/fallback/icons/icon-404.vue",
    "content": "<template>\n  <svg\n    height=\"571\"\n    viewBox=\"0 0 860 571\"\n    width=\"860\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n    xmlns:xlink=\"http://www.w3.org/1999/xlink\"\n  >\n    <path\n      d=\"M605.66974,324.95306c-7.66934-12.68446-16.7572-26.22768-30.98954-30.36953-16.482-4.7965-33.4132,4.73193-47.77473,14.13453a1392.15692,1392.15692,0,0,0-123.89338,91.28311l.04331.49238q46.22556-3.1878,92.451-6.37554c22.26532-1.53546,45.29557-3.2827,64.97195-13.8156,7.46652-3.99683,14.74475-9.33579,23.20555-9.70782,10.51175-.46217,19.67733,6.87923,26.8802,14.54931,42.60731,45.371,54.937,114.75409,102.73817,154.61591A1516.99453,1516.99453,0,0,0,605.66974,324.95306Z\"\n      fill=\"#f2f2f2\"\n      transform=\"translate(-169.93432 -164.42601)\"\n    />\n    <path\n      d=\"M867.57068,709.78146c-4.71167-5.94958-6.6369-7.343-11.28457-13.34761q-56.7644-73.41638-106.70791-151.79237-33.92354-53.23-64.48275-108.50439-14.54864-26.2781-28.29961-52.96872-10.67044-20.6952-20.8646-41.63793c-1.94358-3.98782-3.8321-7.99393-5.71122-12.00922-4.42788-9.44232-8.77341-18.93047-13.43943-28.24449-5.31686-10.61572-11.789-21.74485-21.55259-28.877a29.40493,29.40493,0,0,0-15.31855-5.89458c-7.948-.51336-15.28184,2.76855-22.17568,6.35295-50.43859,26.301-97.65922,59.27589-140.3696,96.79771A730.77816,730.77816,0,0,0,303.32241,496.24719c-1.008,1.43927-3.39164.06417-2.37419-1.38422q6.00933-8.49818,12.25681-16.81288A734.817,734.817,0,0,1,500.80465,303.06436q18.24824-11.82581,37.18269-22.54245c6.36206-3.60275,12.75188-7.15967,19.25136-10.49653,6.37146-3.27274,13.13683-6.21547,20.41563-6.32547,24.7701-.385,37.59539,27.66695,46.40506,46.54248q4.15283,8.9106,8.40636,17.76626,16.0748,33.62106,33.38729,66.628,10.68453,20.379,21.83683,40.51955,34.7071,62.71816,73.77854,122.897c34.5059,53.1429,68.73651,100.08874,108.04585,149.78472C870.59617,709.21309,868.662,711.17491,867.57068,709.78146Z\"\n      fill=\"#e4e4e4\"\n      transform=\"translate(-169.93432 -164.42601)\"\n    />\n    <path\n      d=\"M414.91613,355.804c-1.43911-1.60428-2.86927-3.20856-4.31777-4.81284-11.42244-12.63259-23.6788-25.11847-39.3644-32.36067a57.11025,57.11025,0,0,0-23.92679-5.54622c-8.56213.02753-16.93178,2.27348-24.84306,5.41792-3.74034,1.49427-7.39831,3.1902-11.00078,4.99614-4.11634,2.07182-8.15927,4.28118-12.1834,6.50883q-11.33112,6.27044-22.36816,13.09089-21.9606,13.57221-42.54566,29.21623-10.67111,8.11311-20.90174,16.75788-9.51557,8.03054-18.64618,16.492c-1.30169,1.20091-3.24527-.74255-1.94358-1.94347,1.60428-1.49428,3.22691-2.97938,4.84955-4.44613q6.87547-6.21546,13.9712-12.19257,12.93921-10.91827,26.54851-20.99312,21.16293-15.67614,43.78288-29.22541,11.30361-6.76545,22.91829-12.96259c2.33794-1.24675,4.70318-2.466,7.09572-3.6211a113.11578,113.11578,0,0,1,16.86777-6.86632,60.0063,60.0063,0,0,1,25.476-2.50265,66.32706,66.32706,0,0,1,23.50512,8.1314c15.40091,8.60812,27.34573,21.919,38.97,34.90915C418.03337,355.17141,416.09875,357.12405,414.91613,355.804Z\"\n      fill=\"#e4e4e4\"\n      transform=\"translate(-169.93432 -164.42601)\"\n    />\n    <path\n      d=\"M730.47659,486.71092l36.90462-13.498,18.32327-6.70183c5.96758-2.18267,11.92082-4.66747,18.08988-6.23036a28.53871,28.53871,0,0,1,16.37356.20862,37.73753,37.73753,0,0,1,12.771,7.91666,103.63965,103.63965,0,0,1,10.47487,11.18643c3.98932,4.79426,7.91971,9.63877,11.86772,14.46706q24.44136,29.89094,48.56307,60.04134,24.12117,30.14991,47.91981,60.556,23.85681,30.48041,47.38548,61.21573,2.88229,3.76518,5.75966,7.53415c1.0598,1.38809,3.44949.01962,2.37472-1.38808Q983.582,650.9742,959.54931,620.184q-24.09177-30.86383-48.51647-61.46586-24.42421-30.60141-49.17853-60.93743-6.16706-7.55761-12.35445-15.09858c-3.47953-4.24073-6.91983-8.52718-10.73628-12.47427-7.00539-7.24516-15.75772-13.64794-26.23437-13.82166-6.15972-.10214-12.121,1.85248-17.844,3.92287-6.16968,2.232-12.32455,4.50571-18.48633,6.75941l-37.16269,13.59243-9.29067,3.3981c-1.64875.603-.93651,3.2619.73111,2.652Z\"\n      fill=\"#e4e4e4\"\n      transform=\"translate(-169.93432 -164.42601)\"\n    />\n    <path\n      d=\"M366.37741,334.52609c-18.75411-9.63866-42.77137-7.75087-60.00508,4.29119a855.84708,855.84708,0,0,1,97.37056,22.72581C390.4603,353.75916,380.07013,341.5635,366.37741,334.52609Z\"\n      fill=\"#f2f2f2\"\n      transform=\"translate(-169.93432 -164.42601)\"\n    />\n    <path\n      d=\"M306.18775,338.7841l-3.61042,2.93462c1.22123-1.02713,2.4908-1.99013,3.795-2.90144C306.31073,338.80665,306.24935,338.79473,306.18775,338.7841Z\"\n      fill=\"#f2f2f2\"\n      transform=\"translate(-169.93432 -164.42601)\"\n    />\n    <path\n      d=\"M831.54929,486.84576c-3.6328-4.42207-7.56046-9.05222-12.99421-10.84836l-5.07308.20008A575.436,575.436,0,0,0,966.74929,651.418Q899.14929,569.13192,831.54929,486.84576Z\"\n      fill=\"#f2f2f2\"\n      transform=\"translate(-169.93432 -164.42601)\"\n    />\n    <path\n      d=\"M516.08388,450.36652A37.4811,37.4811,0,0,0,531.015,471.32518c2.82017,1.92011,6.15681,3.76209,7.12158,7.03463a8.37858,8.37858,0,0,1-.87362,6.1499,24.88351,24.88351,0,0,1-3.86126,5.04137l-.13667.512c-6.99843-4.14731-13.65641-9.3934-17.52227-16.55115s-4.40553-16.53895.34116-23.14544\"\n      fill=\"#f2f2f2\"\n      transform=\"translate(-169.93432 -164.42601)\"\n    />\n    <path\n      d=\"M749.08388,653.36652A37.4811,37.4811,0,0,0,764.015,674.32518c2.82017,1.92011,6.15681,3.76209,7.12158,7.03463a8.37858,8.37858,0,0,1-.87362,6.1499,24.88351,24.88351,0,0,1-3.86126,5.04137l-.13667.512c-6.99843-4.14731-13.65641-9.3934-17.52227-16.55115s-4.40553-16.53895.34116-23.14544\"\n      fill=\"#f2f2f2\"\n      transform=\"translate(-169.93432 -164.42601)\"\n    />\n    <path\n      d=\"M284.08388,639.36652A37.4811,37.4811,0,0,0,299.015,660.32518c2.82017,1.92011,6.15681,3.76209,7.12158,7.03463a8.37858,8.37858,0,0,1-.87362,6.1499,24.88351,24.88351,0,0,1-3.86126,5.04137l-.13667.512c-6.99843-4.14731-13.65641-9.3934-17.52227-16.55115s-4.40553-16.53895.34116-23.14544\"\n      fill=\"#f2f2f2\"\n      transform=\"translate(-169.93432 -164.42601)\"\n    />\n    <circle cx=\"649.24878\" cy=\"51\" fill=\"hsl(var(--primary))\" r=\"51\" />\n    <path\n      d=\"M911.21851,176.29639c-24.7168-3.34094-52.93512,10.01868-59.34131,34.12353a21.59653,21.59653,0,0,0-41.09351,2.10871l2.82972,2.02667a372.27461,372.27461,0,0,0,160.65881-.72638C957.07935,195.76,935.93537,179.63727,911.21851,176.29639Z\"\n      fill=\"#f0f0f0\"\n      transform=\"translate(-169.93432 -164.42601)\"\n    />\n    <path\n      d=\"M805.21851,244.29639c-24.7168-3.34094-52.93512,10.01868-59.34131,34.12353a21.59653,21.59653,0,0,0-41.09351,2.10871l2.82972,2.02667a372.27461,372.27461,0,0,0,160.65881-.72638C851.07935,263.76,829.93537,247.63727,805.21851,244.29639Z\"\n      fill=\"#f0f0f0\"\n      transform=\"translate(-169.93432 -164.42601)\"\n    />\n    <path\n      d=\"M1020.94552,257.15423a.98189.98189,0,0,1-.30176-.04688C756.237,173.48919,523.19942,184.42376,374.26388,208.32122c-20.26856,3.251-40.59131,7.00586-60.40381,11.16113-5.05811,1.05957-10.30567,2.19532-15.59668,3.37793-6.31885,1.40723-12.55371,2.85645-18.53223,4.30567q-3.873.917-7.59472,1.84863c-3.75831.92773-7.57178,1.89453-11.65967,2.957-4.56787,1.17774-9.209,2.41309-13.79737,3.67188a.44239.44239,0,0,1-.05127.01465l.00049.001c-5.18261,1.415-10.33789,2.8711-15.32324,4.3252-2.69824.77929-5.30371,1.54785-7.79932,2.30664-.2788.07715-.52587.15136-.77636.22754l-.53614.16308c-.31054.09473-.61718.1875-.92382.27539l-.01953.00586.00048.001-.81152.252c-.96777.293-1.91211.5791-2.84082.86426-24.54492,7.56641-38.03809,12.94922-38.17139,13.00195a1,1,0,1,1-.74414-1.85644c.13428-.05274,13.69336-5.46289,38.32764-13.05762.93213-.28613,1.87891-.57226,2.84961-.86621l.7539-.23438c.02588-.00976.05176-.01757.07813-.02539.30518-.08691.60986-.17968.91943-.27343l.53711-.16309c.26758-.08105.53125-.16113.80127-.23535,2.47852-.75391,5.09278-1.52441,7.79785-2.30664,4.98731-1.45508,10.14746-2.91113,15.334-4.32813.01611-.00586.03271-.00976.04883-.01464v-.001c4.60449-1.2627,9.26269-2.50293,13.84521-3.68457,4.09424-1.06348,7.915-2.03223,11.67969-2.96192q3.73755-.93017,7.60937-1.85253c5.98536-1.45118,12.23291-2.90235,18.563-4.3125,5.29932-1.1836,10.55567-2.32227,15.62207-3.38282,19.84326-4.16211,40.19776-7.92285,60.49707-11.17871C523.09591,182.415,756.46749,171.46282,1021.2463,255.2011a.99974.99974,0,0,1-.30078,1.95313Z\"\n      fill=\"#ccc\"\n      transform=\"translate(-169.93432 -164.42601)\"\n    />\n    <path\n      d=\"M432.92309,584.266a6.72948,6.72948,0,0,0-1.7-2.67,6.42983,6.42983,0,0,0-.92-.71c-2.61-1.74-6.51-2.13-8.99,0a5.81012,5.81012,0,0,0-.69.71q-1.11,1.365-2.28,2.67c-1.28,1.46-2.59,2.87-3.96,4.24-.39.38-.78.77-1.18,1.15-.23.23-.46.45-.69.67-.88.84-1.78,1.65-2.69,2.45-.48.43-.96.85-1.45,1.26-.73.61-1.46,1.22-2.2,1.81-.07.05-.14.1-.21.16-.02.01-.03.03-.05.04-.01,0-.02,0-.03.02a.17861.17861,0,0,0-.07.05c-.22.15-.37.25-.48.34.04-.01995.08-.05.12-.07-.18.14-.37.28-.55.42-1.75,1.29-3.54,2.53-5.37,3.69a99.21022,99.21022,0,0,1-14.22,7.55c-.33.13-.67.27-1.01.4a85.96993,85.96993,0,0,1-40.85,6.02q-2.13008-.165-4.26-.45c-1.64-.24-3.27-.53-4.89-.86a97.93186,97.93186,0,0,1-18.02-5.44,118.65185,118.65185,0,0,1-20.66-12.12c-1-.71-2.01-1.42-3.02-2.11,1.15-2.82,2.28-5.64,3.38-8.48.55-1.37,1.08-2.74,1.6-4.12,4.09-10.63,7.93-21.36,11.61-32.13q5.58-16.365,10.53-32.92.51-1.68.99-3.36,2.595-8.745,4.98-17.53c.15-.56994.31-1.12994.45-1.7q.68994-2.52,1.35-5.04c1-3.79-1.26-8.32-5.24-9.23a7.63441,7.63441,0,0,0-9.22,5.24c-.43,1.62-.86,3.23-1.3,4.85q-3.165,11.74494-6.66,23.41-.51,1.68-1.02,3.36-7.71,25.41-16.93,50.31-1.11,3.015-2.25,6.01c-.37.98-.74,1.96-1.12,2.94-.73,1.93-1.48,3.86-2.23,5.79-.43006,1.13-.87006,2.26-1.31,3.38-.29.71-.57,1.42-.85,2.12a41.80941,41.80941,0,0,0-8.81-2.12l-.48-.06a27.397,27.397,0,0,0-7.01.06,23.91419,23.91419,0,0,0-17.24,10.66c-4.77,7.51-4.71,18.25,1.98,24.63,6.89,6.57,17.32,6.52,25.43,2.41a28.35124,28.35124,0,0,0,10.52-9.86,50.56939,50.56939,0,0,0,2.74-4.65c.21.14.42.28.63.43.8.56,1.6,1.13,2.39,1.69a111.73777,111.73777,0,0,0,14.51,8.91,108.35887,108.35887,0,0,0,34.62,10.47c.27.03.53.07.8.1,1.33.17,2.67.3,4.01.41a103.78229,103.78229,0,0,0,55.58-11.36q2.175-1.125,4.31-2.36,3.315-1.92,6.48-4.08c1.15-.78,2.27-1.57,3.38-2.4a101.04244,101.04244,0,0,0,13.51-11.95q2.35491-2.475,4.51-5.11005a8.0612,8.0612,0,0,0,2.2-5.3A7.5644,7.5644,0,0,0,432.92309,584.266Zm-165.59,23.82c.21-.15.42-.31.62-.47C267.89312,607.766,267.60308,607.936,267.33312,608.086Zm3.21-3.23c-.23.26-.44.52-.67.78a23.36609,23.36609,0,0,1-2.25,2.2c-.11.1-.23.2-.35.29a.00976.00976,0,0,0-.01.01,3.80417,3.80417,0,0,0-.42005.22q-.645.39-1.31994.72a17.00459,17.00459,0,0,1-2.71.75,16.79925,16.79925,0,0,1-2.13.02h-.02a14.82252,14.82252,0,0,1-1.45-.4c-.24-.12-.47-.25994-.7-.4-.09-.08-.17005-.16-.22-.21a2.44015,2.44015,0,0,1-.26995-.29.0098.0098,0,0,0-.01-.01c-.11005-.2-.23005-.4-.34-.6a.031.031,0,0,1-.01-.02c-.08-.25-.15-.51-.21-.77a12.51066,12.51066,0,0,1,.01-1.37,13.4675,13.4675,0,0,1,.54-1.88,11.06776,11.06776,0,0,1,.69-1.26c.02-.04.12-.2.23-.38.01-.01.01-.01.01-.02.15-.17.3-.35.46-.51.27-.3.56-.56.85-.83a18.02212,18.02212,0,0,1,1.75-1.01,19.48061,19.48061,0,0,1,2.93-.79,24.98945,24.98945,0,0,1,4.41.04,30.30134,30.30134,0,0,1,4.1,1.01,36.94452,36.94452,0,0,1-2.77,4.54C270.6231,604.746,270.58312,604.806,270.54308,604.856Zm-11.12-3.29a2.18029,2.18029,0,0,1-.31.38995A1.40868,1.40868,0,0,1,259.42309,601.566Z\"\n      fill=\"hsl(var(--foreground))\"\n      transform=\"translate(-169.93432 -164.42601)\"\n    />\n    <path\n      d=\"M402.86309,482.136q-.13494,4.71-.27,9.42-.285,10.455-.59,20.92-.315,11.775-.66,23.54-.165,6.07507-.34,12.15-.465,16.365-.92,32.72c-.03,1.13-.07,2.25-.1,3.38q-.225,8.11506-.45,16.23-.255,8.805-.5,17.61-.18,6.59994-.37,13.21-1.34994,47.895-2.7,95.79a7.64844,7.64844,0,0,1-7.5,7.5,7.56114,7.56114,0,0,1-7.5-7.5q.75-26.94,1.52-53.88.675-24.36,1.37-48.72.225-8.025.45-16.06.345-12.09.68-24.18c.03-1.13.07-2.25.1-3.38.02-.99.05-1.97.08-2.96q.66-23.475,1.32-46.96.27-9.24.52-18.49.3-10.545.6-21.08c.09-3.09.17005-6.17.26-9.26a7.64844,7.64844,0,0,1,7.5-7.5A7.56116,7.56116,0,0,1,402.86309,482.136Z\"\n      fill=\"hsl(var(--foreground))\"\n      transform=\"translate(-169.93432 -164.42601)\"\n    />\n    <path\n      d=\"M814.29118,484.2172a893.23753,893.23753,0,0,1-28.16112,87.94127c-3.007,7.94641-6.08319,15.877-9.3715,23.71185l.75606-1.7916a54.58274,54.58274,0,0,1-5.58953,10.61184q-.22935.32119-.46685.63642,1.16559-1.49043.4428-.589c-.25405.30065-.5049.60219-.7676.89546a23.66436,23.66436,0,0,1-2.2489,2.20318q-.30139.25767-.61188.5043l.93783-.729c-.10884.25668-.87275.59747-1.11067.74287a18.25362,18.25362,0,0,1-2.40479,1.21853l1.7916-.75606a19.0859,19.0859,0,0,1-4.23122,1.16069l1.9938-.26791a17.02055,17.02055,0,0,1-4.29785.046l1.99379.2679a14.0022,14.0022,0,0,1-3.40493-.917l1.79159.75606a12.01175,12.01175,0,0,1-1.67882-.89614c-.27135-.17688-1.10526-.80852-.01487.02461,1.13336.86595.14562.07434-.08763-.15584-.19427-.19171-.36962-.4-.55974-.595-.88208-.90454.99637,1.55662.39689.49858a18.18179,18.18179,0,0,1-.87827-1.63672l.75606,1.7916a11.92493,11.92493,0,0,1-.728-2.65143l.26791,1.9938a13.65147,13.65147,0,0,1-.00316-3.40491l-.2679,1.9938a15.96371,15.96371,0,0,1,.99486-3.68011l-.75606,1.7916a16.72914,16.72914,0,0,1,1.17794-2.29848,6.72934,6.72934,0,0,1,.72851-1.0714c.04915.01594-1.26865,1.51278-.56937.757.1829-.19767.354-.40592.539-.602.29617-.31382.61354-.60082.92561-.89791,1.04458-.99442-1.46188.966-.25652.17907a19.0489,19.0489,0,0,1,2.74925-1.49923l-1.79159.75606a20.31136,20.31136,0,0,1,4.99523-1.33984l-1.9938.2679a25.62828,25.62828,0,0,1,6.46062.07647l-1.9938-.2679a33.21056,33.21056,0,0,1,7.89178,2.2199l-1.7916-.75606c5.38965,2.31383,10.16308,5.74926,14.928,9.118a111.94962,111.94962,0,0,0,14.50615,8.9065,108.38849,108.38849,0,0,0,34.62226,10.47371,103.93268,103.93268,0,0,0,92.58557-36.75192,8.07773,8.07773,0,0,0,2.1967-5.3033,7.63232,7.63232,0,0,0-2.1967-5.3033c-2.75154-2.52586-7.94926-3.239-10.6066,0a95.63575,95.63575,0,0,1-8.10664,8.72692q-2.01736,1.914-4.14232,3.70983-1.21364,1.02588-2.46086,2.01121c-.3934.31081-1.61863,1.13807.26309-.19744-.43135.30614-.845.64036-1.27058.95478a99.26881,99.26881,0,0,1-20.33215,11.56478l1.79159-.75606a96.8364,96.8364,0,0,1-24.17119,6.62249l1.99379-.2679a97.64308,97.64308,0,0,1-25.75362-.03807l1.99379.2679a99.79982,99.79982,0,0,1-24.857-6.77027l1.7916.75607a116.02515,116.02515,0,0,1-21.7364-12.59112,86.87725,86.87725,0,0,0-11.113-6.99417,42.8238,42.8238,0,0,0-14.43784-4.38851c-9.43884-1.11076-19.0571,2.56562-24.24624,10.72035-4.77557,7.50482-4.71394,18.24362,1.97369,24.62519,6.8877,6.5725,17.31846,6.51693,25.43556,2.40567,7.81741-3.95946,12.51288-12.18539,15.815-19.94186,7.43109-17.45514,14.01023-35.31364,20.1399-53.263q9.09651-26.63712,16.49855-53.81332.91661-3.36581,1.80683-6.73869c1.001-3.78869-1.26094-8.32-5.23829-9.22589a7.63317,7.63317,0,0,0-9.22589,5.23829Z\"\n      fill=\"hsl(var(--foreground))\"\n      transform=\"translate(-169.93432 -164.42601)\"\n    />\n    <path\n      d=\"M889.12382,482.13557l-2.69954,95.79311-2.68548,95.29418-1.5185,53.88362a7.56465,7.56465,0,0,0,7.5,7.5,7.64923,7.64923,0,0,0,7.5-7.5l2.69955-95.79311,2.68548-95.29418,1.51849-53.88362a7.56465,7.56465,0,0,0-7.5-7.5,7.64923,7.64923,0,0,0-7.5,7.5Z\"\n      fill=\"hsl(var(--foreground))\"\n      transform=\"translate(-169.93432 -164.42601)\"\n    />\n    <path\n      d=\"M629.52566,700.36106h2.32885V594.31942h54.32863v-2.32291H631.85451V547.25214H673.8102q-.92256-1.17339-1.89893-2.31694H631.85451V515.38231c-.7703-.32846-1.54659-.64493-2.32885-.9435V544.9352h-45.652V507.07c-.78227.03583-1.55258.08959-2.3289.15527v37.71h-36.4201V516.68409c-.78227.34636-1.55258.71061-2.31694,1.0928V544.9352h-30.6158v2.31694h30.6158v44.74437h-30.6158v2.32291h30.6158V700.36106h2.31694V594.31942a36.41283,36.41283,0,0,1,36.4201,36.42007v69.62157h2.3289V594.31942h45.652Zm-84.401-108.36455V547.25214h36.4201v44.74437Zm38.749,0V547.25214h.91362a44.74135,44.74135,0,0,1,44.73842,44.74437Z\"\n      opacity=\"0.2\"\n      transform=\"translate(-169.93432 -164.42601)\"\n    />\n    <path\n      d=\"M615.30309,668.566a63.05854,63.05854,0,0,1-20.05,33.7c-.74.64-1.48,1.26-2.25,1.87q-2.805.25506-5.57.52c-1.53.14-3.04.29-4.54.43l-.27.03-.19-1.64-.76-6.64a37.623,37.623,0,0,1-3.3-32.44c2.64-7.12,7.42-13.41,12.12-19.65,6.49-8.62,12.8-17.14,13.03-27.65a60.54415,60.54415,0,0,1,7.9,13.33,16.432,16.432,0,0,0-5.12,3.76995c-.41.45-.82,1.08-.54,1.62006.24.46.84.57,1.36.62994,1.25.13,2.51.26,3.76.39,1,.11,2,.21,3,.32a63.99025,63.99025,0,0,1,2.45,12.18A61.18851,61.18851,0,0,1,615.30309,668.566Z\"\n      fill=\"hsl(var(--foreground))\"\n      transform=\"translate(-169.93432 -164.42601)\"\n    />\n    <path\n      d=\"M648.50311,642.356c-5.9,4.29-9.35,10.46-12.03,17.26a16.62776,16.62776,0,0,0-7.17,4.58c-.41.45-.82,1.08-.54,1.62006.24.46.84.57,1.36.62994,1.25.13,2.51.26,3.76.39-2.68,8.04-5.14,16.36-9.88,23.15a36.98942,36.98942,0,0,1-12.03,10.91,38.49166,38.49166,0,0,1-4.02,1.99q-7.62.585-14.95,1.25-2.805.25506-5.57.52c-1.53.14-3.04.29-4.54.43q-.015-.825,0-1.65a63.30382,63.30382,0,0,1,15.25-39.86c.45-.52.91-1.03,1.38-1.54a61.7925,61.7925,0,0,1,16.81-12.7A62.65425,62.65425,0,0,1,648.50311,642.356Z\"\n      fill=\"hsl(var(--primary))\"\n      transform=\"translate(-169.93432 -164.42601)\"\n    />\n    <path\n      d=\"M589.16308,699.526l-1.15,3.4-.58,1.73c-1.53.14-3.04.29-4.54.43l-.27.03c-1.66.17-3.31.34-4.96.51-.43-.5-.86-1.01-1.28-1.53a62.03045,62.03045,0,0,1,8.07-87.11c-1.32,6.91.22,13.53,2.75,20.1-.27.11-.53.22-.78.34a16.432,16.432,0,0,0-5.12,3.76995c-.41.45-.82,1.08-.54,1.62006.24.46.84.57,1.36.62994,1.25.13,2.51.26,3.76.39,1,.11,2,.21,3,.32q.705.075,1.41.15c.07.15.13.29.2.44,2.85,6.18,5.92,12.39,7.65,18.83a43.66591,43.66591,0,0,1,1.02,4.91A37.604,37.604,0,0,1,589.16308,699.526Z\"\n      fill=\"hsl(var(--primary))\"\n      transform=\"translate(-169.93432 -164.42601)\"\n    />\n    <path\n      d=\"M689.82123,554.48655c-8.60876-16.79219-21.94605-30.92088-37.63219-41.30357a114.2374,114.2374,0,0,0-52.5626-18.37992q-3.69043-.33535-7.399-.39281c-2.92141-.04371-46.866,12.63176-61.58712,22.98214a114.29462,114.29462,0,0,0-35.333,39.527,102.49972,102.49972,0,0,0-12.12557,51.6334,113.56387,113.56387,0,0,0,14.70268,51.47577,110.47507,110.47507,0,0,0,36.44425,38.74592C549.66655,708.561,565.07375,734.51,583.1831,735.426c18.24576.923,39.05418-23.55495,55.6951-30.98707a104.42533,104.42533,0,0,0,41.72554-34.005,110.24964,110.24964,0,0,0,19.599-48.94777c2.57368-18.08313,1.37415-36.73271-4.80123-54.01627a111.85969,111.85969,0,0,0-5.58024-12.9833c-1.77961-3.50519-6.996-4.7959-10.26142-2.69063a7.67979,7.67979,0,0,0-2.69064,10.26142q1.56766,3.08773,2.91536,6.27758l-.75606-1.7916a101.15088,101.15088,0,0,1,6.87641,25.53816l-.26791-1.99379a109.2286,109.2286,0,0,1-.06613,28.68252l.26791-1.9938a109.73379,109.73379,0,0,1-7.55462,27.67419l.75606-1.79159a104.212,104.212,0,0,1-6.67151,13.09835q-1.92308,3.18563-4.08062,6.22159c-.63172.8881-1.28287,1.761-1.939,2.63114-.85625,1.13555,1.16691-1.48321.28228-.36941-.15068.18972-.30049.3801-.45182.5693q-.68121.85165-1.3818,1.68765a93.61337,93.61337,0,0,1-10.17647,10.38359q-1.36615,1.19232-2.77786,2.33115c-.46871.37832-.932.77269-1.42079,1.12472.01861-.0134,1.57956-1.19945.65556-.511-.2905.21644-.57851.43619-.86961.65184q-2.90994,2.1558-5.97433,4.092a103.48509,103.48509,0,0,1-14.75565,7.7131l1.7916-.75606a109.21493,109.21493,0,0,1-27.59663,7.55154l1.9938-.26791a108.15361,108.15361,0,0,1-28.58907.0506l1.99379.2679a99.835,99.835,0,0,1-25.09531-6.78448l1.79159.75607a93.64314,93.64314,0,0,1-13.41605-6.99094q-3.17437-2-6.18358-4.24743c-.2862-.21359-.56992-.43038-.855-.64549-.9155-.69088.65765.50965.67021.51787a19.16864,19.16864,0,0,1-1.535-1.22469q-1.45353-1.18358-2.86136-2.4218a101.98931,101.98931,0,0,1-10.49319-10.70945q-1.21308-1.43379-2.37407-2.91054c-.33524-.4263-.9465-1.29026.40424.5289-.17775-.23939-.36206-.47414-.54159-.71223q-.64657-.85751-1.27568-1.72793-2.203-3.048-4.18787-6.24586a109.29037,109.29037,0,0,1-7.8054-15.10831l.75606,1.7916a106.58753,106.58753,0,0,1-7.34039-26.837l.26791,1.9938a97.86589,97.86589,0,0,1-.04843-25.63587l-.2679,1.9938A94.673,94.673,0,0,1,505.27587,570.55l-.75606,1.7916a101.55725,101.55725,0,0,1,7.19519-13.85624q2.0655-3.32328,4.37767-6.4847.52528-.71832,1.06244-1.42786c.324-.4279,1.215-1.49333-.30537.38842.14906-.18449.29252-.37428.43942-.56041q1.26882-1.60756,2.59959-3.1649A107.40164,107.40164,0,0,1,530.772,536.21508q1.47408-1.29171,2.99464-2.52906.6909-.56218,1.39108-1.11284c.18664-.14673.37574-.29073.56152-.43858-1.99743,1.58953-.555.43261-.10157.09288q3.13393-2.34833,6.43534-4.46134a103.64393,103.64393,0,0,1,15.38655-8.10791l-1.7916.75606c7.76008-3.25839,42.14086-10.9492,48.394-10.10973l-1.99379-.26791A106.22471,106.22471,0,0,1,628.768,517.419l-1.7916-.75606a110.31334,110.31334,0,0,1,12.6002,6.32922q3.04344,1.78405,5.96742,3.76252,1.38351.93658,2.73809,1.915.677.48917,1.34626.98885c.24789.185.49386.37253.74135.558,1.03924.779-1.43148-1.1281-.34209-.26655a110.84261,110.84261,0,0,1,10.36783,9.2532q2.401,2.445,4.63686,5.04515,1.14659,1.33419,2.24643,2.70757c.36436.45495,1.60506,2.101.08448.08457.37165.49285.74744.98239,1.11436,1.47884a97.97718,97.97718,0,0,1,8.39161,13.53807c1.79317,3.49775,6.98675,4.80186,10.26142,2.69064A7.67666,7.67666,0,0,0,689.82123,554.48655Z\"\n      fill=\"hsl(var(--foreground))\"\n      transform=\"translate(-169.93432 -164.42601)\"\n    />\n    <path\n      d=\"M602.43116,676.88167a3.77983,3.77983,0,0,1-2.73939-6.55137c.09531-.37882.16368-.65085.259-1.02968q-.05115-.12366-.1029-.24717c-3.47987-8.29769-25.685,14.83336-26.645,22.63179a30.029,30.029,0,0,0,.52714,10.32752A120.39223,120.39223,0,0,1,562.77838,652.01a116.20247,116.20247,0,0,1,.72078-12.96332q.59712-5.293,1.65679-10.51055a121.78667,121.78667,0,0,1,24.1515-51.61646c6.87378.38364,12.898-.66348,13.47967-13.98532.10346-2.36972,1.86113-4.42156,2.24841-6.756-.65621.08607-1.32321.13985-1.97941.18285-.20444.0107-.41958.02149-.624.03228l-.07709.00346a3.745,3.745,0,0,1-3.07566-6.10115q.425-.52305.85054-1.04557c.43036-.53793.87143-1.06507,1.30171-1.60292a1.865,1.865,0,0,0,.13986-.16144c.49494-.61322.98971-1.21564,1.48465-1.82885a10.82911,10.82911,0,0,0-3.55014-3.43169c-4.95941-2.90463-11.80146-.89293-15.38389,3.59313-3.59313,4.486-4.27083,10.77947-3.023,16.3843a43.39764,43.39764,0,0,0,6.003,13.3828c-.269.34429-.54872.67779-.81765,1.02209a122.57366,122.57366,0,0,0-12.79359,20.2681c1.0163-7.93863-11.41159-36.60795-16.21776-42.68052-5.773-7.29409-17.61108-4.11077-18.62815,5.13562q-.01476.13428-.02884.26849,1.07082.60411,2.0964,1.28237a5.12707,5.12707,0,0,1-2.06713,9.33031l-.10452.01613c-9.55573,13.64367,21.07745,49.1547,28.74518,41.18139a125.11045,125.11045,0,0,0-6.73449,31.69282,118.66429,118.66429,0,0,0,.08607,19.15986l-.03231-.22593C558.90163,648.154,529.674,627.51374,521.139,629.233c-4.91675.99041-9.75952.76525-9.01293,5.72484q.01788.11874.03635.2375a34.4418,34.4418,0,0,1,3.862,1.86105q1.07082.60423,2.09639,1.28237a5.12712,5.12712,0,0,1-2.06712,9.33039l-.10464.01606c-.07528.01079-.13987.02157-.21507.03237-4.34967,14.96631,27.90735,39.12,47.5177,31.43461h.01081a125.07484,125.07484,0,0,0,8.402,24.52806H601.679c.10765-.3335.20443-.67779.3013-1.01129a34.102,34.102,0,0,1-8.30521-.49477c2.22693-2.73257,4.45377-5.48664,6.6807-8.21913a1.86122,1.86122,0,0,0,.13986-.16135c1.12956-1.39849,2.26992-2.78627,3.39948-4.18476l.00061-.00173a49.95232,49.95232,0,0,0-1.46367-12.72495Zm-34.37066-67.613.0158-.02133-.0158.04282Zm-6.64832,59.93237-.25822-.58084c.01079-.41957.01079-.83914,0-1.26942,0-.11845-.0215-.23672-.0215-.35508.09678.74228.18285,1.48464.29042,2.22692Z\"\n      fill=\"hsl(var(--foreground))\"\n      transform=\"translate(-169.93432 -164.42601)\"\n    />\n    <circle cx=\"95.24878\" cy=\"439\" fill=\"hsl(var(--foreground))\" r=\"11\" />\n    <circle cx=\"227.24878\" cy=\"559\" fill=\"hsl(var(--foreground))\" r=\"11\" />\n    <circle cx=\"728.24878\" cy=\"559\" fill=\"hsl(var(--foreground))\" r=\"11\" />\n    <circle cx=\"755.24878\" cy=\"419\" fill=\"hsl(var(--foreground))\" r=\"11\" />\n    <circle cx=\"723.24878\" cy=\"317\" fill=\"hsl(var(--foreground))\" r=\"11\" />\n    <path\n      d=\"M434.1831,583.426a10.949,10.949,0,1,1-.21-2.16A10.9921,10.9921,0,0,1,434.1831,583.426Z\"\n      fill=\"hsl(var(--foreground))\"\n      transform=\"translate(-169.93432 -164.42601)\"\n    />\n    <circle cx=\"484.24878\" cy=\"349\" fill=\"hsl(var(--foreground))\" r=\"11\" />\n    <path\n      d=\"M545.1831,513.426a10.949,10.949,0,1,1-.21-2.16A10.9921,10.9921,0,0,1,545.1831,513.426Z\"\n      fill=\"hsl(var(--foreground))\"\n      transform=\"translate(-169.93432 -164.42601)\"\n    />\n    <path\n      d=\"M403.1831,481.426a10.949,10.949,0,1,1-.21-2.16A10.9921,10.9921,0,0,1,403.1831,481.426Z\"\n      fill=\"hsl(var(--foreground))\"\n      transform=\"translate(-169.93432 -164.42601)\"\n    />\n    <circle cx=\"599.24878\" cy=\"443\" fill=\"hsl(var(--foreground))\" r=\"11\" />\n    <circle cx=\"426.24878\" cy=\"338\" fill=\"hsl(var(--foreground))\" r=\"16\" />\n    <path\n      d=\"M1028.875,735.26666l-857.75.30733a1.19068,1.19068,0,1,1,0-2.38136l857.75-.30734a1.19069,1.19069,0,0,1,0,2.38137Z\"\n      fill=\"#cacaca\"\n      transform=\"translate(-169.93432 -164.42601)\"\n    />\n  </svg>\n</template>\n"
  },
  {
    "path": "packages/effects/common-ui/src/ui/fallback/icons/icon-500.vue",
    "content": "<template>\n  <svg\n    height=\"699\"\n    viewBox=\"0 0 1119 699\"\n    width=\"1119\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n    xmlns:xlink=\"http://www.w3.org/1999/xlink\"\n  >\n    <title>server down</title>\n    <circle cx=\"292.60911\" cy=\"213\" fill=\"#f2f2f2\" r=\"213\" />\n    <path\n      d=\"M31.39089,151.64237c0,77.49789,48.6181,140.20819,108.70073,140.20819\"\n      fill=\"#2f2e41\"\n      transform=\"translate(-31.39089 -100.5)\"\n    />\n    <path\n      d=\"M140.09162,291.85056c0-78.36865,54.255-141.78356,121.30372-141.78356\"\n      fill=\"hsl(var(--primary))\"\n      transform=\"translate(-31.39089 -100.5)\"\n    />\n    <path\n      d=\"M70.77521,158.66768c0,73.61476,31.00285,133.18288,69.31641,133.18288\"\n      fill=\"hsl(var(--primary))\"\n      transform=\"translate(-31.39089 -100.5)\"\n    />\n    <path\n      d=\"M140.09162,291.85056c0-100.13772,62.7103-181.16788,140.20819-181.16788\"\n      fill=\"#2f2e41\"\n      transform=\"translate(-31.39089 -100.5)\"\n    />\n    <path\n      d=\"M117.22379,292.83905s15.41555-.47479,20.06141-3.783,23.713-7.2585,24.86553-1.95278,23.16671,26.38821,5.76263,26.5286-40.43935-2.711-45.07627-5.53549S117.22379,292.83905,117.22379,292.83905Z\"\n      fill=\"#a8a8a8\"\n      transform=\"translate(-31.39089 -100.5)\"\n    />\n    <path\n      d=\"M168.224,311.78489c-17.40408.14042-40.43933-2.71094-45.07626-5.53548-3.53126-2.151-4.93843-9.86945-5.40926-13.43043-.32607.014-.51463.02-.51463.02s.97638,12.43276,5.61331,15.2573,27.67217,5.67589,45.07626,5.53547c5.02386-.04052,6.7592-1.82793,6.66391-4.47526C173.87935,310.756,171.96329,311.75474,168.224,311.78489Z\"\n      opacity=\"0.2\"\n      transform=\"translate(-31.39089 -100.5)\"\n    />\n    <ellipse cx=\"198.60911\" cy=\"424.5\" fill=\"#3f3d56\" rx=\"187\" ry=\"25.43993\" />\n    <ellipse cx=\"198.60911\" cy=\"424.5\" opacity=\"0.1\" rx=\"157\" ry=\"21.35866\" />\n    <ellipse cx=\"836.60911\" cy=\"660.5\" fill=\"#3f3d56\" rx=\"283\" ry=\"38.5\" />\n    <ellipse cx=\"310.60911\" cy=\"645.5\" fill=\"#3f3d56\" rx=\"170\" ry=\"23.12721\" />\n    <path\n      d=\"M494,726.5c90,23,263-30,282-90\"\n      fill=\"none\"\n      stroke=\"#2f2e41\"\n      stroke-miterlimit=\"10\"\n      stroke-width=\"2\"\n      transform=\"translate(-31.39089 -100.5)\"\n    />\n    <path\n      d=\"M341,359.5s130-36,138,80-107,149-17,172\"\n      fill=\"none\"\n      stroke=\"#2f2e41\"\n      stroke-miterlimit=\"10\"\n      stroke-width=\"2\"\n      transform=\"translate(-31.39089 -100.5)\"\n    />\n    <path\n      d=\"M215.40233,637.78332s39.0723-10.82,41.47675,24.04449-32.15951,44.78287-5.10946,51.69566\"\n      fill=\"none\"\n      stroke=\"#2f2e41\"\n      stroke-miterlimit=\"10\"\n      stroke-width=\"2\"\n      transform=\"translate(-31.39089 -100.5)\"\n    />\n    <path\n      d=\"M810.09554,663.73988,802.218,714.03505s-38.78182,20.60284-11.51335,21.20881,155.73324,0,155.73324,0,24.84461,0-14.54318-21.81478l-7.87756-52.719Z\"\n      fill=\"#2f2e41\"\n      transform=\"translate(-31.39089 -100.5)\"\n    />\n    <path\n      d=\"M785.21906,734.69812c6.193-5.51039,16.9989-11.252,16.9989-11.252l7.87756-50.2952,113.9216.10717,7.87756,49.582c9.185,5.08711,14.8749,8.987,18.20362,11.97818,5.05882-1.15422,10.58716-5.44353-18.20362-21.38921l-7.87756-52.719-113.9216,3.02983L802.218,714.03506S769.62985,731.34968,785.21906,734.69812Z\"\n      opacity=\"0.1\"\n      transform=\"translate(-31.39089 -100.5)\"\n    />\n    <rect\n      fill=\"#2f2e41\"\n      height=\"357.51989\"\n      rx=\"18.04568\"\n      width=\"513.25314\"\n      x=\"578.43291\"\n      y=\"212.68859\"\n    />\n    <rect\n      fill=\"#3f3d56\"\n      height=\"267.83694\"\n      width=\"478.71308\"\n      x=\"595.70294\"\n      y=\"231.77652\"\n    />\n    <circle cx=\"835.05948\" cy=\"223.29299\" fill=\"#f2f2f2\" r=\"3.02983\" />\n    <path\n      d=\"M1123.07694,621.32226V652.6628a18.04341,18.04341,0,0,1-18.04568,18.04568H627.86949A18.04341,18.04341,0,0,1,609.8238,652.6628V621.32226Z\"\n      fill=\"#2f2e41\"\n      transform=\"translate(-31.39089 -100.5)\"\n    />\n    <polygon\n      fill=\"#2f2e41\"\n      points=\"968.978 667.466 968.978 673.526 642.968 673.526 642.968 668.678 643.417 667.466 651.452 645.651 962.312 645.651 968.978 667.466\"\n    />\n    <path\n      d=\"M1125.828,762.03359c-.59383,2.539-2.83591,5.21743-7.90178,7.75032-18.179,9.08949-55.1429-2.42386-55.1429-2.42386s-28.4804-4.84773-28.4804-17.573a22.72457,22.72457,0,0,1,2.49658-1.48459c7.64294-4.04351,32.98449-14.02122,77.9177.42248a18.73921,18.73921,0,0,1,8.54106,5.59715C1125.07908,756.45353,1126.50669,759.15715,1125.828,762.03359Z\"\n      fill=\"#2f2e41\"\n      transform=\"translate(-31.39089 -100.5)\"\n    />\n    <path\n      d=\"M1125.828,762.03359c-22.251,8.526-42.0843,9.1622-62.43871-4.975-10.26507-7.12617-19.59089-8.88955-26.58979-8.75618,7.64294-4.04351,32.98449-14.02122,77.9177.42248a18.73921,18.73921,0,0,1,8.54106,5.59715C1125.07908,756.45353,1126.50669,759.15715,1125.828,762.03359Z\"\n      opacity=\"0.1\"\n      transform=\"translate(-31.39089 -100.5)\"\n    />\n    <ellipse\n      cx=\"1066.53846\"\n      cy=\"654.13477\"\n      fill=\"#f2f2f2\"\n      rx=\"7.87756\"\n      ry=\"2.42386\"\n    />\n    <circle cx=\"835.05948\" cy=\"545.66686\" fill=\"#f2f2f2\" r=\"11.51335\" />\n    <polygon\n      opacity=\"0.1\"\n      points=\"968.978 667.466 968.978 673.526 642.968 673.526 642.968 668.678 643.417 667.466 968.978 667.466\"\n    />\n    <rect fill=\"#2f2e41\" height=\"242\" width=\"208\" x=\"108.60911\" y=\"159\" />\n    <rect fill=\"#3f3d56\" height=\"86\" width=\"250\" x=\"87.60911\" y=\"135\" />\n    <rect fill=\"#3f3d56\" height=\"86\" width=\"250\" x=\"87.60911\" y=\"237\" />\n    <rect fill=\"#3f3d56\" height=\"86\" width=\"250\" x=\"87.60911\" y=\"339\" />\n    <rect\n      fill=\"#6c63ff\"\n      height=\"16\"\n      opacity=\"0.4\"\n      width=\"16\"\n      x=\"271.60911\"\n      y=\"150\"\n    />\n    <rect\n      fill=\"#6c63ff\"\n      height=\"16\"\n      opacity=\"0.8\"\n      width=\"16\"\n      x=\"294.60911\"\n      y=\"150\"\n    />\n    <rect fill=\"#6c63ff\" height=\"16\" width=\"16\" x=\"317.60911\" y=\"150\" />\n    <rect\n      fill=\"#6c63ff\"\n      height=\"16\"\n      opacity=\"0.4\"\n      width=\"16\"\n      x=\"271.60911\"\n      y=\"251\"\n    />\n    <rect\n      fill=\"#6c63ff\"\n      height=\"16\"\n      opacity=\"0.8\"\n      width=\"16\"\n      x=\"294.60911\"\n      y=\"251\"\n    />\n    <rect fill=\"#6c63ff\" height=\"16\" width=\"16\" x=\"317.60911\" y=\"251\" />\n    <rect\n      fill=\"#6c63ff\"\n      height=\"16\"\n      opacity=\"0.4\"\n      width=\"16\"\n      x=\"271.60911\"\n      y=\"352\"\n    />\n    <rect\n      fill=\"#6c63ff\"\n      height=\"16\"\n      opacity=\"0.8\"\n      width=\"16\"\n      x=\"294.60911\"\n      y=\"352\"\n    />\n    <rect fill=\"#6c63ff\" height=\"16\" width=\"16\" x=\"317.60911\" y=\"352\" />\n    <circle cx=\"316.60911\" cy=\"538\" fill=\"#2f2e41\" r=\"79\" />\n    <rect fill=\"#2f2e41\" height=\"43\" width=\"24\" x=\"280.60911\" y=\"600\" />\n    <rect fill=\"#2f2e41\" height=\"43\" width=\"24\" x=\"328.60911\" y=\"600\" />\n    <ellipse cx=\"300.60911\" cy=\"643.5\" fill=\"#2f2e41\" rx=\"20\" ry=\"7.5\" />\n    <ellipse cx=\"348.60911\" cy=\"642.5\" fill=\"#2f2e41\" rx=\"20\" ry=\"7.5\" />\n    <circle cx=\"318.60911\" cy=\"518\" fill=\"#fff\" r=\"27\" />\n    <circle cx=\"318.60911\" cy=\"518\" fill=\"#3f3d56\" r=\"9\" />\n    <path\n      d=\"M271.36733,565.03228c-6.37889-28.56758,14.01185-57.43392,45.544-64.47477s62.2651,10.41,68.644,38.9776-14.51861,39.10379-46.05075,46.14464S277.74622,593.59986,271.36733,565.03228Z\"\n      fill=\"#6c63ff\"\n      transform=\"translate(-31.39089 -100.5)\"\n    />\n    <ellipse\n      cx=\"417.21511\"\n      cy=\"611.34365\"\n      fill=\"#2f2e41\"\n      rx=\"39.5\"\n      ry=\"12.40027\"\n      transform=\"translate(-238.28665 112.98044) rotate(-23.17116)\"\n    />\n    <ellipse\n      cx=\"269.21511\"\n      cy=\"664.34365\"\n      fill=\"#2f2e41\"\n      rx=\"39.5\"\n      ry=\"12.40027\"\n      transform=\"translate(-271.07969 59.02084) rotate(-23.17116)\"\n    />\n    <path\n      d=\"M394,661.5c0,7.732-19.90861,23-42,23s-43-14.268-43-22,20.90861-6,43-6S394,653.768,394,661.5Z\"\n      fill=\"#fff\"\n      transform=\"translate(-31.39089 -100.5)\"\n    />\n  </svg>\n</template>\n"
  },
  {
    "path": "packages/effects/common-ui/src/ui/fallback/icons/icon-coming-soon.vue",
    "content": "<template>\n  <svg\n    data-name=\"Layer 1\"\n    height=\"424.8366\"\n    viewBox=\"0 0 979.32677 424.8366\"\n    width=\"979.32677\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n    xmlns:xlink=\"http://www.w3.org/1999/xlink\"\n  >\n    <path\n      d=\"M993.71816,412.83936H419.142a9.19888,9.19888,0,0,0,0,18.39776H435.417V651.3026a9.19888,9.19888,0,0,0,18.39776,0l.1398-220.06548h461.1557l42.52,220.06548a9.19887,9.19887,0,1,0,18.39775,0l2.67633-220.06548h15.01383a9.19888,9.19888,0,0,0,0-18.39776Z\"\n      fill=\"#ccc\"\n      transform=\"translate(-110.33661 -237.5817)\"\n    />\n    <path\n      d=\"M518.73716,371.85047v38.9547H421.141a19.48915,19.48915,0,1,1-1.35523-38.95474q.67739-.02358,1.35523,0Z\"\n      fill=\"#f2f2f2\"\n      transform=\"translate(-110.33661 -237.5817)\"\n    />\n    <path\n      d=\"M521.13449,410.50552a1.49881,1.49881,0,0,1-1.49822,1.49822H419.40273a20.52615,20.52615,0,0,1,0-41.05229H519.63627a1.49827,1.49827,0,1,1,0,2.99653H419.40273a17.52964,17.52964,0,0,0,0,35.05924H519.63627A1.49883,1.49883,0,0,1,521.13449,410.50552Z\"\n      fill=\"hsl(var(--primary))\"\n      transform=\"translate(-110.33661 -237.5817)\"\n    />\n    <path\n      d=\"M518.73716,380.84H413.85905a.29966.29966,0,0,1-.00552-.59929H518.73716a.29966.29966,0,0,1,0,.59929Z\"\n      fill=\"#ccc\"\n      transform=\"translate(-110.33661 -237.5817)\"\n    />\n    <path\n      d=\"M518.73716,388.03169H413.85905a.29966.29966,0,0,1-.00552-.59929H518.73716a.29966.29966,0,0,1,0,.59929Z\"\n      fill=\"#ccc\"\n      transform=\"translate(-110.33661 -237.5817)\"\n    />\n    <path\n      d=\"M518.73716,395.22332H413.85905a.29966.29966,0,0,1-.00552-.59929H518.73716a.29966.29966,0,0,1,0,.59929Z\"\n      fill=\"#ccc\"\n      transform=\"translate(-110.33661 -237.5817)\"\n    />\n    <path\n      d=\"M518.73716,402.41487H413.85905a.29966.29966,0,0,1-.00552-.59929H518.73716a.29966.29966,0,0,1,0,.59929Z\"\n      fill=\"#ccc\"\n      transform=\"translate(-110.33661 -237.5817)\"\n    />\n    <path\n      d=\"M500.33941,330.80932v38.95474H402.74324a19.48915,19.48915,0,0,1-1.35522-38.95474q.67737-.02358,1.35522,0Z\"\n      fill=\"#f2f2f2\"\n      transform=\"translate(-110.33661 -237.5817)\"\n    />\n    <path\n      d=\"M502.73673,369.46442a1.49885,1.49885,0,0,1-1.49822,1.49826H401.005a20.52614,20.52614,0,0,1,0-41.05229H501.23851a1.49826,1.49826,0,1,1,0,2.99652H401.005a17.52964,17.52964,0,0,0,0,35.05928H501.23851A1.49884,1.49884,0,0,1,502.73673,369.46442Z\"\n      fill=\"#3f3d56\"\n      transform=\"translate(-110.33661 -237.5817)\"\n    />\n    <path\n      d=\"M500.33941,339.79886H395.4613a.29966.29966,0,0,1-.00553-.59929H500.33941a.29966.29966,0,0,1,0,.59929Z\"\n      fill=\"#ccc\"\n      transform=\"translate(-110.33661 -237.5817)\"\n    />\n    <path\n      d=\"M500.33941,346.99054H395.4613a.29966.29966,0,0,1-.00553-.59929H500.33941a.29966.29966,0,0,1,0,.59929Z\"\n      fill=\"#ccc\"\n      transform=\"translate(-110.33661 -237.5817)\"\n    />\n    <path\n      d=\"M500.33941,354.18217H395.4613a.29966.29966,0,0,1-.00553-.59929H500.33941a.29966.29966,0,0,1,0,.59929Z\"\n      fill=\"#ccc\"\n      transform=\"translate(-110.33661 -237.5817)\"\n    />\n    <path\n      d=\"M500.33941,361.37376H395.4613a.29966.29966,0,0,1-.00553-.59929H500.33941a.29966.29966,0,0,1,0,.59929Z\"\n      fill=\"#ccc\"\n      transform=\"translate(-110.33661 -237.5817)\"\n    />\n    <path\n      d=\"M613.87355,550.68347V516.71838a5.661,5.661,0,0,0-5.66085-5.66085H479.4284a5.661,5.661,0,0,0-5.66084,5.66085v33.96509Z\"\n      fill=\"#ccc\"\n      transform=\"translate(-110.33661 -237.5817)\"\n    />\n    <rect\n      fill=\"#ccc\"\n      height=\"43.87158\"\n      width=\"140.10602\"\n      x=\"363.43092\"\n      y=\"325.83868\"\n    />\n    <path\n      d=\"M473.76756,620.02887V653.994a5.661,5.661,0,0,0,5.66084,5.66084H608.2127a5.661,5.661,0,0,0,5.66085-5.66084V620.02887Z\"\n      fill=\"#ccc\"\n      transform=\"translate(-110.33661 -237.5817)\"\n    />\n    <circle cx=\"432.77633\" cy=\"294.70402\" fill=\"#fff\" r=\"4.24564\" />\n    <circle cx=\"432.77633\" cy=\"351.3125\" fill=\"#fff\" r=\"4.24564\" />\n    <circle cx=\"433.00385\" cy=\"406.72228\" fill=\"#fff\" r=\"4.24564\" />\n    <path\n      d=\"M597.989,472.33053v38.9547H500.39287a19.48916,19.48916,0,0,1-1.35647-38.9547q.678-.02358,1.35647,0Z\"\n      fill=\"#f2f2f2\"\n      transform=\"translate(-110.33661 -237.5817)\"\n    />\n    <path\n      d=\"M600.38637,510.98558a1.49881,1.49881,0,0,1-1.49822,1.49822H498.65461a20.52615,20.52615,0,0,1-.0247-41.05229H598.88815a1.49827,1.49827,0,1,1,0,2.99653H498.65461a17.52963,17.52963,0,0,0,0,35.05923H598.88815A1.49885,1.49885,0,0,1,600.38637,510.98558Z\"\n      fill=\"#3f3d56\"\n      transform=\"translate(-110.33661 -237.5817)\"\n    />\n    <path\n      d=\"M597.989,481.32H493.111a.29966.29966,0,0,1-.00553-.59929H597.98913a.29966.29966,0,0,1,0,.59929Z\"\n      fill=\"#ccc\"\n      transform=\"translate(-110.33661 -237.5817)\"\n    />\n    <path\n      d=\"M597.989,488.51175H493.111a.29966.29966,0,0,1-.00553-.59929H597.98913a.29966.29966,0,0,1,0,.59929Z\"\n      fill=\"#ccc\"\n      transform=\"translate(-110.33661 -237.5817)\"\n    />\n    <path\n      d=\"M597.989,495.70338H493.111a.29966.29966,0,0,1-.00553-.59929H597.98913a.29966.29966,0,0,1,0,.59929Z\"\n      fill=\"#ccc\"\n      transform=\"translate(-110.33661 -237.5817)\"\n    />\n    <path\n      d=\"M597.989,502.89493H493.111a.29966.29966,0,0,1-.00553-.59929H597.98913a.29966.29966,0,0,1,0,.59929Z\"\n      fill=\"#ccc\"\n      transform=\"translate(-110.33661 -237.5817)\"\n    />\n    <path\n      d=\"M483.36747,317.81415H438.90162a2.74745,2.74745,0,0,0-1.21689.28306l-11.22288,5.61835a2.0452,2.0452,0,0,0,0,3.76443l11.22288,5.61835a2.74718,2.74718,0,0,0,1.21689.28306h44.46585a2.33381,2.33381,0,0,0,2.4628-2.16532v-11.2367A2.3338,2.3338,0,0,0,483.36747,317.81415Z\"\n      fill=\"#3f3d56\"\n      transform=\"translate(-110.33661 -237.5817)\"\n    />\n    <path\n      d=\"M485.83027,319.97947v11.2367a2.33383,2.33383,0,0,1-2.4628,2.16532h-8.8589V317.81415h8.8589A2.33383,2.33383,0,0,1,485.83027,319.97947Z\"\n      fill=\"hsl(var(--primary))\"\n      transform=\"translate(-110.33661 -237.5817)\"\n    />\n    <path\n      d=\"M216.78083,537.99332a35.33951,35.33951,0,0,0,34.12552-6.01134c11.95262-10.03214,15.70013-26.56,18.74934-41.864q4.50949-22.63308,9.019-45.26617l-18.88217,13.00153c-13.57891,9.34993-27.46375,18.99939-36.86572,32.54233S209.42082,522.42587,216.975,537.08\"\n      fill=\"#e6e6e6\"\n      transform=\"translate(-110.33661 -237.5817)\"\n    />\n    <path\n      d=\"M218.39489,592.79741c-1.91113-13.92071-3.87625-28.0202-2.53572-42.09016,1.19057-12.4956,5.00277-24.70032,12.764-34.70734a57.73582,57.73582,0,0,1,14.81307-13.42309c1.48131-.935,2.84468,1.41257,1.36983,2.34348a54.88844,54.88844,0,0,0-21.71125,26.19626c-4.72684,12.02273-5.48591,25.12848-4.67135,37.90006.4926,7.72345,1.53656,15.39627,2.58859,23.05926a1.40615,1.40615,0,0,1-.94781,1.66928,1.3653,1.3653,0,0,1-1.6693-.94781Z\"\n      fill=\"#f2f2f2\"\n      transform=\"translate(-110.33661 -237.5817)\"\n    />\n    <path\n      d=\"M236.80246,568.16434a26.01425,26.01425,0,0,0,22.6665,11.69871c11.47417-.54466,21.04-8.55293,29.651-16.15584l25.46969-22.48783-16.85671-.80672c-12.12234-.58011-24.55745-1.12124-36.10356,2.617s-22.19457,12.73508-24.30583,24.68624\"\n      fill=\"#e6e6e6\"\n      transform=\"translate(-110.33661 -237.5817)\"\n    />\n    <path\n      d=\"M212.99392,600.79976c9.19853-16.27621,19.86805-34.36538,38.93262-40.14695A43.445,43.445,0,0,1,268.3022,558.962c1.73863.14991,1.30448,2.82994-.431,2.6803a40.36111,40.36111,0,0,0-26.133,6.91386c-7.36852,5.01554-13.10573,11.98848-17.96161,19.383-2.97439,4.52936-5.63867,9.25082-8.30346,13.966-.85161,1.50687-3.34078.41915-2.47922-1.10534Z\"\n      fill=\"#f2f2f2\"\n      transform=\"translate(-110.33661 -237.5817)\"\n    />\n    <path\n      d=\"M198.25523,617.93168a19.69836,19.69836,0,0,1,12.0709-16.49847v-9.40956h15.782v9.70608a19.68812,19.68812,0,0,1,11.41362,16.202l3.711,43.13835H194.54417Z\"\n      fill=\"#f2f2f2\"\n      transform=\"translate(-110.33661 -237.5817)\"\n    />\n    <path\n      d=\"M734.973,411.955l-4.69488-1.97685-3.22067-23.53551h-42.889l-3.491,23.43936-4.20031,2.10013a.99744.99744,0,0,0,.44611,1.88955h57.66283A.99739.99739,0,0,0,734.973,411.955Z\"\n      fill=\"#e6e6e6\"\n      transform=\"translate(-110.33661 -237.5817)\"\n    />\n    <path\n      d=\"M811.1898,389.574H600.50692a4.174,4.174,0,0,1-4.16467-4.174V355.69092H815.35446V385.4A4.17408,4.17408,0,0,1,811.1898,389.574Z\"\n      fill=\"#ccc\"\n      transform=\"translate(-110.33661 -237.5817)\"\n    />\n    <path\n      d=\"M815.57469,369.73213H596.15V242.61337a5.0375,5.0375,0,0,1,5.03186-5.03167h209.361a5.03755,5.03755,0,0,1,5.03191,5.03167Z\"\n      fill=\"#3f3d56\"\n      transform=\"translate(-110.33661 -237.5817)\"\n    />\n    <path\n      d=\"M802.46932,360.50584h-193.214a3.88344,3.88344,0,0,1-3.87919-3.87908V250.68707a3.88365,3.88365,0,0,1,3.87919-3.87932h193.214a3.88366,3.88366,0,0,1,3.8792,3.87932V356.62676A3.88345,3.88345,0,0,1,802.46932,360.50584Z\"\n      fill=\"#fff\"\n      transform=\"translate(-110.33661 -237.5817)\"\n    />\n    <path\n      d=\"M751.57964,397.88662a11.6159,11.6159,0,0,1,17.666,2.27241l26.13446-4.64642,6.69716,15.19317-36.99908,6.04328a11.67883,11.67883,0,0,1-13.49855-18.86244Z\"\n      fill=\"#ffb6b6\"\n      transform=\"translate(-110.33661 -237.5817)\"\n    />\n    <path\n      d=\"M775.77611,417.286l27.24571-.33963,3.44882-.04668,55.43253-.69843s15.05312-14.3609,28.16068-29.1465l-1.83719-13.28833A54.29159,54.29159,0,0,0,870.023,340.1519C851.24988,352.696,840.363,377.52559,840.363,377.52559l-34.37018,8.22071-3.43848.82227-21.35608,5.10326Z\"\n      fill=\"hsl(var(--primary))\"\n      transform=\"translate(-110.33661 -237.5817)\"\n    />\n    <path\n      d=\"M915.25011,498.96167H864.39249c0,2.17915-55.59414,3.94772-55.59414,3.94772a20.30858,20.30858,0,0,0-3.33166,3.15818,19.59694,19.59694,0,0,0-4.58,12.63271v3.15818a19.74588,19.74588,0,0,0,19.73861,19.73861h94.62478a19.75579,19.75579,0,0,0,19.73862-19.73861v-3.15818A19.76607,19.76607,0,0,0,915.25011,498.96167Z\"\n      fill=\"#e4e4e4\"\n      transform=\"translate(-110.33661 -237.5817)\"\n    />\n    <rect\n      fill=\"#e4e4e4\"\n      height=\"118.48951\"\n      width=\"20.52816\"\n      x=\"747.4019\"\n      y=\"303.23122\"\n    />\n    <path\n      d=\"M799.31222,658.58132c0,2.218,31.10721.858,69.47992.858s69.47991,1.36012,69.47991-.858-31.1072-19.807-69.47991-19.807S799.31222,656.36323,799.31222,658.58132Z\"\n      fill=\"#e4e4e4\"\n      transform=\"translate(-110.33661 -237.5817)\"\n    />\n    <polygon\n      fill=\"#ffb6b6\"\n      points=\"675.186 407.461 659.908 407.46 652.64 348.531 675.188 348.532 675.186 407.461\"\n    />\n    <path\n      d=\"M789.41863,659.852l-49.2623-.00183v-.62309a19.17528,19.17528,0,0,1,19.17426-19.17395h.00122l30.08773.00122Z\"\n      fill=\"#2f2e41\"\n      transform=\"translate(-110.33661 -237.5817)\"\n    />\n    <polygon\n      fill=\"#ffb6b6\"\n      points=\"630.031 407.461 614.753 407.46 607.485 348.531 630.033 348.532 630.031 407.461\"\n    />\n    <path\n      d=\"M744.2636,659.852l-49.2623-.00183v-.62309a19.1753,19.1753,0,0,1,19.17426-19.17395h.00122l30.08773.00122Z\"\n      fill=\"#2f2e41\"\n      transform=\"translate(-110.33661 -237.5817)\"\n    />\n    <circle cx=\"766.88656\" cy=\"41.63615\" fill=\"#ffb6b6\" r=\"26.56401\" />\n    <path\n      d=\"M920.21655,461.22417s8.91308,47.1307-24.99958,53.13247-82.86639,10.21993-82.86639,10.21993L790.36706,627.14324l-29.53443-2.63675s3.928-123.46737,13.5876-133.127,70.71212-38.58282,70.71212-38.58282Z\"\n      fill=\"#2f2e41\"\n      transform=\"translate(-110.33661 -237.5817)\"\n    />\n    <path\n      d=\"M853.98286,441.47135,839.151,456.35062s-107.0941,17.25-111.22553,41.9852c-6.23747,37.34427-13.60493,118.552-13.60493,118.552l32.1988-2.41491,12.62647-92.31123,51.5182-11.71874L869.27729,478.5Z\"\n      fill=\"#2f2e41\"\n      transform=\"translate(-110.33661 -237.5817)\"\n    />\n    <path\n      d=\"M902.78526,263.36115c-2.6223-4.94751-5.95413-14.80785-11.24679-16.63657a42.07731,42.07731,0,0,0-9.05841-1.92972l-8.99618,3.46009,4.89616-3.808q-1.42988-.08519-2.85817-.13928l-6.0699,2.33453,3.10542-2.41532c-5.65883-.05808-11.5.53031-15.88468,3.9752-3.73817,2.93677-7.44169,14.06185-8.04057,18.77753a35.9171,35.9171,0,0,0,.6603,13.53055l1.53716,1.46166a18.85936,18.85936,0,0,0,1.206-3.83883,18.18056,18.18056,0,0,1,8.70263-11.80641l.08368-.0472c2.5782-1.451,5.7065-1.3841,8.66308-1.27769l14.04158.50527c3.37829.12158,7.01608.33533,9.64978,2.45443a15.888,15.888,0,0,1,3.85826,5.58929c1.30868,2.6414,3.8661,12.60418,3.8661,12.60418s1.44689-1.88062,2.1404-.48092a48.39766,48.39766,0,0,0,2.01437-11.23347A22.00877,22.00877,0,0,0,902.78526,263.36115Z\"\n      fill=\"#2f2e41\"\n      transform=\"translate(-110.33661 -237.5817)\"\n    />\n    <path\n      d=\"M995.69426,290.88349A11.61582,11.61582,0,0,0,985.181,305.26136l-21.3614,15.75722,6.40951,15.31674,29.8539-22.67594a11.67883,11.67883,0,0,0-4.38876-22.77589Z\"\n      fill=\"#ffb6b6\"\n      transform=\"translate(-110.33661 -237.5817)\"\n    />\n    <path\n      d=\"M992.25627,323.052l-53.551,59.4744s-25.60913-8.19816-45.41466-17.08624l-8.8977-27.32787a54.34329,54.34329,0,0,1-2.60112-19.66442c27.45606-7.306,59.391,19.87863,59.391,19.87863l40.08517-31.39877Z\"\n      fill=\"hsl(var(--primary))\"\n      transform=\"translate(-110.33661 -237.5817)\"\n    />\n    <path\n      d=\"M867.301,465.6169c-9.554-3.30029-19.43312-6.71277-30.08912-7.99385l-.45773-.05533.12632-.443c11.03073-38.7308,8.27761-63.50657,2.87195-100.72306a37.59072,37.59072,0,0,1,21.5483-39.50121l.06542-.02958,30.43436-1.93391.06935-.00423,22.13437,6.50989a15.18313,15.18313,0,0,1,10.86724,14.83111c-.23987,12.23937.26868,25.9043.80711,40.37114,1.20787,32.45569,2.45686,66.01647-4.63045,87.79166l-.03718.11412-.09462.07416a36.09883,36.09883,0,0,1-23.08086,8.10758C887.90057,472.73235,877.76186,469.23034,867.301,465.6169Z\"\n      fill=\"hsl(var(--primary))\"\n      transform=\"translate(-110.33661 -237.5817)\"\n    />\n    <path\n      d=\"M1088.24817,662.4183H111.75183a1.41521,1.41521,0,1,1,0-2.83042h976.49634a1.41521,1.41521,0,1,1,0,2.83042Z\"\n      fill=\"#ccc\"\n      transform=\"translate(-110.33661 -237.5817)\"\n    />\n  </svg>\n</template>\n"
  },
  {
    "path": "packages/effects/common-ui/src/ui/fallback/icons/icon-offline.vue",
    "content": "<template>\n  <svg\n    height=\"458.68642\"\n    viewBox=\"0 0 656 458.68642\"\n    width=\"656\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n    xmlns:xlink=\"http://www.w3.org/1999/xlink\"\n  >\n    <rect fill=\"#3f3d56\" height=\"2\" width=\"656\" y=\"434.34322\" />\n    <g>\n      <path\n        d=\"M471.97092,210.81397c-6.0733-36.41747-37.72842-64.16942-75.86423-64.16942H240.14931c-38.12099,0-69.76869,27.72972-75.86421,64.12497-.70358,4.16241-1.06653,8.44331-1.06653,12.80573v135.88599c0,4.36237,.36295,8.63589,1.06653,12.79831,4.85126,28.99625,25.92996,52.49686,53.58563,60.84393,7.05095,2.13306,14.53143,3.28104,22.27859,3.28104h155.9574c7.74716,0,15.22763-1.14798,22.27859-3.28104,27.66309-8.35449,48.74921-31.86993,53.58563-60.88837,.6962-4.14758,1.05911-8.40628,1.05911-12.75388V223.57525c0-4.34758-.36292-8.61369-1.05911-12.76128h-.00003Zm-62.66592,222.28954c-4.2883,.76285-8.69516,1.16281-13.19827,1.16281H240.14931c-4.50313,0-8.90997-.39999-13.19829-1.16281-35.01768-6.22885-61.60677-36.83228-61.60677-73.64224v-45.10526c0-127.45004,103.31242-165.58582,230.76244-165.58582,41.31314,0,74.80505,33.49194,74.80505,74.80505v135.88599c-100.29059,13.42047-26.58911,67.41339-61.60678,73.64224l.00003,.00003Z\"\n        fill=\"#3f3d56\"\n      />\n      <polygon\n        fill=\"hsl(var(--primary))\"\n        points=\"349.16196 249.18644 355.16196 288.18642 443.16196 276.18642 434.66196 230.6195 349.16196 249.18644\"\n      />\n      <rect\n        fill=\"#2f2e41\"\n        height=\"37.66125\"\n        width=\"36.38461\"\n        x=\"381.84177\"\n        y=\"30.34218\"\n      />\n      <polygon\n        fill=\"#ffb6b6\"\n        points=\"385.16196 70.18643 394.16196 43.18643 411.70447 43.18643 412.62653 70.18643 385.16196 70.18643\"\n      />\n      <polygon\n        isolation=\"isolate\"\n        opacity=\".1\"\n        points=\"385.16196 70.18643 394.16196 43.18643 411.70447 43.18643 412.62653 70.18643 385.16196 70.18643\"\n      />\n      <path\n        d=\"M394.66196,310.68642l-1,104-1,8v11.48425l15,1.51575,1-23s16-45,12-80-2-25-2-25l-24,3Z\"\n        fill=\"#ffb6b6\"\n      />\n      <path\n        d=\"M404.18408,318.85363l-36.90134,97.23831-1.97873,7.81567-4.1777,10.69742-14.52368-4.04477,7.43539-21.78796s1.46619-47.7373,17.92432-78.88422,10.9574-22.5596,10.9574-22.5596l21.26434,11.52512v.00003Z\"\n        fill=\"#ffb6b6\"\n      />\n      <path\n        d=\"M385.16196,67.18643l-27,12,17.23959,89.01208-2.72385,127.75565-18,38s-3.01575,21.73227,27.98425,7.73227,66-18,66-18l-8.5-58.5-7.5-153.5,1-34-22-14s-26.5,3.5-26.5,3.50001Z\"\n        fill=\"#2f2e41\"\n      />\n      <path\n        d=\"M370.1243,335.34322l-29.96231-50.15677,34.23959-116.98792-16.23959-89.01208,28.49045-12.19685s14.74915,14.36248,14.74915,26.20894-31.27728,242.1447-31.27728,242.1447v-.00003Z\"\n        fill=\"#e6e6e6\"\n      />\n      <path\n        d=\"M435.1243,325.34322l-27.19693-233.62811c-.34341-2.94999,.16013-5.93678,1.45178-8.6111l7.78284-16.11441,30.5,8.69685-12.26041,95.51208,32.76041,93.98792-33.03769,60.15677Z\"\n        fill=\"#e6e6e6\"\n      />\n      <path\n        d=\"M410.66196,433.68642s-19-11-21-5-3,11-3,11c0,0-5,19,10,19s14-8.64172,14-8.64172v-16.35828Z\"\n        fill=\"#2f2e41\"\n      />\n      <path\n        d=\"M344.53574,427.60598s21.69977-3.33459,21.3801,2.9819c-.3197,6.31647-1.20709,11.33768-1.20709,11.33768,0,0-2.25433,19.51712-16.22662,14.06046s-9.89713-13.14252-9.89713-13.14252l5.95078-15.23749-.00003-.00003Z\"\n        fill=\"#2f2e41\"\n      />\n      <circle cx=\"404.10297\" cy=\"33.02146\" fill=\"#ffb6b6\" r=\"24.85993\" />\n      <path\n        d=\"M423.96469,10.86766c-1.15707-6.12936-7.44913-10.27514-13.66504-10.79501s-12.30453,1.82726-17.90228,4.57921c-3.79456,1.86548-7.53061,3.96811-10.60425,6.87182s-5.46063,6.69692-6.01202,10.88913c-.19507,1.48324-.1698,3.03289-.77692,4.40016-.75845,1.708-2.38654,2.86795-3.36917,4.4576-1.76227,2.85096-.95267,6.99858,1.75238,8.97753-3.40024,1.44912-6.89398,2.96069-9.48602,5.59563s-4.08878,6.70308-2.66644,10.11462c.50323,1.20699,1.33481,2.26349,1.76489,3.49843,.81668,2.34499,.03943,5.00909-1.40924,7.02585s-3.49316,3.51228-5.50174,4.97226c5.16196,1.01177,10.43097,1.80015,15.66992,1.32811s10.49707-2.30805,14.29086-5.95176c3.79379-3.64371,5.88083-9.26437,4.51974-14.34539-1.04269-3.89231-3.95898-7.30301-3.95712-11.33256,.00143-3.09747,1.7431-5.89158,3.4249-8.49271,3.67291-5.68066,7.34579-11.36132,11.01868-17.04197,.66068-1.02183,1.35739-2.07924,2.4014-2.70425,1.77606-1.06326,4.0798-.59568,5.95227,.28683,1.87244,.88252,3.58304,2.14867,5.57941,2.69585,4.07452,1.11677,8.80106-1.44789,10.08575-5.47261\"\n        fill=\"#2f2e41\"\n      />\n      <path\n        d=\"M409.27951,61.42523c-2.07159,2.0061-5.05701,2.65225-7.82379,3.46516s-5.70978,2.09141-6.95499,4.69243c-1.22101,2.55043-.33459,5.78793,1.68692,7.76505s4.95816,2.80999,7.78555,2.77077c2.82736-.03922,5.58282-.86796,8.24176-1.8301,7.27054-2.63087,14.15665-6.32148,20.37314-10.919-4.02679-1.11411-6.66107-5.81614-5.50836-9.83205,.93768-3.26677,3.80499-5.54528,5.75616-8.32809,3.35959-4.79151,3.91925-11.10753,2.80676-16.85277-1.11246-5.74524-3.73163-11.07097-6.32358-16.3176-.81934-1.65853-1.65805-3.34513-2.93619-4.68245-1.27814-1.33731-3.08783-2.29539-4.92776-2.10379-3.05334,.31795-5.00302,3.66989-5.02377,6.7397s1.32593,5.95491,2.34732,8.84988c1.05231,2.98259,1.78381,6.14409,1.50146,9.29425-.2366,2.63989-1.19669,5.21132-2.74811,7.36029-1.19809,1.65954-2.72479,3.05223-4.0275,4.63097-1.00714,1.22055-1.90009,2.60309-2.16486,4.16321-.48181,2.83914,1.18356,5.71186,.72714,8.55519-.48248,3.0056-3.6452,5.3067-6.65341,4.84085\"\n        fill=\"#2f2e41\"\n      />\n      <g>\n        <circle\n          cx=\"333.2486\"\n          cy=\"323.64455\"\n          fill=\"hsl(var(--primary))\"\n          r=\"85\"\n        />\n        <g>\n          <path\n            d=\"M384.17838,316.82296h-10.56668c-1.64377-9.68713-6.7168-18.46011-14.2923-24.71729-17.43427-14.39993-43.24109-11.94022-57.64099,5.49411-.04913,.05563-.09644,.11282-.14169,.17151-1.15063,1.49146-.87427,3.63333,.61716,4.784,1.49118,1.1507,3.63306,.87448,4.78394-.61697,6.25537-7.5788,15.72369-12.40167,26.31064-12.40167,16.20853,.00195,30.17899,11.40631,33.42572,27.28629h-9.31805c-.3988,.00012-.78458,.13992-1.09082,.39502-.72375,.60281-.82175,1.6781-.21915,2.40186l13.41125,16.09894c.06577,.07889,.13855,.1517,.21759,.21747,.72324,.60327,1.79871,.50583,2.40186-.21747l13.41125-16.09894c.25504-.30624,.3949-.69223,.39514-1.09082,.00027-.94186-.763-1.70566-1.70486-1.70605v.00003Z\"\n            fill=\"#fff\"\n          />\n          <path\n            d=\"M364.34329,344.7337c-1.49146-1.15063-3.63333-.87433-4.78394,.6171-4.96201,6.00781-11.83066,10.13629-19.46436,11.69922-18.46167,3.77988-36.49231-8.12213-40.27225-26.58392h9.3183c.94186-.0004,1.70514-.76419,1.70486-1.70605-.00027-.39853-.14011-.78452-.39514-1.09082l-13.41125-16.09888c-.60312-.72336-1.67862-.8208-2.40186-.21753-.07904,.06577-.15182,.13855-.21759,.21753l-13.41125,16.09888c-.6026,.72375-.50461,1.7991,.21915,2.40186,.30624,.25516,.69205,.3949,1.09082,.39502h10.56641c1.64404,9.68723,6.7168,18.46011,14.29254,24.71729,17.43427,14.39999,43.24109,11.94022,57.64099-5.49405,.04913-.05569,.09619-.11295,.14142-.17163,1.15088-1.49146,.87454-3.63327-.61691-4.784h.00006Z\"\n            fill=\"#fff\"\n          />\n        </g>\n      </g>\n      <path\n        id=\"uuid-da16df1e-5659-4232-96f6-61e8c639a9ec-574\"\n        d=\"M356.98148,237.19363c-1.02939,7.36621-5.66458,12.80598-10.35239,12.15012-4.68781-.65588-7.65225-7.15837-6.62149-14.52707,.37137-2.94914,1.4436-5.76646,3.12701-8.21626l4.75577-31.15587,14.57297,2.54338-6.23553,30.44414c.94736,2.81844,1.20581,5.82278,.75369,8.76157h-.00003Z\"\n        fill=\"#ffb6b6\"\n      />\n      <path\n        d=\"M369.66196,77.68643s-15-5-17,13-4,39.99999-4,39.99999c0,0-9,21-5,32s11,3.3307,4,12.66534-6.02478,40.04724-6.02478,40.04724l22.52478-1.13387s12.5-82.57875,12.5-84.57875-7-52-7-52v.00004Z\"\n        fill=\"#e6e6e6\"\n      />\n      <g>\n        <path\n          id=\"uuid-6bf35aa9-e432-4b51-af77-8f4eb19e6e42-575\"\n          d=\"M467.16132,233.84998c.27881,7.43257-3.33017,13.60114-8.06033,13.7778s-8.78937-5.70491-9.06732-13.14017c-.15176-2.96857,.40961-5.93028,1.63712-8.63741l-.78369-31.507,14.79315-.05261-.798,31.0659c1.42709,2.60854,2.20859,5.52095,2.27905,8.49347l.00003,.00002Z\"\n          fill=\"#ffb6b6\"\n        />\n        <path\n          d=\"M444.06961,77.34876s15.08694-4.73121,16.76505,13.30165,3.28473,51.06508,3.28473,51.06508c0,0,8.62338,21.15744,4.42749,32.08421s-11.05774,3.13365-4.22565,12.59187c6.83212,9.45822,4.37997,36.13126,4.37997,36.13126l-22.50095-1.53612s-10.09427-78.77167-10.05853-80.77133,7.92792-62.86664,7.92792-62.86664l-.00003,.00002Z\"\n          fill=\"#e6e6e6\"\n        />\n      </g>\n    </g>\n  </svg>\n</template>\n"
  },
  {
    "path": "packages/effects/common-ui/src/ui/fallback/index.ts",
    "content": "export type * from './fallback';\nexport { default as Fallback } from './fallback.vue';\n"
  },
  {
    "path": "packages/effects/common-ui/src/ui/index.ts",
    "content": "export * from './about';\nexport * from './authentication';\nexport * from './dashboard';\nexport * from './fallback';\n"
  },
  {
    "path": "packages/effects/common-ui/tsconfig.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/tsconfig\",\n  \"extends\": \"@vben/tsconfig/web.json\",\n  \"include\": [\"src\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "packages/effects/hooks/README.md",
    "content": "# @vben/hooks\n\n用于多个 `app` 公用的 hook，继承了 `@vben/hooks` 的所有能力。业务上有通用 hooks 可以放在这里。\n\n## 用法\n\n### 添加依赖\n\n```bash\n# 进入目标应用目录，例如 apps/xxxx-app\n# cd apps/xxxx-app\npnpm add @vben/hooks\n```\n\n### 使用\n\n```ts\nimport { useNamespace } from '@vben/hooks';\n```\n"
  },
  {
    "path": "packages/effects/hooks/package.json",
    "content": "{\n  \"name\": \"@vben/hooks\",\n  \"version\": \"5.5.9\",\n  \"homepage\": \"https://github.com/vbenjs/vue-vben-admin\",\n  \"bugs\": \"https://github.com/vbenjs/vue-vben-admin/issues\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/vbenjs/vue-vben-admin.git\",\n    \"directory\": \"packages/effects/hooks\"\n  },\n  \"license\": \"MIT\",\n  \"type\": \"module\",\n  \"sideEffects\": [\n    \"**/*.css\"\n  ],\n  \"exports\": {\n    \".\": {\n      \"types\": \"./src/index.ts\",\n      \"default\": \"./src/index.ts\"\n    }\n  },\n  \"dependencies\": {\n    \"@vben-core/composables\": \"workspace:*\",\n    \"@vben/preferences\": \"workspace:*\",\n    \"@vben/stores\": \"workspace:*\",\n    \"@vben/types\": \"workspace:*\",\n    \"@vben/utils\": \"workspace:*\",\n    \"@vueuse/core\": \"catalog:\",\n    \"vue\": \"catalog:\",\n    \"vue-router\": \"catalog:\",\n    \"watermark-js-plus\": \"catalog:\"\n  }\n}\n"
  },
  {
    "path": "packages/effects/hooks/src/index.ts",
    "content": "export * from './use-app-config';\nexport * from './use-content-maximize';\nexport * from './use-design-tokens';\nexport * from './use-hover-toggle';\nexport * from './use-pagination';\nexport * from './use-refresh';\nexport * from './use-tabs';\nexport * from './use-watermark';\nexport * from '@vben-core/composables';\n"
  },
  {
    "path": "packages/effects/hooks/src/use-app-config.ts",
    "content": "import type {\n  ApplicationConfig,\n  VbenAdminProAppConfigRaw,\n} from '@vben/types/global';\n\n/**\n * 由 vite-inject-app-config 注入的全局配置\n */\nexport function useAppConfig(\n  env: Record<string, any>,\n  isProduction: boolean,\n): ApplicationConfig {\n  // 生产环境下，直接使用 window._VBEN_ADMIN_PRO_APP_CONF_ 全局变量\n  const config = isProduction\n    ? window._VBEN_ADMIN_PRO_APP_CONF_\n    : (env as VbenAdminProAppConfigRaw);\n\n  const {\n    VITE_GLOB_API_URL,\n    VITE_GLOB_APP_CLIENT_ID,\n    VITE_GLOB_ENABLE_ENCRYPT,\n    VITE_GLOB_RSA_PRIVATE_KEY,\n    VITE_GLOB_RSA_PUBLIC_KEY,\n    VITE_GLOB_SSE_ENABLE,\n    VITE_GLOB_WEBSOCKET_ENABLE,\n  } = config;\n\n  return {\n    // 后端地址\n    apiURL: VITE_GLOB_API_URL,\n    // 客户端key\n    clientId: VITE_GLOB_APP_CLIENT_ID,\n    enableEncrypt: VITE_GLOB_ENABLE_ENCRYPT === 'true',\n    // RSA私钥\n    rsaPrivateKey: VITE_GLOB_RSA_PRIVATE_KEY,\n    // RSA公钥\n    rsaPublicKey: VITE_GLOB_RSA_PUBLIC_KEY,\n    // 是否开启sse\n    sseEnable: VITE_GLOB_SSE_ENABLE === 'true',\n    // 是否开启websocket\n    websocketEnable: VITE_GLOB_WEBSOCKET_ENABLE === 'true',\n  };\n}\n"
  },
  {
    "path": "packages/effects/hooks/src/use-content-maximize.ts",
    "content": "import { updatePreferences, usePreferences } from '@vben/preferences';\n/**\n * 主体区域最大化\n */\nexport function useContentMaximize() {\n  const { contentIsMaximize } = usePreferences();\n\n  function toggleMaximize() {\n    const isMaximize = contentIsMaximize.value;\n\n    updatePreferences({\n      header: {\n        hidden: !isMaximize,\n      },\n      sidebar: {\n        hidden: !isMaximize,\n      },\n    });\n  }\n  return {\n    contentIsMaximize,\n    toggleMaximize,\n  };\n}\n"
  },
  {
    "path": "packages/effects/hooks/src/use-design-tokens.ts",
    "content": "import { reactive, watch } from 'vue';\n\nimport { preferences, usePreferences } from '@vben/preferences';\nimport { convertToRgb, updateCSSVariables } from '@vben/utils';\n\n/**\n * 用于适配各个框架的设计系统\n */\n\nexport function useAntdDesignTokens() {\n  const rootStyles = getComputedStyle(document.documentElement);\n\n  const tokens = reactive({\n    borderRadius: '' as any,\n    colorBgBase: '',\n    colorBgContainer: '',\n    colorBgElevated: '',\n    colorBgLayout: '',\n    colorBgMask: '',\n    colorBorder: '',\n    colorBorderSecondary: '',\n    colorError: '',\n    colorInfo: '',\n    colorPrimary: '',\n    colorSuccess: '',\n    colorTextBase: '',\n    colorWarning: '',\n    zIndexPopupBase: 2000, // 调整基础弹层层级，避免下拉等组件被弹窗或者最大化状态下的表格遮挡\n  });\n\n  const getCssVariableValue = (variable: string, isColor: boolean = true) => {\n    const value = rootStyles.getPropertyValue(variable);\n    return isColor ? `hsl(${value})` : value;\n  };\n\n  watch(\n    () => preferences.theme,\n    () => {\n      tokens.colorPrimary = getCssVariableValue('--primary');\n\n      tokens.colorInfo = getCssVariableValue('--primary');\n\n      tokens.colorError = getCssVariableValue('--destructive');\n\n      tokens.colorWarning = getCssVariableValue('--warning');\n\n      tokens.colorSuccess = getCssVariableValue('--success');\n\n      tokens.colorTextBase = getCssVariableValue('--foreground');\n\n      getCssVariableValue('--primary-foreground');\n\n      tokens.colorBorderSecondary = tokens.colorBorder =\n        getCssVariableValue('--border');\n\n      tokens.colorBgElevated = getCssVariableValue('--popover');\n\n      tokens.colorBgContainer = getCssVariableValue('--card');\n\n      tokens.colorBgBase = getCssVariableValue('--background');\n\n      const radius = Number.parseFloat(getCssVariableValue('--radius', false));\n      // 1rem = 16px\n      tokens.borderRadius = radius * 16;\n\n      tokens.colorBgLayout = getCssVariableValue('--background-deep');\n      tokens.colorBgMask = getCssVariableValue('--overlay');\n    },\n    { immediate: true },\n  );\n\n  return {\n    tokens,\n  };\n}\n\nexport function useNaiveDesignTokens() {\n  const rootStyles = getComputedStyle(document.documentElement);\n\n  const commonTokens = reactive({\n    baseColor: '',\n    bodyColor: '',\n    borderColor: '',\n    borderRadius: '',\n    cardColor: '',\n    dividerColor: '',\n    errorColor: '',\n    errorColorHover: '',\n    errorColorPressed: '',\n    errorColorSuppl: '',\n    invertedColor: '',\n    modalColor: '',\n    popoverColor: '',\n    primaryColor: '',\n    primaryColorHover: '',\n    primaryColorPressed: '',\n    primaryColorSuppl: '',\n    successColor: '',\n    successColorHover: '',\n    successColorPressed: '',\n    successColorSuppl: '',\n    tableColor: '',\n    textColorBase: '',\n    warningColor: '',\n    warningColorHover: '',\n    warningColorPressed: '',\n    warningColorSuppl: '',\n  });\n\n  const getCssVariableValue = (variable: string, isColor: boolean = true) => {\n    const value = rootStyles.getPropertyValue(variable);\n    return isColor ? convertToRgb(`hsl(${value})`) : value;\n  };\n\n  watch(\n    () => preferences.theme,\n    () => {\n      commonTokens.primaryColor = getCssVariableValue('--primary');\n      commonTokens.primaryColorHover = getCssVariableValue('--primary-600');\n      commonTokens.primaryColorPressed = getCssVariableValue('--primary-700');\n      commonTokens.primaryColorSuppl = getCssVariableValue('--primary-800');\n\n      commonTokens.errorColor = getCssVariableValue('--destructive');\n      commonTokens.errorColorHover = getCssVariableValue('--destructive-600');\n      commonTokens.errorColorPressed = getCssVariableValue('--destructive-700');\n      commonTokens.errorColorSuppl = getCssVariableValue('--destructive-800');\n\n      commonTokens.warningColor = getCssVariableValue('--warning');\n      commonTokens.warningColorHover = getCssVariableValue('--warning-600');\n      commonTokens.warningColorPressed = getCssVariableValue('--warning-700');\n      commonTokens.warningColorSuppl = getCssVariableValue('--warning-800');\n\n      commonTokens.successColor = getCssVariableValue('--success');\n      commonTokens.successColorHover = getCssVariableValue('--success-600');\n      commonTokens.successColorPressed = getCssVariableValue('--success-700');\n      commonTokens.successColorSuppl = getCssVariableValue('--success-800');\n\n      commonTokens.textColorBase = getCssVariableValue('--foreground');\n\n      commonTokens.baseColor = getCssVariableValue('--primary-foreground');\n\n      commonTokens.dividerColor = commonTokens.borderColor =\n        getCssVariableValue('--border');\n\n      commonTokens.modalColor = commonTokens.popoverColor =\n        getCssVariableValue('--popover');\n\n      commonTokens.tableColor = commonTokens.cardColor =\n        getCssVariableValue('--card');\n\n      commonTokens.bodyColor = getCssVariableValue('--background');\n      commonTokens.invertedColor = getCssVariableValue('--background-deep');\n\n      commonTokens.borderRadius = getCssVariableValue('--radius', false);\n    },\n    { immediate: true },\n  );\n  return {\n    commonTokens,\n  };\n}\n\nexport function useElementPlusDesignTokens() {\n  const { isDark } = usePreferences();\n  const rootStyles = getComputedStyle(document.documentElement);\n\n  const getCssVariableValueRaw = (variable: string) => {\n    return rootStyles.getPropertyValue(variable);\n  };\n\n  const getCssVariableValue = (variable: string, isColor: boolean = true) => {\n    const value = getCssVariableValueRaw(variable);\n    return isColor ? convertToRgb(`hsl(${value})`) : value;\n  };\n\n  watch(\n    () => preferences.theme,\n    () => {\n      const background = getCssVariableValue('--background');\n      const border = getCssVariableValue('--border');\n      const accent = getCssVariableValue('--accent');\n\n      const variables: Record<string, string> = {\n        '--el-bg-color': background,\n        '--el-bg-color-overlay': getCssVariableValue('--popover'),\n        '--el-bg-color-page': getCssVariableValue('--background-deep'),\n        '--el-border-color': border,\n        '--el-border-color-dark': border,\n        '--el-border-color-extra-light': border,\n        '--el-border-color-hover': accent,\n        '--el-border-color-light': border,\n        '--el-border-color-lighter': border,\n\n        '--el-border-radius-base': getCssVariableValue('--radius', false),\n        '--el-color-danger': getCssVariableValue('--destructive-500'),\n        '--el-color-danger-dark-2': isDark.value\n          ? getCssVariableValue('--destructive-400')\n          : getCssVariableValue('--destructive-600'),\n        '--el-color-danger-light-3': isDark.value\n          ? getCssVariableValue('--destructive-600')\n          : getCssVariableValue('--destructive-400'),\n        '--el-color-danger-light-5': isDark.value\n          ? getCssVariableValue('--destructive-700')\n          : getCssVariableValue('--destructive-300'),\n        '--el-color-danger-light-7': isDark.value\n          ? getCssVariableValue('--destructive-800')\n          : getCssVariableValue('--destructive-200'),\n        '--el-color-danger-light-8': isDark.value\n          ? getCssVariableValue('--destructive-900')\n          : getCssVariableValue('--destructive-100'),\n        '--el-color-danger-light-9': isDark.value\n          ? getCssVariableValue('--destructive-950')\n          : getCssVariableValue('--destructive-50'),\n\n        '--el-color-error': getCssVariableValue('--destructive-500'),\n        '--el-color-error-dark-2': isDark.value\n          ? getCssVariableValue('--destructive-400')\n          : getCssVariableValue('--destructive-600'),\n        '--el-color-error-light-3': isDark.value\n          ? getCssVariableValue('--destructive-600')\n          : getCssVariableValue('--destructive-400'),\n        '--el-color-error-light-5': isDark.value\n          ? getCssVariableValue('--destructive-700')\n          : getCssVariableValue('--destructive-300'),\n        '--el-color-error-light-7': isDark.value\n          ? getCssVariableValue('--destructive-800')\n          : getCssVariableValue('--destructive-200'),\n        '--el-color-error-light-8': isDark.value\n          ? getCssVariableValue('--destructive-900')\n          : getCssVariableValue('--destructive-100'),\n        '--el-color-error-light-9': isDark.value\n          ? getCssVariableValue('--destructive-950')\n          : getCssVariableValue('--destructive-50'),\n\n        '--el-color-info-light-5': border,\n        '--el-color-info-light-8': border,\n        '--el-color-info-light-9': getCssVariableValue('--info'), // getCssVariableValue('--secondary'),\n\n        '--el-color-primary': getCssVariableValue('--primary-500'),\n        '--el-color-primary-dark-2': isDark.value\n          ? getCssVariableValue('--primary-400')\n          : getCssVariableValue('--primary-600'),\n        '--el-color-primary-light-3': isDark.value\n          ? getCssVariableValue('--primary-600')\n          : getCssVariableValue('--primary-400'),\n        '--el-color-primary-light-5': isDark.value\n          ? getCssVariableValue('--primary-700')\n          : getCssVariableValue('--primary-300'),\n        '--el-color-primary-light-7': isDark.value\n          ? getCssVariableValue('--primary-800')\n          : getCssVariableValue('--primary-200'),\n        '--el-color-primary-light-8': isDark.value\n          ? getCssVariableValue('--primary-900')\n          : getCssVariableValue('--primary-100'),\n        '--el-color-primary-light-9': isDark.value\n          ? getCssVariableValue('--primary-950')\n          : getCssVariableValue('--primary-50'),\n\n        '--el-color-success': getCssVariableValue('--success-500'),\n        '--el-color-success-dark-2': isDark.value\n          ? getCssVariableValue('--success-400')\n          : getCssVariableValue('--success-600'),\n        '--el-color-success-light-3': isDark.value\n          ? getCssVariableValue('--success-600')\n          : getCssVariableValue('--success-400'),\n        '--el-color-success-light-5': isDark.value\n          ? getCssVariableValue('--success-700')\n          : getCssVariableValue('--success-300'),\n        '--el-color-success-light-7': isDark.value\n          ? getCssVariableValue('--success-800')\n          : getCssVariableValue('--success-200'),\n        '--el-color-success-light-8': isDark.value\n          ? getCssVariableValue('--success-900')\n          : getCssVariableValue('--success-100'),\n        '--el-color-success-light-9': isDark.value\n          ? getCssVariableValue('--success-950')\n          : getCssVariableValue('--success-50'),\n\n        '--el-color-warning': getCssVariableValue('--warning-500'),\n        '--el-color-warning-dark-2': isDark.value\n          ? getCssVariableValue('--warning-400')\n          : getCssVariableValue('--warning-600'),\n        '--el-color-warning-light-3': isDark.value\n          ? getCssVariableValue('--warning-600')\n          : getCssVariableValue('--warning-400'),\n        '--el-color-warning-light-5': isDark.value\n          ? getCssVariableValue('--warning-700')\n          : getCssVariableValue('--warning-300'),\n        '--el-color-warning-light-7': isDark.value\n          ? getCssVariableValue('--warning-800')\n          : getCssVariableValue('--warning-200'),\n        '--el-color-warning-light-8': isDark.value\n          ? getCssVariableValue('--warning-900')\n          : getCssVariableValue('--warning-100'),\n        '--el-color-warning-light-9': isDark.value\n          ? getCssVariableValue('--warning-950')\n          : getCssVariableValue('--warning-50'),\n\n        '--el-fill-color': getCssVariableValue('--accent'),\n        '--el-fill-color-blank': background,\n        '--el-fill-color-light': getCssVariableValue('--accent'),\n        '--el-fill-color-lighter': getCssVariableValue('--accent-lighter'),\n\n        '--el-fill-color-dark': getCssVariableValue('--accent-dark'),\n        '--el-fill-color-darker': getCssVariableValue('--accent-darker'),\n\n        // 解决ElLoading背景色问题\n        '--el-mask-color': isDark.value\n          ? 'rgba(0,0,0,.8)'\n          : 'rgba(255,255,255,.9)',\n\n        '--el-text-color-primary': getCssVariableValue('--foreground'),\n\n        '--el-text-color-regular': getCssVariableValue('--foreground'),\n      };\n\n      updateCSSVariables(variables, `__vben_design_styles__`);\n    },\n    { immediate: true },\n  );\n}\n"
  },
  {
    "path": "packages/effects/hooks/src/use-hover-toggle.ts",
    "content": "import type { Arrayable, MaybeElementRef } from '@vueuse/core';\n\nimport type { Ref } from 'vue';\n\nimport { computed, effectScope, onUnmounted, ref, unref, watch } from 'vue';\n\nimport { isFunction } from '@vben/utils';\n\nimport { useElementHover } from '@vueuse/core';\n\ninterface HoverDelayOptions {\n  /** 鼠标进入延迟时间 */\n  enterDelay?: (() => number) | number;\n  /** 鼠标离开延迟时间 */\n  leaveDelay?: (() => number) | number;\n}\n\nconst DEFAULT_LEAVE_DELAY = 500; // 鼠标离开延迟时间，默认为 500ms\nconst DEFAULT_ENTER_DELAY = 0; // 鼠标进入延迟时间，默认为 0（立即响应）\n\n/**\n * 监测鼠标是否在元素内部，如果在元素内部则返回 true，否则返回 false\n * @param refElement 所有需要检测的元素。支持单个元素、元素数组或响应式引用的元素数组。如果鼠标在任何一个元素内部都会返回 true\n * @param delay 延迟更新状态的时间，可以是数字或包含进入/离开延迟的配置对象\n * @returns 返回一个数组，第一个元素是一个 ref，表示鼠标是否在元素内部，第二个元素是一个控制器，可以通过 enable 和 disable 方法来控制监听器的启用和禁用\n */\nexport function useHoverToggle(\n  refElement: Arrayable<MaybeElementRef> | Ref<HTMLElement[] | null>,\n  delay: (() => number) | HoverDelayOptions | number = DEFAULT_LEAVE_DELAY,\n) {\n  // 兼容旧版本API\n  const normalizedOptions: HoverDelayOptions =\n    typeof delay === 'number' || isFunction(delay)\n      ? { enterDelay: DEFAULT_ENTER_DELAY, leaveDelay: delay }\n      : {\n          enterDelay: DEFAULT_ENTER_DELAY,\n          leaveDelay: DEFAULT_LEAVE_DELAY,\n          ...delay,\n        };\n\n  const value = ref(false);\n  const enterTimer = ref<ReturnType<typeof setTimeout> | undefined>();\n  const leaveTimer = ref<ReturnType<typeof setTimeout> | undefined>();\n  const hoverScopes = ref<ReturnType<typeof effectScope>[]>([]);\n\n  // 使用计算属性包装 refElement，使其响应式变化\n  const refs = computed(() => {\n    const raw = unref(refElement);\n    if (raw === null) return [];\n    return Array.isArray(raw) ? raw : [raw];\n  });\n  // 存储所有 hover 状态\n  const isHovers = ref<Array<Ref<boolean>>>([]);\n\n  // 更新 hover 监听的函数\n  function updateHovers() {\n    // 停止并清理之前的作用域\n    hoverScopes.value.forEach((scope) => scope.stop());\n    hoverScopes.value = [];\n\n    isHovers.value = refs.value.map((refEle) => {\n      if (!refEle) {\n        return ref(false);\n      }\n      const eleRef = computed(() => {\n        const ele = unref(refEle);\n        return ele instanceof Element ? ele : (ele?.$el as Element);\n      });\n\n      // 为每个元素创建独立的作用域\n      const scope = effectScope();\n      const hoverRef = scope.run(() => useElementHover(eleRef)) || ref(false);\n      hoverScopes.value.push(scope);\n\n      return hoverRef;\n    });\n  }\n\n  // 监听元素数量变化，避免过度执行\n  const elementsCount = computed(() => {\n    const raw = unref(refElement);\n    if (raw === null) return 0;\n    return Array.isArray(raw) ? raw.length : 1;\n  });\n\n  // 初始设置\n  updateHovers();\n\n  // 只在元素数量变化时重新设置监听器\n  const stopWatcher = watch(elementsCount, updateHovers, { deep: false });\n\n  const isOutsideAll = computed(() => isHovers.value.every((v) => !v.value));\n\n  function clearTimers() {\n    if (enterTimer.value) {\n      clearTimeout(enterTimer.value);\n      enterTimer.value = undefined;\n    }\n    if (leaveTimer.value) {\n      clearTimeout(leaveTimer.value);\n      leaveTimer.value = undefined;\n    }\n  }\n\n  function setValueDelay(val: boolean) {\n    clearTimers();\n\n    if (val) {\n      // 鼠标进入\n      const enterDelay = normalizedOptions.enterDelay ?? DEFAULT_ENTER_DELAY;\n      const delayTime = isFunction(enterDelay) ? enterDelay() : enterDelay;\n\n      if (delayTime <= 0) {\n        value.value = true;\n      } else {\n        enterTimer.value = setTimeout(() => {\n          value.value = true;\n          enterTimer.value = undefined;\n        }, delayTime);\n      }\n    } else {\n      // 鼠标离开\n      const leaveDelay = normalizedOptions.leaveDelay ?? DEFAULT_LEAVE_DELAY;\n      const delayTime = isFunction(leaveDelay) ? leaveDelay() : leaveDelay;\n\n      if (delayTime <= 0) {\n        value.value = false;\n      } else {\n        leaveTimer.value = setTimeout(() => {\n          value.value = false;\n          leaveTimer.value = undefined;\n        }, delayTime);\n      }\n    }\n  }\n\n  const hoverWatcher = watch(\n    isOutsideAll,\n    (val) => {\n      setValueDelay(!val);\n    },\n    { immediate: true },\n  );\n\n  const controller = {\n    enable() {\n      hoverWatcher.resume();\n    },\n    disable() {\n      hoverWatcher.pause();\n    },\n  };\n\n  onUnmounted(() => {\n    clearTimers();\n    // 停止监听器\n    stopWatcher();\n    // 停止所有剩余的作用域\n    hoverScopes.value.forEach((scope) => scope.stop());\n  });\n\n  return [value, controller] as [typeof value, typeof controller];\n}\n"
  },
  {
    "path": "packages/effects/hooks/src/use-pagination.ts",
    "content": "import type { Ref } from 'vue';\n\nimport { computed, ref, unref } from 'vue';\n\n/**\n * Paginates an array of items\n * @param list The array to paginate\n * @param pageNo The current page number (1-based)\n * @param pageSize Number of items per page\n * @returns Paginated array slice\n * @throws {Error} If pageNo or pageSize are invalid\n */\nfunction pagination<T = any>(list: T[], pageNo: number, pageSize: number): T[] {\n  if (pageNo < 1) throw new Error('Page number must be positive');\n  if (pageSize < 1) throw new Error('Page size must be positive');\n\n  const offset = (pageNo - 1) * Number(pageSize);\n  const ret =\n    offset + pageSize >= list.length\n      ? list.slice(offset)\n      : list.slice(offset, offset + pageSize);\n  return ret;\n}\n\nexport function usePagination<T = any>(list: Ref<T[]>, pageSize: number) {\n  const currentPage = ref(1);\n  const pageSizeRef = ref(pageSize);\n\n  const totalPages = computed(() =>\n    Math.ceil(unref(list).length / unref(pageSizeRef)),\n  );\n\n  const paginationList = computed(() => {\n    return pagination(unref(list), unref(currentPage), unref(pageSizeRef));\n  });\n\n  const total = computed(() => {\n    return unref(list).length;\n  });\n\n  function setCurrentPage(page: number) {\n    if (page < 1 || page > unref(totalPages)) {\n      throw new Error('Invalid page number');\n    }\n    currentPage.value = page;\n  }\n\n  function setPageSize(pageSize: number) {\n    if (pageSize < 1) {\n      throw new Error('Page size must be positive');\n    }\n    pageSizeRef.value = pageSize;\n    // Reset to first page to prevent invalid state\n    currentPage.value = 1;\n  }\n\n  return { setCurrentPage, total, setPageSize, paginationList };\n}\n"
  },
  {
    "path": "packages/effects/hooks/src/use-refresh.ts",
    "content": "import { useRouter } from 'vue-router';\n\nimport { useTabbarStore } from '@vben/stores';\n\nexport function useRefresh() {\n  const router = useRouter();\n  const tabbarStore = useTabbarStore();\n\n  async function refresh() {\n    await tabbarStore.refresh(router);\n  }\n\n  return {\n    refresh,\n  };\n}\n"
  },
  {
    "path": "packages/effects/hooks/src/use-tabs.ts",
    "content": "import type { ComputedRef } from 'vue';\nimport type { RouteLocationNormalized } from 'vue-router';\n\nimport { useTabbarStore } from '@vben/stores';\nimport { useRoute, useRouter } from 'vue-router';\n\nexport function useTabs() {\n  const router = useRouter();\n  const route = useRoute();\n  const tabbarStore = useTabbarStore();\n\n  async function closeLeftTabs(tab?: RouteLocationNormalized) {\n    await tabbarStore.closeLeftTabs(tab || route);\n  }\n\n  async function closeAllTabs() {\n    await tabbarStore.closeAllTabs(router);\n  }\n\n  async function closeRightTabs(tab?: RouteLocationNormalized) {\n    await tabbarStore.closeRightTabs(tab || route);\n  }\n\n  async function closeOtherTabs(tab?: RouteLocationNormalized) {\n    await tabbarStore.closeOtherTabs(tab || route);\n  }\n\n  async function closeCurrentTab(tab?: RouteLocationNormalized) {\n    await tabbarStore.closeTab(tab || route, router);\n  }\n\n  async function pinTab(tab?: RouteLocationNormalized) {\n    await tabbarStore.pinTab(tab || route);\n  }\n\n  async function unpinTab(tab?: RouteLocationNormalized) {\n    await tabbarStore.unpinTab(tab || route);\n  }\n\n  async function toggleTabPin(tab?: RouteLocationNormalized) {\n    await tabbarStore.toggleTabPin(tab || route);\n  }\n\n  async function refreshTab(name?: string) {\n    await tabbarStore.refresh(name || router);\n  }\n\n  async function openTabInNewWindow(tab?: RouteLocationNormalized) {\n    await tabbarStore.openTabInNewWindow(tab || route);\n  }\n\n  async function closeTabByKey(key: string) {\n    await tabbarStore.closeTabByKey(key, router);\n  }\n\n  /**\n   * 设置当前标签页的标题\n   *\n   * @description 支持设置静态标题字符串或动态计算标题\n   * @description 动态标题会在每次渲染时重新计算,适用于多语言或状态相关的标题\n   *\n   * @param title - 标题内容\n   *   - 静态标题: 直接传入字符串\n   *   - 动态标题: 传入 ComputedRef\n   *\n   * @example\n   * // 静态标题\n   * setTabTitle('标签页')\n   *\n   * // 动态标题(多语言)\n   * setTabTitle(computed(() => t('page.title')))\n   */\n  async function setTabTitle(title: ComputedRef<string> | string) {\n    tabbarStore.setUpdateTime();\n    await tabbarStore.setTabTitle(route, title);\n  }\n\n  async function resetTabTitle() {\n    tabbarStore.setUpdateTime();\n    await tabbarStore.resetTabTitle(route);\n  }\n\n  /**\n   * 获取操作是否禁用\n   * @param tab\n   */\n  function getTabDisableState(tab: RouteLocationNormalized = route) {\n    const tabs = tabbarStore.getTabs;\n    const affixTabs = tabbarStore.affixTabs;\n    const index = tabs.findIndex((item) => item.path === tab.path);\n\n    const disabled = tabs.length <= 1;\n\n    const { meta } = tab;\n    const affixTab = meta?.affixTab ?? false;\n    const isCurrentTab = route.path === tab.path;\n\n    // 当前处于最左侧或者减去固定标签页的数量等于0\n    const disabledCloseLeft =\n      index === 0 || index - affixTabs.length <= 0 || !isCurrentTab;\n\n    const disabledCloseRight = !isCurrentTab || index === tabs.length - 1;\n\n    const disabledCloseOther =\n      disabled || !isCurrentTab || tabs.length - affixTabs.length <= 1;\n    return {\n      disabledCloseAll: disabled,\n      disabledCloseCurrent: !!affixTab || disabled,\n      disabledCloseLeft,\n      disabledCloseOther,\n      disabledCloseRight,\n      disabledRefresh: !isCurrentTab,\n    };\n  }\n\n  return {\n    closeAllTabs,\n    closeCurrentTab,\n    closeLeftTabs,\n    closeOtherTabs,\n    closeRightTabs,\n    closeTabByKey,\n    getTabDisableState,\n    openTabInNewWindow,\n    pinTab,\n    refreshTab,\n    resetTabTitle,\n    setTabTitle,\n    toggleTabPin,\n    unpinTab,\n  };\n}\n"
  },
  {
    "path": "packages/effects/hooks/src/use-watermark.ts",
    "content": "import type { Watermark, WatermarkOptions } from 'watermark-js-plus';\n\nimport { nextTick, onUnmounted, readonly, ref } from 'vue';\n\nconst watermark = ref<Watermark>();\nconst unmountedHooked = ref<boolean>(false);\nconst cachedOptions = ref<Partial<WatermarkOptions>>({\n  advancedStyle: {\n    colorStops: [\n      {\n        color: 'gray',\n        offset: 0,\n      },\n      {\n        color: 'gray',\n        offset: 1,\n      },\n    ],\n    type: 'linear',\n  },\n  // fontSize: '20px',\n  content: '',\n  contentType: 'multi-line-text',\n  globalAlpha: 0.25,\n  gridLayoutOptions: {\n    cols: 2,\n    gap: [20, 20],\n    matrix: [\n      [1, 0],\n      [0, 1],\n    ],\n    rows: 2,\n  },\n  height: 200,\n  layout: 'grid',\n  rotate: 30,\n  width: 160,\n});\n\nexport function useWatermark() {\n  async function initWatermark(options: Partial<WatermarkOptions>) {\n    const { Watermark } = await import('watermark-js-plus');\n\n    cachedOptions.value = {\n      ...cachedOptions.value,\n      ...options,\n    };\n    watermark.value = new Watermark(cachedOptions.value);\n    await watermark.value?.create();\n  }\n\n  async function updateWatermark(options: Partial<WatermarkOptions>) {\n    if (watermark.value) {\n      await nextTick();\n      await watermark.value?.changeOptions({\n        ...cachedOptions.value,\n        ...options,\n      });\n    } else {\n      await initWatermark(options);\n    }\n  }\n\n  function destroyWatermark() {\n    if (watermark.value) {\n      watermark.value.destroy();\n      watermark.value = undefined;\n    }\n  }\n\n  // 只在第一次调用时注册卸载钩子，防止重复注册以致于在路由切换时销毁了水印\n  if (!unmountedHooked.value) {\n    unmountedHooked.value = true;\n    onUnmounted(() => {\n      destroyWatermark();\n    });\n  }\n\n  return {\n    destroyWatermark,\n    updateWatermark,\n    watermark: readonly(watermark),\n  };\n}\n"
  },
  {
    "path": "packages/effects/hooks/tsconfig.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/tsconfig\",\n  \"extends\": \"@vben/tsconfig/web.json\",\n  \"compilerOptions\": {\n    \"types\": [\"vite/client\", \"@vben/types/global\"]\n  },\n  \"include\": [\"src\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "packages/effects/layouts/package.json",
    "content": "{\n  \"name\": \"@vben/layouts\",\n  \"version\": \"5.5.9\",\n  \"homepage\": \"https://github.com/vbenjs/vue-vben-admin\",\n  \"bugs\": \"https://github.com/vbenjs/vue-vben-admin/issues\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/vbenjs/vue-vben-admin.git\",\n    \"directory\": \"packages/effects/layouts\"\n  },\n  \"license\": \"MIT\",\n  \"type\": \"module\",\n  \"sideEffects\": [\n    \"**/*.css\"\n  ],\n  \"exports\": {\n    \".\": {\n      \"types\": \"./src/index.ts\",\n      \"default\": \"./src/index.ts\"\n    }\n  },\n  \"dependencies\": {\n    \"@vben-core/composables\": \"workspace:*\",\n    \"@vben-core/form-ui\": \"workspace:*\",\n    \"@vben-core/layout-ui\": \"workspace:*\",\n    \"@vben-core/menu-ui\": \"workspace:*\",\n    \"@vben-core/popup-ui\": \"workspace:*\",\n    \"@vben-core/shadcn-ui\": \"workspace:*\",\n    \"@vben-core/shared\": \"workspace:*\",\n    \"@vben-core/tabs-ui\": \"workspace:*\",\n    \"@vben/constants\": \"workspace:*\",\n    \"@vben/hooks\": \"workspace:*\",\n    \"@vben/icons\": \"workspace:*\",\n    \"@vben/locales\": \"workspace:*\",\n    \"@vben/preferences\": \"workspace:*\",\n    \"@vben/stores\": \"workspace:*\",\n    \"@vben/types\": \"workspace:*\",\n    \"@vben/utils\": \"workspace:*\",\n    \"@vueuse/core\": \"catalog:\",\n    \"vue\": \"catalog:\",\n    \"vue-router\": \"catalog:\"\n  }\n}\n"
  },
  {
    "path": "packages/effects/layouts/src/authentication/authentication.vue",
    "content": "<script setup lang=\"ts\">\nimport type { ToolbarType } from './types';\n\nimport { preferences, usePreferences } from '@vben/preferences';\n\nimport { Copyright } from '../basic/copyright';\nimport AuthenticationFormView from './form.vue';\nimport SloganIcon from './icons/slogan.vue';\nimport Toolbar from './toolbar.vue';\n\ninterface Props {\n  appName?: string;\n  logo?: string;\n  pageTitle?: string;\n  pageDescription?: string;\n  sloganImage?: string;\n  toolbar?: boolean;\n  copyright?: boolean;\n  toolbarList?: ToolbarType[];\n  clickLogo?: () => void;\n}\n\nwithDefaults(defineProps<Props>(), {\n  appName: '',\n  copyright: true,\n  logo: '',\n  pageDescription: '',\n  pageTitle: '',\n  sloganImage: '',\n  toolbar: true,\n  toolbarList: () => ['color', 'language', 'layout', 'theme'],\n  clickLogo: () => {},\n});\n\nconst { authPanelCenter, authPanelLeft, authPanelRight, isDark } =\n  usePreferences();\n</script>\n\n<template>\n  <div\n    :class=\"[isDark ? 'dark' : '']\"\n    class=\"flex min-h-full flex-1 select-none overflow-x-hidden\"\n  >\n    <template v-if=\"toolbar\">\n      <slot name=\"toolbar\">\n        <Toolbar :toolbar-list=\"toolbarList\" />\n      </slot>\n    </template>\n    <!-- 左侧认证面板 -->\n    <AuthenticationFormView\n      v-if=\"authPanelLeft\"\n      class=\"min-h-full w-2/5 flex-1\"\n      transition-name=\"slide-left\"\n    >\n      <template v-if=\"copyright\" #copyright>\n        <slot name=\"copyright\">\n          <Copyright\n            v-if=\"preferences.copyright.enable\"\n            v-bind=\"preferences.copyright\"\n          />\n        </slot>\n      </template>\n    </AuthenticationFormView>\n\n    <slot name=\"logo\">\n      <!-- 头部 Logo 和应用名称 -->\n      <div\n        v-if=\"logo || appName\"\n        class=\"absolute left-0 top-0 z-10 flex flex-1\"\n        @click=\"clickLogo\"\n      >\n        <div\n          class=\"text-foreground lg:text-foreground ml-4 mt-4 flex flex-1 items-center sm:left-6 sm:top-6\"\n        >\n          <img v-if=\"logo\" :alt=\"appName\" :src=\"logo\" class=\"mr-2\" width=\"42\" />\n          <p v-if=\"appName\" class=\"m-0 text-xl font-medium\">\n            {{ appName }}\n          </p>\n        </div>\n      </div>\n    </slot>\n\n    <!-- 系统介绍 -->\n    <div v-if=\"!authPanelCenter\" class=\"relative hidden w-0 flex-1 lg:block\">\n      <div\n        class=\"bg-background-deep absolute inset-0 h-full w-full dark:bg-[#070709]\"\n      >\n        <div class=\"login-background absolute left-0 top-0 size-full\"></div>\n        <div class=\"flex-col-center -enter-x mr-20 h-full\">\n          <template v-if=\"sloganImage\">\n            <img\n              :alt=\"appName\"\n              :src=\"sloganImage\"\n              class=\"animate-float h-64 w-2/5\"\n            />\n          </template>\n          <SloganIcon v-else :alt=\"appName\" class=\"animate-float h-64 w-2/5\" />\n          <div class=\"text-1xl text-foreground mt-6 font-sans lg:text-2xl\">\n            {{ pageTitle }}\n          </div>\n          <div class=\"dark:text-muted-foreground mt-2\">\n            {{ pageDescription }}\n          </div>\n        </div>\n      </div>\n    </div>\n\n    <!-- 中心认证面板 -->\n    <div v-if=\"authPanelCenter\" class=\"flex-center relative w-full\">\n      <div class=\"login-background absolute left-0 top-0 size-full\"></div>\n      <AuthenticationFormView\n        class=\"md:bg-background shadow-primary/5 shadow-float w-full rounded-3xl pb-20 md:w-2/3 lg:w-1/2 xl:w-[36%]\"\n      >\n        <template v-if=\"copyright\" #copyright>\n          <slot name=\"copyright\">\n            <Copyright\n              v-if=\"preferences.copyright.enable\"\n              v-bind=\"preferences.copyright\"\n            />\n          </slot>\n        </template>\n      </AuthenticationFormView>\n    </div>\n\n    <!-- 右侧认证面板 -->\n    <AuthenticationFormView\n      v-if=\"authPanelRight\"\n      class=\"min-h-full w-[34%] flex-1\"\n    >\n      <template v-if=\"copyright\" #copyright>\n        <slot name=\"copyright\">\n          <Copyright\n            v-if=\"preferences.copyright.enable\"\n            v-bind=\"preferences.copyright\"\n          />\n        </slot>\n      </template>\n    </AuthenticationFormView>\n  </div>\n</template>\n\n<style scoped>\n.login-background {\n  background: linear-gradient(\n    154deg,\n    #07070915 30%,\n    hsl(var(--primary) / 30%) 48%,\n    #07070915 64%\n  );\n  filter: blur(100px);\n}\n\n.dark {\n  .login-background {\n    background: linear-gradient(\n      154deg,\n      #07070915 30%,\n      hsl(var(--primary) / 20%) 48%,\n      #07070915 64%\n    );\n    filter: blur(100px);\n  }\n}\n</style>\n"
  },
  {
    "path": "packages/effects/layouts/src/authentication/form.vue",
    "content": "<script setup lang=\"ts\">\ndefineOptions({\n  name: 'AuthenticationFormView',\n});\n</script>\n\n<template>\n  <div\n    class=\"flex-col-center dark:bg-background-deep bg-background relative px-6 py-10 lg:flex-initial lg:px-8\"\n  >\n    <slot></slot>\n    <!-- Router View with Transition and KeepAlive -->\n    <RouterView v-slot=\"{ Component, route }\">\n      <Transition appear mode=\"out-in\" name=\"slide-right\">\n        <KeepAlive :include=\"['Login']\">\n          <component\n            :is=\"Component\"\n            :key=\"route.fullPath\"\n            class=\"enter-x mt-6 w-full sm:mx-auto md:max-w-md\"\n          />\n        </KeepAlive>\n      </Transition>\n    </RouterView>\n\n    <!-- Footer Copyright -->\n\n    <div\n      class=\"text-muted-foreground absolute bottom-3 flex text-center text-xs\"\n    >\n      <slot name=\"copyright\"> </slot>\n    </div>\n  </div>\n</template>\n"
  },
  {
    "path": "packages/effects/layouts/src/authentication/icons/slogan.vue",
    "content": "<template>\n  <svg\n    enable-background=\"new 0 0 800 800\"\n    version=\"1.1\"\n    viewBox=\"0 0 800 800\"\n    x=\"0px\"\n    xml:space=\"preserve\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n    xmlns:xlink=\"http://www.w3.org/1999/xlink\"\n    y=\"0px\"\n  >\n    <filter\n      id=\"filter-95\"\n      filterUnits=\"objectBoundingBox\"\n      height=\"124.9%\"\n      width=\"114.5%\"\n      x=\"-7.3%\"\n      y=\"-12.5%\"\n    >\n      <feGaussianBlur in=\"SourceGraphic\" stdDeviation=\"4.21434086\" />\n    </filter>\n    <filter\n      id=\"filter-79\"\n      filterUnits=\"objectBoundingBox\"\n      height=\"101.4%\"\n      width=\"100.8%\"\n      x=\"-0.4%\"\n      y=\"-0.7%\"\n    >\n      <feOffset dx=\"0\" dy=\"-1\" in=\"SourceAlpha\" result=\"shadowOffsetInner1\" />\n      <feComposite\n        in=\"shadowOffsetInner1\"\n        in2=\"SourceAlpha\"\n        k2=\"-1\"\n        k3=\"1\"\n        operator=\"arithmetic\"\n        result=\"shadowInnerInner1\"\n      />\n      <feColorMatrix\n        in=\"shadowInnerInner1\"\n        type=\"matrix\"\n        values=\"0 0 0 0 1   0 0 0 0 1   0 0 0 0 1  0 0 0 0.498880026 0\"\n      />\n    </filter>\n    <filter\n      id=\"filter-71\"\n      filterUnits=\"objectBoundingBox\"\n      height=\"142.5%\"\n      width=\"120.1%\"\n      x=\"-10.1%\"\n      y=\"-21.3%\"\n    >\n      <feGaussianBlur in=\"SourceGraphic\" stdDeviation=\"4.21434086\" />\n    </filter>\n    <filter\n      id=\"filter-59\"\n      filterUnits=\"objectBoundingBox\"\n      height=\"181.0%\"\n      width=\"133.4%\"\n      x=\"-16.7%\"\n      y=\"-40.5%\"\n    >\n      <feGaussianBlur in=\"SourceGraphic\" stdDeviation=\"4.85695311\" />\n    </filter>\n    <filter\n      id=\"filter-56\"\n      filterUnits=\"objectBoundingBox\"\n      height=\"181.0%\"\n      width=\"133.4%\"\n      x=\"-16.7%\"\n      y=\"-40.5%\"\n    >\n      <feGaussianBlur in=\"SourceGraphic\" stdDeviation=\"2.91417187\" />\n    </filter>\n    <filter\n      id=\"filter-53\"\n      filterUnits=\"objectBoundingBox\"\n      height=\"100.9%\"\n      width=\"100.5%\"\n      x=\"-0.2%\"\n      y=\"-0.4%\"\n    >\n      <feOffset dx=\"0\" dy=\"-1\" in=\"SourceAlpha\" result=\"shadowOffsetInner1\" />\n      <feComposite\n        in=\"shadowOffsetInner1\"\n        in2=\"SourceAlpha\"\n        k2=\"-1\"\n        k3=\"1\"\n        operator=\"arithmetic\"\n        result=\"shadowInnerInner1\"\n      />\n      <feColorMatrix\n        in=\"shadowInnerInner1\"\n        type=\"matrix\"\n        values=\"0 0 0 0 1   0 0 0 0 1   0 0 0 0 1  0 0 0 0.498880026 0\"\n      />\n    </filter>\n    <filter\n      id=\"filter-50\"\n      filterUnits=\"objectBoundingBox\"\n      height=\"241.5%\"\n      width=\"643.8%\"\n      x=\"-271.9%\"\n      y=\"-70.7%\"\n    >\n      <feGaussianBlur in=\"SourceGraphic\" stdDeviation=\"16.2954513\" />\n    </filter>\n    <filter\n      id=\"filter-5\"\n      filterUnits=\"objectBoundingBox\"\n      height=\"100.8%\"\n      width=\"100.4%\"\n      x=\"-0.2%\"\n      y=\"-0.4%\"\n    >\n      <feOffset dx=\"0\" dy=\"-3\" in=\"SourceAlpha\" result=\"shadowOffsetInner1\" />\n      <feComposite\n        in=\"shadowOffsetInner1\"\n        in2=\"SourceAlpha\"\n        k2=\"-1\"\n        k3=\"1\"\n        operator=\"arithmetic\"\n        result=\"shadowInnerInner1\"\n      />\n      <feColorMatrix\n        in=\"shadowInnerInner1\"\n        type=\"matrix\"\n        values=\"0 0 0 0 1   0 0 0 0 1   0 0 0 0 1  0 0 0 0.498880026 0\"\n      />\n    </filter>\n    <filter\n      id=\"filter-46\"\n      filterUnits=\"objectBoundingBox\"\n      height=\"100.7%\"\n      width=\"100.4%\"\n      x=\"-0.2%\"\n      y=\"-0.3%\"\n    >\n      <feOffset dx=\"0\" dy=\"-1\" in=\"SourceAlpha\" result=\"shadowOffsetInner1\" />\n      <feComposite\n        in=\"shadowOffsetInner1\"\n        in2=\"SourceAlpha\"\n        k2=\"-1\"\n        k3=\"1\"\n        operator=\"arithmetic\"\n        result=\"shadowInnerInner1\"\n      />\n      <feColorMatrix\n        in=\"shadowInnerInner1\"\n        type=\"matrix\"\n        values=\"0 0 0 0 1   0 0 0 0 1   0 0 0 0 1  0 0 0 0.498880026 0\"\n      />\n    </filter>\n    <filter\n      id=\"filter-43\"\n      filterUnits=\"objectBoundingBox\"\n      height=\"241.5%\"\n      width=\"643.8%\"\n      x=\"-271.9%\"\n      y=\"-70.7%\"\n    >\n      <feGaussianBlur in=\"SourceGraphic\" stdDeviation=\"20.3693141\" />\n    </filter>\n    <filter\n      id=\"filter-39\"\n      filterUnits=\"objectBoundingBox\"\n      height=\"113.3%\"\n      width=\"107.0%\"\n      x=\"-3.5%\"\n      y=\"-6.7%\"\n    >\n      <feGaussianBlur in=\"SourceGraphic\" stdDeviation=\"3.51195071\" />\n    </filter>\n    <filter\n      id=\"filter-34\"\n      filterUnits=\"objectBoundingBox\"\n      height=\"103.6%\"\n      width=\"102.1%\"\n      x=\"-1.1%\"\n      y=\"-1.8%\"\n    >\n      <feOffset dx=\"0\" dy=\"-1\" in=\"SourceAlpha\" result=\"shadowOffsetInner1\" />\n      <feComposite\n        in=\"shadowOffsetInner1\"\n        in2=\"SourceAlpha\"\n        k2=\"-1\"\n        k3=\"1\"\n        operator=\"arithmetic\"\n        result=\"shadowInnerInner1\"\n      />\n      <feColorMatrix\n        in=\"shadowInnerInner1\"\n        type=\"matrix\"\n        values=\"0 0 0 0 1   0 0 0 0 1   0 0 0 0 1  0 0 0 0.833806818 0\"\n      />\n    </filter>\n    <filter\n      id=\"filter-29\"\n      filterUnits=\"objectBoundingBox\"\n      height=\"103.6%\"\n      width=\"102.1%\"\n      x=\"-1.1%\"\n      y=\"-1.8%\"\n    >\n      <feOffset dx=\"0\" dy=\"-1\" in=\"SourceAlpha\" result=\"shadowOffsetInner1\" />\n      <feComposite\n        in=\"shadowOffsetInner1\"\n        in2=\"SourceAlpha\"\n        k2=\"-1\"\n        k3=\"1\"\n        operator=\"arithmetic\"\n        result=\"shadowInnerInner1\"\n      />\n      <feColorMatrix\n        in=\"shadowInnerInner1\"\n        type=\"matrix\"\n        values=\"0 0 0 0 1   0 0 0 0 1   0 0 0 0 1  0 0 0 0.833806818 0\"\n      />\n    </filter>\n    <filter\n      id=\"filter-24\"\n      filterUnits=\"objectBoundingBox\"\n      height=\"103.6%\"\n      width=\"102.1%\"\n      x=\"-1.1%\"\n      y=\"-1.8%\"\n    >\n      <feOffset dx=\"0\" dy=\"-1\" in=\"SourceAlpha\" result=\"shadowOffsetInner1\" />\n      <feComposite\n        in=\"shadowOffsetInner1\"\n        in2=\"SourceAlpha\"\n        k2=\"-1\"\n        k3=\"1\"\n        operator=\"arithmetic\"\n        result=\"shadowInnerInner1\"\n      />\n      <feColorMatrix\n        in=\"shadowInnerInner1\"\n        type=\"matrix\"\n        values=\"0 0 0 0 1   0 0 0 0 1   0 0 0 0 1  0 0 0 0.833806818 0\"\n      />\n    </filter>\n    <filter\n      id=\"filter-20\"\n      filterUnits=\"objectBoundingBox\"\n      height=\"102.3%\"\n      width=\"101.3%\"\n      x=\"-0.7%\"\n      y=\"-1.1%\"\n    >\n      <feOffset dx=\"0\" dy=\"-3\" in=\"SourceAlpha\" result=\"shadowOffsetInner1\" />\n      <feComposite\n        in=\"shadowOffsetInner1\"\n        in2=\"SourceAlpha\"\n        k2=\"-1\"\n        k3=\"1\"\n        operator=\"arithmetic\"\n        result=\"shadowInnerInner1\"\n      />\n      <feColorMatrix\n        in=\"shadowInnerInner1\"\n        type=\"matrix\"\n        values=\"0 0 0 0 1   0 0 0 0 1   0 0 0 0 1  0 0 0 0.498880026 0\"\n      />\n    </filter>\n    <filter\n      id=\"filter-14\"\n      filterUnits=\"objectBoundingBox\"\n      height=\"103.6%\"\n      width=\"102.1%\"\n      x=\"-1.0%\"\n      y=\"-1.8%\"\n    >\n      <feOffset dx=\"0\" dy=\"-1\" in=\"SourceAlpha\" result=\"shadowOffsetInner1\" />\n      <feComposite\n        in=\"shadowOffsetInner1\"\n        in2=\"SourceAlpha\"\n        k2=\"-1\"\n        k3=\"1\"\n        operator=\"arithmetic\"\n        result=\"shadowInnerInner1\"\n      />\n      <feColorMatrix\n        in=\"shadowInnerInner1\"\n        type=\"matrix\"\n        values=\"0 0 0 0 1   0 0 0 0 1   0 0 0 0 1  0 0 0 0.833806818 0\"\n      />\n    </filter>\n    <filter\n      id=\"filter-109\"\n      filterUnits=\"objectBoundingBox\"\n      height=\"102.5%\"\n      width=\"101.4%\"\n      x=\"-0.7%\"\n      y=\"-1.2%\"\n    >\n      <feOffset dx=\"0\" dy=\"-1\" in=\"SourceAlpha\" result=\"shadowOffsetInner1\" />\n      <feComposite\n        in=\"shadowOffsetInner1\"\n        in2=\"SourceAlpha\"\n        k2=\"-1\"\n        k3=\"1\"\n        operator=\"arithmetic\"\n        result=\"shadowInnerInner1\"\n      />\n      <feColorMatrix\n        in=\"shadowInnerInner1\"\n        type=\"matrix\"\n        values=\"0 0 0 0 1   0 0 0 0 1   0 0 0 0 1  0 0 0 0.833806818 0\"\n      />\n    </filter>\n    <filter\n      id=\"filter-106\"\n      filterUnits=\"objectBoundingBox\"\n      height=\"149.1%\"\n      width=\"128.6%\"\n      x=\"-14.3%\"\n      y=\"-24.5%\"\n    >\n      <feGaussianBlur in=\"SourceGraphic\" stdDeviation=\"4.71559623\" />\n    </filter>\n    <filter\n      id=\"filter-100\"\n      filterUnits=\"objectBoundingBox\"\n      height=\"133.5%\"\n      width=\"119.4%\"\n      x=\"-9.7%\"\n      y=\"-16.8%\"\n    >\n      <feGaussianBlur in=\"SourceGraphic\" stdDeviation=\"4.21434086\" />\n    </filter>\n    <filter\n      id=\"filter-10\"\n      filterUnits=\"objectBoundingBox\"\n      height=\"100.7%\"\n      width=\"100.4%\"\n      x=\"-0.2%\"\n      y=\"-0.3%\"\n    >\n      <feOffset dx=\"0\" dy=\"-3\" in=\"SourceAlpha\" result=\"shadowOffsetInner1\" />\n      <feComposite\n        in=\"shadowOffsetInner1\"\n        in2=\"SourceAlpha\"\n        k2=\"-1\"\n        k3=\"1\"\n        operator=\"arithmetic\"\n        result=\"shadowInnerInner1\"\n      />\n      <feColorMatrix\n        in=\"shadowInnerInner1\"\n        type=\"matrix\"\n        values=\"0 0 0 0 1   0 0 0 0 1   0 0 0 0 1  0 0 0 0.498880026 0\"\n      />\n    </filter>\n    <title>87667-SVG8</title>\n    <g>\n      <g id=\"Group备份\" transform=\"translate(40.000000, 350.000000)\">\n        <linearGradient\n          id=\"Shape_1_\"\n          gradientTransform=\"matrix(352.3498 0 0 -229.3688 -21376.5684 335815.1875)\"\n          gradientUnits=\"userSpaceOnUse\"\n          x1=\"62.7934\"\n          x2=\"61.7099\"\n          y1=\"1462.9332\"\n          y2=\"1462.535\"\n        >\n          <stop offset=\"0\" style=\"stop-color: #0043fb\" />\n          <stop offset=\"1\" style=\"stop-color: #3684ff\" />\n        </linearGradient>\n        <path\n          id=\"Shape\"\n          d=\"M368,428.9c1.4,0.8,3.2,0.8,4.6,0l344.4-198.3c2.8-1.6,3.4-5.4,1.2-7.8l-16.3-22.7\n\t\t\tL370.3,395.7v31.6l-3.2,1L368,428.9z\"\n          fill=\"url(#Shape_1_)\"\n        />\n\n        <linearGradient\n          id=\"SVGID_1_\"\n          gradientTransform=\"matrix(369.5781 0 0 -241.7468 -22811.1367 353968.8438)\"\n          gradientUnits=\"userSpaceOnUse\"\n          x1=\"63.31\"\n          x2=\"61.5984\"\n          y1=\"1462.5217\"\n          y2=\"1463.1935\"\n        >\n          <stop offset=\"5.400000e-03\" style=\"stop-color: #00c6fb\" />\n          <stop offset=\"1\" style=\"stop-color: #005bea\" />\n        </linearGradient>\n        <path\n          d=\"M370.3,427.3c0,1.3-1.4,2.1-2.5,1.4L3.2,217.3c-2.8-1.6-3.4-5.4-1.2-7.8l17.8-22.3l350.5,208.4\n\t\t\tV427.3L370.3,427.3z\"\n          fill=\"url(#SVGID_1_)\"\n        />\n        <g>\n          <g>\n            <linearGradient\n              id=\"path-4_1_\"\n              gradientTransform=\"matrix(689.8593 0 0 -397.2563 -42654.3008 582014.8125)\"\n              gradientUnits=\"userSpaceOnUse\"\n              x1=\"62.5423\"\n              x2=\"62.1013\"\n              y1=\"1464.4305\"\n              y2=\"1464.7888\"\n            >\n              <stop offset=\"0\" style=\"stop-color: #4587ef\" />\n              <stop offset=\"1\" style=\"stop-color: #00209e\" />\n            </linearGradient>\n            <path\n              id=\"path-4\"\n              clip-rule=\"evenodd\"\n              d=\"M355.7,393.8L18,198.7\n\t\t\t\t\tc-4-2.3-4-8.1,0-10.4L337.8,3.6c7.4-4.3,16.5-4.3,24,0l340.1,196.5c4,2.3,4,8.1,0,10.4L384.4,393.8\n\t\t\t\t\tC375.5,398.9,364.6,398.9,355.7,393.8z\"\n              fill=\"url(#path-4_1_)\"\n              fill-rule=\"evenodd\"\n            />\n          </g>\n          <g filter=\"url(#filter-5)\">\n            <path\n              clip-rule=\"evenodd\"\n              d=\"M355.7,393.8L18,198.7c-4-2.3-4-8.1,0-10.4L337.8,3.6c7.4-4.3,16.5-4.3,24,0\n\t\t\t\t\tl340.1,196.5c4,2.3,4,8.1,0,10.4L384.4,393.8C375.5,398.9,364.6,398.9,355.7,393.8z\"\n              fill-rule=\"evenodd\"\n            />\n          </g>\n        </g>\n        <path\n          id=\"Shape备份\"\n          d=\"M356.1,376L49.6,199c-3.6-2.1-3.6-7.4,0-9.5\n\t\t\tL339.8,21.9c6.7-3.9,15-3.9,21.7,0l308.6,178.3c3.6,2.1,3.6,7.4,0,9.5L382.1,376C374.1,380.7,364.1,380.7,356.1,376z\"\n          fill=\"none\"\n          stroke=\"#FFFFFF\"\n          stroke-width=\"0.9511\"\n        />\n      </g>\n      <g id=\"Group\" transform=\"translate(0.000000, 259.000000)\">\n        <linearGradient\n          id=\"SVGID_2_\"\n          gradientTransform=\"matrix(391.4998 0 0 -254.8542 -39423.0664 396432.7188)\"\n          gradientUnits=\"userSpaceOnUse\"\n          x1=\"102.8224\"\n          x2=\"101.7388\"\n          y1=\"1554.3767\"\n          y2=\"1553.9784\"\n        >\n          <stop offset=\"0\" style=\"stop-color: #0043fb\" />\n          <stop offset=\"1\" style=\"stop-color: #3684ff\" />\n        </linearGradient>\n        <path\n          d=\"M408.9,476.5c1.6,0.9,3.5,0.9,5.1,0l382.6-220.3c3.1-1.8,3.8-6,1.3-8.7l-18.1-25.2L411.4,439.6\n\t\t\tv35.2l-3.5,1.1L408.9,476.5z\"\n          fill=\"url(#SVGID_2_)\"\n        />\n\n        <linearGradient\n          id=\"SVGID_3_\"\n          gradientTransform=\"matrix(410.6423 0 0 -268.6075 -41782.7344 417855)\"\n          gradientUnits=\"userSpaceOnUse\"\n          x1=\"103.3376\"\n          x2=\"101.626\"\n          y1=\"1553.9424\"\n          y2=\"1554.6141\"\n        >\n          <stop offset=\"5.400000e-03\" style=\"stop-color: #00c6fb\" />\n          <stop offset=\"1\" style=\"stop-color: #005bea\" />\n        </linearGradient>\n        <path\n          d=\"M411.4,474.8c0,1.4-1.5,2.3-2.8,1.6L3.6,241.5c-3.1-1.8-3.8-6-1.3-8.7L22,208l389.4,231.6V474.8\n\t\t\tL411.4,474.8z\"\n          fill=\"url(#SVGID_3_)\"\n        />\n        <g>\n          <g>\n            <linearGradient\n              id=\"path-9_1_\"\n              gradientTransform=\"matrix(766.5103 0 0 -441.3958 -78065.4141 686963.125)\"\n              gradientUnits=\"userSpaceOnUse\"\n              x1=\"102.5571\"\n              x2=\"102.1161\"\n              y1=\"1555.6865\"\n              y2=\"1556.0448\"\n            >\n              <stop offset=\"0\" style=\"stop-color: #4587ef\" />\n              <stop offset=\"1\" style=\"stop-color: #00209e\" />\n            </linearGradient>\n            <path\n              id=\"path-9\"\n              clip-rule=\"evenodd\"\n              d=\"M395.2,437.6L20,220.8\n\t\t\t\t\tc-4.5-2.6-4.5-9,0-11.6L375.3,4c8.2-4.8,18.4-4.8,26.6,0l377.9,218.3c4.5,2.6,4.5,9,0,11.6L427.1,437.6\n\t\t\t\t\tC417.2,443.3,405.1,443.3,395.2,437.6z\"\n              fill=\"url(#path-9_1_)\"\n              fill-rule=\"evenodd\"\n            />\n          </g>\n          <g filter=\"url(#filter-10)\">\n            <path\n              clip-rule=\"evenodd\"\n              d=\"M395.2,437.6L20,220.8c-4.5-2.6-4.5-9,0-11.6L375.3,4c8.2-4.8,18.4-4.8,26.6,0\n\t\t\t\t\tl377.9,218.3c4.5,2.6,4.5,9,0,11.6L427.1,437.6C417.2,443.3,405.1,443.3,395.2,437.6z\"\n              fill-rule=\"evenodd\"\n            />\n          </g>\n        </g>\n        <path\n          d=\"M395.6,417.8L55.2,221.1c-4.1-2.3-4.1-8.2,0-10.5L377.6,24.4\n\t\t\tc7.5-4.3,16.7-4.3,24.2,0l342.9,198.1c4.1,2.3,4.1,8.2,0,10.5L424.6,417.8C415.6,423,404.6,423,395.6,417.8z\"\n          fill=\"none\"\n          stroke=\"#FFFFFF\"\n          stroke-width=\"1.0568\"\n        />\n      </g>\n      <g id=\"编组备份-4\" transform=\"translate(386.000000, 561.000000)\">\n        <g id=\"编组备份-3\" transform=\"translate(0.000000, 69.529529)\">\n          <g id=\"Stroke-1\">\n            <g>\n              <linearGradient\n                id=\"path-13_1_\"\n                gradientTransform=\"matrix(47.9924 0 0 -27.7074 13731.9688 31874.9453)\"\n                gradientUnits=\"userSpaceOnUse\"\n                x1=\"-285.6253\"\n                x2=\"-285.6253\"\n                y1=\"1149.4111\"\n                y2=\"1150.4111\"\n              >\n                <stop offset=\"0\" style=\"stop-color: #3a3aff\" />\n                <stop offset=\"1\" style=\"stop-color: #0707d6\" />\n              </linearGradient>\n              <polygon\n                id=\"path-13\"\n                clip-rule=\"evenodd\"\n                fill=\"url(#path-13_1_)\"\n                fill-rule=\"evenodd\"\n                points=\"0.1,13.9 24.1,27.8 48.1,13.9\n\t\t\t\t\t\t24.1,0.1 \t\t\t\t\t\"\n              />\n            </g>\n            <g>\n              <linearGradient\n                id=\"SVGID_4_\"\n                gradientTransform=\"matrix(47.9924 0 0 -27.7074 13731.9688 31874.9453)\"\n                gradientUnits=\"userSpaceOnUse\"\n                x1=\"-285.6253\"\n                x2=\"-285.6253\"\n                y1=\"1149.4111\"\n                y2=\"1150.4294\"\n              >\n                <stop\n                  offset=\"0\"\n                  style=\"stop-color: #8fa0ff; stop-opacity: 0.467\"\n                />\n                <stop\n                  offset=\"1\"\n                  style=\"stop-color: #0f0f0f; stop-opacity: 0.06\"\n                />\n                <stop offset=\"1\" style=\"stop-color: #2f59e1; stop-opacity: 0\" />\n              </linearGradient>\n              <polygon\n                clip-rule=\"evenodd\"\n                fill=\"url(#SVGID_4_)\"\n                fill-rule=\"evenodd\"\n                points=\"0.1,13.9 24.1,27.8 48.1,13.9 24.1,0.1\n\t\t\t\t\t\t\"\n              />\n            </g>\n            <g filter=\"url(#filter-14)\">\n              <polygon\n                clip-rule=\"evenodd\"\n                fill-rule=\"evenodd\"\n                points=\"0.1,13.9 24.1,27.8 48.1,13.9 24.1,0.1 \t\t\t\t\t\"\n              />\n            </g>\n          </g>\n\n          <linearGradient\n            id=\"Stroke-3_1_\"\n            gradientTransform=\"matrix(23.9962 0 0 -41.5628 6916.9932 48336.7578)\"\n            gradientUnits=\"userSpaceOnUse\"\n            x1=\"-287.7007\"\n            x2=\"-287.815\"\n            y1=\"1162.8857\"\n            y2=\"1161.1971\"\n          >\n            <stop offset=\"0\" style=\"stop-color: #4f3eff\" />\n            <stop offset=\"1\" style=\"stop-color: #060071\" />\n          </linearGradient>\n          <polygon\n            id=\"Stroke-3\"\n            clip-rule=\"evenodd\"\n            fill=\"url(#Stroke-3_1_)\"\n            fill-rule=\"evenodd\"\n            points=\"24.1,55.5 24.1,27.8 0.1,13.9\n\t\t\t\t0.1,41.6 \t\t\t\"\n          />\n\n          <linearGradient\n            id=\"Stroke-5_1_\"\n            gradientTransform=\"matrix(23.9962 0 0 -41.5628 6941.0435 48336.7578)\"\n            gradientUnits=\"userSpaceOnUse\"\n            x1=\"-287.7007\"\n            x2=\"-287.815\"\n            y1=\"1162.8857\"\n            y2=\"1161.1971\"\n          >\n            <stop offset=\"0\" style=\"stop-color: #1200ca\" />\n            <stop offset=\"1\" style=\"stop-color: #060071\" />\n          </linearGradient>\n          <polygon\n            id=\"Stroke-5\"\n            clip-rule=\"evenodd\"\n            fill=\"url(#Stroke-5_1_)\"\n            fill-rule=\"evenodd\"\n            points=\"24.1,55.5 24.1,27.8\n\t\t\t\t48.1,13.9 48.1,41.6 \t\t\t\"\n          />\n        </g>\n\n        <linearGradient\n          id=\"矩形_1_\"\n          gradientTransform=\"matrix(48.245 0 0 -97.3101 13803.5742 121267.3516)\"\n          gradientUnits=\"userSpaceOnUse\"\n          x1=\"-285.6142\"\n          x2=\"-285.6142\"\n          y1=\"1246.0574\"\n          y2=\"1244.9515\"\n        >\n          <stop offset=\"0\" style=\"stop-color: #fff; stop-opacity: 0\" />\n          <stop offset=\"1\" style=\"stop-color: #fff\" />\n        </linearGradient>\n\n        <polygon\n          id=\"矩形\"\n          clip-rule=\"evenodd\"\n          enable-background=\"new    \"\n          fill=\"url(#矩形_1_)\"\n          fill-rule=\"evenodd\"\n          opacity=\"0.1977\"\n          points=\"\n\t\t\t0,0 48.2,0 48.2,83.7 24.1,97.3 0,83.7 \t\t\"\n        />\n      </g>\n      <g id=\"编组-4\" transform=\"translate(71.000000, 199.000000)\">\n        <g id=\"Shape备份-2\">\n          <g>\n            <linearGradient\n              id=\"path-19_1_\"\n              gradientTransform=\"matrix(228.3947 0 0 -131.5657 -6978.2363 212124.2031)\"\n              gradientUnits=\"userSpaceOnUse\"\n              x1=\"31.2434\"\n              x2=\"30.8024\"\n              y1=\"1610.2603\"\n              y2=\"1610.6188\"\n            >\n              <stop offset=\"0\" style=\"stop-color: #4587ef\" />\n              <stop offset=\"1\" style=\"stop-color: #00209e\" />\n            </linearGradient>\n            <path\n              id=\"path-19\"\n              clip-rule=\"evenodd\"\n              d=\"M63.4,312.7l-62-35.9\n\t\t\t\t\tc-1.9-1.1-1.9-3.8,0-4.9l151.1-87.4c3.5-2,7.8-2,11.3,0l63.1,36.6c1.9,1.1,1.9,3.8,0,4.9L77,312.7\n\t\t\t\t\tC72.8,315.2,67.6,315.2,63.4,312.7z\"\n              fill=\"url(#path-19_1_)\"\n              fill-rule=\"evenodd\"\n            />\n          </g>\n          <g filter=\"url(#filter-20)\">\n            <path\n              clip-rule=\"evenodd\"\n              d=\"M63.4,312.7l-62-35.9c-1.9-1.1-1.9-3.8,0-4.9l151.1-87.4c3.5-2,7.8-2,11.3,0\n\t\t\t\t\tl63.1,36.6c1.9,1.1,1.9,3.8,0,4.9L77,312.7C72.8,315.2,67.6,315.2,63.4,312.7z\"\n              fill-rule=\"evenodd\"\n            />\n          </g>\n        </g>\n        <g id=\"编组-13\" transform=\"translate(45.000000, 0.000000)\">\n          <g id=\"编组备份-17\" transform=\"translate(90.066225, 54.000000)\">\n            <g>\n              <g>\n                <linearGradient\n                  id=\"path-23_1_\"\n                  gradientTransform=\"matrix(47.5983 0 0 -27.4798 5055.4995 41979.2891)\"\n                  gradientUnits=\"userSpaceOnUse\"\n                  x1=\"-105.7092\"\n                  x2=\"-105.7092\"\n                  y1=\"1526.6367\"\n                  y2=\"1527.6367\"\n                >\n                  <stop offset=\"0\" style=\"stop-color: #3a3aff\" />\n                  <stop offset=\"1\" style=\"stop-color: #0707d6\" />\n                </linearGradient>\n                <polygon\n                  id=\"path-23\"\n                  clip-rule=\"evenodd\"\n                  fill=\"url(#path-23_1_)\"\n                  fill-rule=\"evenodd\"\n                  points=\"0.1,13.8 23.9,27.6\n\t\t\t\t\t\t\t47.7,13.8 23.9,0.1 \t\t\t\t\t\t\"\n                />\n              </g>\n              <g>\n                <linearGradient\n                  id=\"SVGID_5_\"\n                  gradientTransform=\"matrix(47.5983 0 0 -27.4798 5055.4995 41979.2891)\"\n                  gradientUnits=\"userSpaceOnUse\"\n                  x1=\"-105.7092\"\n                  x2=\"-105.7092\"\n                  y1=\"1526.6367\"\n                  y2=\"1527.655\"\n                >\n                  <stop\n                    offset=\"0\"\n                    style=\"stop-color: #8fa0ff; stop-opacity: 0.467\"\n                  />\n                  <stop\n                    offset=\"1\"\n                    style=\"stop-color: #0f0f0f; stop-opacity: 0.06\"\n                  />\n                  <stop\n                    offset=\"1\"\n                    style=\"stop-color: #2f59e1; stop-opacity: 0\"\n                  />\n                </linearGradient>\n                <polygon\n                  clip-rule=\"evenodd\"\n                  fill=\"url(#SVGID_5_)\"\n                  fill-rule=\"evenodd\"\n                  points=\"0.1,13.8 23.9,27.6 47.7,13.8 23.9,0.1\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"\n                />\n              </g>\n              <g filter=\"url(#filter-24)\">\n                <polygon\n                  clip-rule=\"evenodd\"\n                  fill-rule=\"evenodd\"\n                  points=\"0.1,13.8 23.9,27.6 47.7,13.8 23.9,0.1 \t\t\t\t\t\t\"\n                />\n              </g>\n            </g>\n\n            <linearGradient\n              id=\"SVGID_6_\"\n              gradientTransform=\"matrix(23.7991 0 0 -168.4388 2578.7588 262543.8125)\"\n              gradientUnits=\"userSpaceOnUse\"\n              x1=\"-107.8491\"\n              x2=\"-107.8559\"\n              y1=\"1558.8499\"\n              y2=\"1557.1613\"\n            >\n              <stop offset=\"0\" style=\"stop-color: #4f3eff\" />\n              <stop offset=\"1\" style=\"stop-color: #060071\" />\n            </linearGradient>\n            <polygon\n              clip-rule=\"evenodd\"\n              fill=\"url(#SVGID_6_)\"\n              fill-rule=\"evenodd\"\n              points=\"23.9,182.3 23.9,27.6 0.1,13.8 0.1,168.5\n\t\t\t\t\t\"\n            />\n\n            <linearGradient\n              id=\"SVGID_7_\"\n              gradientTransform=\"matrix(23.7991 0 0 -168.4388 2602.6116 262543.8125)\"\n              gradientUnits=\"userSpaceOnUse\"\n              x1=\"-107.8491\"\n              x2=\"-107.8559\"\n              y1=\"1558.8499\"\n              y2=\"1557.1613\"\n            >\n              <stop offset=\"0\" style=\"stop-color: #1200ca\" />\n              <stop offset=\"1\" style=\"stop-color: #060071\" />\n            </linearGradient>\n            <polygon\n              clip-rule=\"evenodd\"\n              fill=\"url(#SVGID_7_)\"\n              fill-rule=\"evenodd\"\n              points=\"23.9,182.3 23.9,27.6 47.7,13.8 47.7,168.5\n\t\t\t\t\t\t\t\t\t\"\n            />\n          </g>\n          <g id=\"编组备份-16\" transform=\"translate(45.562914, 134.529801)\">\n            <g>\n              <g>\n                <linearGradient\n                  id=\"path-28_1_\"\n                  gradientTransform=\"matrix(47.5983 0 0 -27.4798 2937.218 39766.3438)\"\n                  gradientUnits=\"userSpaceOnUse\"\n                  x1=\"-61.7058\"\n                  x2=\"-60.7058\"\n                  y1=\"1446.6069\"\n                  y2=\"1446.6069\"\n                >\n                  <stop offset=\"0\" style=\"stop-color: #bda0e5\" />\n                  <stop offset=\"1\" style=\"stop-color: #5f3abd\" />\n                </linearGradient>\n                <polygon\n                  id=\"path-28\"\n                  clip-rule=\"evenodd\"\n                  fill=\"url(#path-28_1_)\"\n                  fill-rule=\"evenodd\"\n                  points=\"0.1,13.8 23.9,27.6\n\t\t\t\t\t\t\t47.7,13.8 23.9,0.1 \t\t\t\t\t\t\"\n                />\n              </g>\n              <g>\n                <linearGradient\n                  id=\"SVGID_8_\"\n                  gradientTransform=\"matrix(47.5983 0 0 -27.4798 2937.218 39766.3438)\"\n                  gradientUnits=\"userSpaceOnUse\"\n                  x1=\"-61.2058\"\n                  x2=\"-61.2058\"\n                  y1=\"1446.1069\"\n                  y2=\"1447.1252\"\n                >\n                  <stop\n                    offset=\"0\"\n                    style=\"stop-color: #8fa0ff; stop-opacity: 0.467\"\n                  />\n                  <stop\n                    offset=\"1\"\n                    style=\"stop-color: #0f0f0f; stop-opacity: 0.06\"\n                  />\n                  <stop\n                    offset=\"1\"\n                    style=\"stop-color: #2f59e1; stop-opacity: 0\"\n                  />\n                </linearGradient>\n                <polygon\n                  clip-rule=\"evenodd\"\n                  fill=\"url(#SVGID_8_)\"\n                  fill-rule=\"evenodd\"\n                  points=\"0.1,13.8 23.9,27.6 47.7,13.8 23.9,0.1\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"\n                />\n              </g>\n              <g filter=\"url(#filter-29)\">\n                <polygon\n                  clip-rule=\"evenodd\"\n                  fill-rule=\"evenodd\"\n                  points=\"0.1,13.8 23.9,27.6 47.7,13.8 23.9,0.1 \t\t\t\t\t\t\"\n                />\n              </g>\n            </g>\n\n            <linearGradient\n              id=\"SVGID_9_\"\n              gradientTransform=\"matrix(23.7991 0 0 -114.399 1519.618 168778.2344)\"\n              gradientUnits=\"userSpaceOnUse\"\n              x1=\"-63.3488\"\n              x2=\"-63.3488\"\n              y1=\"1475.4679\"\n              y2=\"1474.0222\"\n            >\n              <stop offset=\"0\" style=\"stop-color: #4f3eff\" />\n              <stop offset=\"1\" style=\"stop-color: #f77\" />\n            </linearGradient>\n            <polygon\n              clip-rule=\"evenodd\"\n              fill=\"url(#SVGID_9_)\"\n              fill-rule=\"evenodd\"\n              points=\"23.9,128.2 23.9,27.6 0.1,13.8 0.1,114.5\n\t\t\t\t\t\"\n            />\n\n            <linearGradient\n              id=\"SVGID_10_\"\n              gradientTransform=\"matrix(23.7991 0 0 -114.399 1543.4708 168778.2344)\"\n              gradientUnits=\"userSpaceOnUse\"\n              x1=\"-63.3488\"\n              x2=\"-63.3488\"\n              y1=\"1475.4679\"\n              y2=\"1474.0222\"\n            >\n              <stop offset=\"0\" style=\"stop-color: #4f3eff\" />\n              <stop offset=\"1\" style=\"stop-color: #d73838\" />\n            </linearGradient>\n            <polygon\n              clip-rule=\"evenodd\"\n              fill=\"url(#SVGID_10_)\"\n              fill-rule=\"evenodd\"\n              points=\"23.9,128.2 23.9,27.6 47.7,13.8 47.7,114.5\n\t\t\t\t\t\t\t\t\t\"\n            />\n          </g>\n          <g id=\"编组备份-18\" transform=\"translate(0.000000, 0.271523)\">\n            <g>\n              <g>\n                <linearGradient\n                  id=\"path-33_1_\"\n                  gradientTransform=\"matrix(47.5983 0 0 -27.4798 768.5011 43638.7383)\"\n                  gradientUnits=\"userSpaceOnUse\"\n                  x1=\"-16.1429\"\n                  x2=\"-15.1429\"\n                  y1=\"1580.8652\"\n                  y2=\"1580.8652\"\n                >\n                  <stop offset=\"0\" style=\"stop-color: #8afffc\" />\n                  <stop offset=\"1\" style=\"stop-color: #003dde\" />\n                </linearGradient>\n                <polygon\n                  id=\"path-33\"\n                  clip-rule=\"evenodd\"\n                  fill=\"url(#path-33_1_)\"\n                  fill-rule=\"evenodd\"\n                  points=\"0.1,196.8 23.9,210.6\n\t\t\t\t\t\t\t47.7,196.8 23.9,183.1 \t\t\t\t\t\t\"\n                />\n              </g>\n              <g>\n                <linearGradient\n                  id=\"SVGID_11_\"\n                  gradientTransform=\"matrix(47.5983 0 0 -27.4798 768.5011 43638.7383)\"\n                  gradientUnits=\"userSpaceOnUse\"\n                  x1=\"-15.6429\"\n                  x2=\"-15.6429\"\n                  y1=\"1580.3652\"\n                  y2=\"1581.3835\"\n                >\n                  <stop\n                    offset=\"0\"\n                    style=\"stop-color: #8fa0ff; stop-opacity: 0.467\"\n                  />\n                  <stop\n                    offset=\"1\"\n                    style=\"stop-color: #0f0f0f; stop-opacity: 0.06\"\n                  />\n                  <stop\n                    offset=\"1\"\n                    style=\"stop-color: #2f59e1; stop-opacity: 0\"\n                  />\n                </linearGradient>\n                <polygon\n                  clip-rule=\"evenodd\"\n                  fill=\"url(#SVGID_11_)\"\n                  fill-rule=\"evenodd\"\n                  points=\"0.1,196.8 23.9,210.6 47.7,196.8\n\t\t\t\t\t\t\t23.9,183.1 \t\t\t\t\t\t\"\n                />\n              </g>\n              <g filter=\"url(#filter-34)\">\n                <polygon\n                  clip-rule=\"evenodd\"\n                  fill-rule=\"evenodd\"\n                  points=\"0.1,196.8 23.9,210.6 47.7,196.8 23.9,183.1 \t\t\t\t\t\t\"\n                />\n              </g>\n            </g>\n\n            <linearGradient\n              id=\"SVGID_12_\"\n              gradientTransform=\"matrix(48 0 0 -80.5525 774 129477.0156)\"\n              gradientUnits=\"userSpaceOnUse\"\n              x1=\"-15.625\"\n              x2=\"-15.625\"\n              y1=\"1604.7488\"\n              y2=\"1605.7488\"\n            >\n              <stop offset=\"0\" style=\"stop-color: #45efed\" />\n              <stop offset=\"1\" style=\"stop-color: #d0ffed; stop-opacity: 0\" />\n            </linearGradient>\n            <polygon\n              clip-rule=\"evenodd\"\n              enable-background=\"new    \"\n              fill=\"url(#SVGID_12_)\"\n              fill-rule=\"evenodd\"\n              opacity=\"0.3\"\n              points=\"\n\t\t\t\t\t0,130 48,130 48,196.7 24,210.6 0,196.7 \t\t\t\t\"\n            />\n\n            <linearGradient\n              id=\"矩形备份_1_\"\n              gradientTransform=\"matrix(48 0 0 -80.5525 820 129428.7578)\"\n              gradientUnits=\"userSpaceOnUse\"\n              x1=\"-15.625\"\n              x2=\"-15.625\"\n              y1=\"1604.7488\"\n              y2=\"1605.7488\"\n            >\n              <stop offset=\"0\" style=\"stop-color: #45efed\" />\n              <stop offset=\"1\" style=\"stop-color: #d0ffed; stop-opacity: 0\" />\n            </linearGradient>\n\n            <polygon\n              id=\"矩形备份\"\n              clip-rule=\"evenodd\"\n              enable-background=\"new    \"\n              fill=\"url(#矩形备份_1_)\"\n              fill-rule=\"evenodd\"\n              opacity=\"0.3\"\n              points=\"\n\t\t\t\t\t46,81.7 94,81.7 94,148.5 70,162.3 46,148.5 \t\t\t\t\"\n            />\n\n            <linearGradient\n              id=\"矩形备份-4_1_\"\n              gradientTransform=\"matrix(48 0 0 -80.5525 863 129347.75)\"\n              gradientUnits=\"userSpaceOnUse\"\n              x1=\"-15.625\"\n              x2=\"-15.625\"\n              y1=\"1604.7488\"\n              y2=\"1605.7488\"\n            >\n              <stop offset=\"0\" style=\"stop-color: #45efed\" />\n              <stop offset=\"1\" style=\"stop-color: #d0ffed; stop-opacity: 0\" />\n            </linearGradient>\n\n            <polygon\n              id=\"矩形备份-4\"\n              clip-rule=\"evenodd\"\n              enable-background=\"new    \"\n              fill=\"url(#矩形备份-4_1_)\"\n              fill-rule=\"evenodd\"\n              opacity=\"0.3\"\n              points=\"\n\t\t\t\t\t89,0.7 137,0.7 137,67.5 113,81.3 89,67.5 \t\t\t\t\"\n            />\n\n            <linearGradient\n              id=\"SVGID_13_\"\n              gradientTransform=\"matrix(23.7991 0 0 -93.207 435.2595 150023.5938)\"\n              gradientUnits=\"userSpaceOnUse\"\n              x1=\"-17.8122\"\n              x2=\"-17.7572\"\n              y1=\"1607.5193\"\n              y2=\"1606.3573\"\n            >\n              <stop offset=\"0\" style=\"stop-color: #68dfff\" />\n              <stop offset=\"1\" style=\"stop-color: #004a8b\" />\n            </linearGradient>\n            <polygon\n              clip-rule=\"evenodd\"\n              fill=\"url(#SVGID_13_)\"\n              fill-rule=\"evenodd\"\n              points=\"23.9,290 23.9,210.6 0.1,196.8 0.1,276.3\n\t\t\t\t\t\"\n            />\n\n            <linearGradient\n              id=\"SVGID_14_\"\n              gradientTransform=\"matrix(23.7991 0 0 -93.207 459.1122 150023.5938)\"\n              gradientUnits=\"userSpaceOnUse\"\n              x1=\"-17.8038\"\n              x2=\"-17.7678\"\n              y1=\"1607.3881\"\n              y2=\"1606.5361\"\n            >\n              <stop offset=\"0\" style=\"stop-color: #0567de\" />\n              <stop offset=\"1\" style=\"stop-color: #00048b\" />\n            </linearGradient>\n            <polygon\n              clip-rule=\"evenodd\"\n              fill=\"url(#SVGID_14_)\"\n              fill-rule=\"evenodd\"\n              points=\"23.9,290 23.9,210.6 47.7,196.8 47.7,276.3\n\t\t\t\t\t\t\t\t\t\"\n            />\n          </g>\n        </g>\n      </g>\n      <g id=\"编组-9\" transform=\"translate(286.000000, 26.000000)\">\n        <g id=\"编组-3\" transform=\"translate(0.000000, 326.797996)\">\n          <g id=\"椭圆形\" filter=\"url(#filter-39)\">\n            <linearGradient\n              id=\"SVGID_15_\"\n              gradientTransform=\"matrix(298.9761 -31.4237 -16.5194 -157.172 79324.2969 223473.4688)\"\n              gradientUnits=\"userSpaceOnUse\"\n              x1=\"-183.6863\"\n              x2=\"-184.3618\"\n              y1=\"1458.1069\"\n              y2=\"1457.9841\"\n            >\n              <stop offset=\"0\" style=\"stop-color: #1b0f9c; stop-opacity: 0\" />\n              <stop offset=\"1\" style=\"stop-color: #060071\" />\n            </linearGradient>\n            <path\n              clip-rule=\"evenodd\"\n              d=\"M161.7,23.6c82.6-8.7,153.2,19.5,157.7,62.9\n\t\t\t\t\tc4.6,43.4-58.7,85.6-141.2,94.3C95.7,189.5,25,161.3,20.5,117.9S79.1,32.3,161.7,23.6z\"\n              fill=\"url(#SVGID_15_)\"\n              fill-rule=\"evenodd\"\n            />\n          </g>\n          <g transform=\"translate(0.000000, 32.309947)\">\n            <g id=\"形状结合\">\n              <linearGradient\n                id=\"SVGID_16_\"\n                gradientTransform=\"matrix(252.8605 0 0 -153.121 46628.3242 218334.6719)\"\n                gradientUnits=\"userSpaceOnUse\"\n                x1=\"-184.3435\"\n                x2=\"-183.4387\"\n                y1=\"1425.4349\"\n                y2=\"1425.3547\"\n              >\n                <stop offset=\"0\" style=\"stop-color: #3b76df\" />\n                <stop offset=\"1\" style=\"stop-color: #0c2ea7\" />\n              </linearGradient>\n              <path\n                clip-rule=\"evenodd\"\n                d=\"M36.9,21.3c49.2-28.4,129.8-28.4,179,0\n\t\t\t\t\t\tc9.7,5.6,17.5,12.2,23.4,19.4l13.5,0v39.3l-0.1,0c-1.3,19.3-13.6,38.3-36.8,51.7c-49.2,28.4-129.8,28.4-179,0\n\t\t\t\t\t\tC13.7,118.4,1.4,99.4,0.1,80.1l-0.1,0V40.7l13.5,0C19.4,33.5,27.2,26.9,36.9,21.3z\"\n                fill=\"url(#SVGID_16_)\"\n                fill-rule=\"evenodd\"\n              />\n            </g>\n            <defs>\n              <filter\n                id=\"Adobe_OpacityMaskFilter\"\n                filterUnits=\"userSpaceOnUse\"\n                height=\"86.4\"\n                width=\"22.5\"\n                x=\"18.3\"\n                y=\"59.7\"\n              >\n                <feColorMatrix\n                  type=\"matrix\"\n                  values=\"1 0 0 0 0  0 1 0 0 0  0 0 1 0 0  0 0 0 1 0\"\n                />\n              </filter>\n            </defs>\n            <mask\n              id=\"mask-42_1_\"\n              height=\"86.4\"\n              maskUnits=\"userSpaceOnUse\"\n              width=\"22.5\"\n              x=\"18.3\"\n              y=\"59.7\"\n            >\n              <g filter=\"url(#Adobe_OpacityMaskFilter)\">\n                <path\n                  id=\"path-41_1_\"\n                  clip-rule=\"evenodd\"\n                  d=\"M36.9,21.3c49.2-28.4,129.8-28.4,179,0\n\t\t\t\t\t\t\tc9.7,5.6,17.5,12.2,23.4,19.4l13.5,0v39.3l-0.1,0c-1.3,19.3-13.6,38.3-36.8,51.7c-49.2,28.4-129.8,28.4-179,0\n\t\t\t\t\t\t\tC13.7,118.4,1.4,99.4,0.1,80.1l-0.1,0V40.7l13.5,0C19.4,33.5,27.2,26.9,36.9,21.3z\"\n                  fill=\"#FFFFFF\"\n                  fill-rule=\"evenodd\"\n                />\n              </g>\n            </mask>\n            <g filter=\"url(#filter-43)\" mask=\"url(#mask-42_1_)\">\n              <rect\n                clip-rule=\"evenodd\"\n                fill=\"#FFFFFF\"\n                fill-rule=\"evenodd\"\n                height=\"86.4\"\n                width=\"22.5\"\n                x=\"18.3\"\n                y=\"59.7\"\n              />\n            </g>\n          </g>\n          <g id=\"Stroke-1备份\">\n            <g>\n              <linearGradient\n                id=\"path-45_1_\"\n                gradientTransform=\"matrix(252.8604 0 0 -146.0971 46628.3203 212993.0469)\"\n                gradientUnits=\"userSpaceOnUse\"\n                x1=\"-183.7127\"\n                x2=\"-184.1537\"\n                y1=\"1457.2306\"\n                y2=\"1457.5913\"\n              >\n                <stop offset=\"0\" style=\"stop-color: #4587ef\" />\n                <stop offset=\"1\" style=\"stop-color: #00209e\" />\n              </linearGradient>\n              <path\n                id=\"path-45\"\n                clip-rule=\"evenodd\"\n                d=\"M36.9,21.3c-49.2,28.4-49.2,75,0,103.4\n\t\t\t\t\t\tc49.2,28.4,129.8,28.4,179,0c49.2-28.4,49.2-75,0-103.4C166.7-7.1,86.2-7.1,36.9,21.3z\"\n                fill=\"url(#path-45_1_)\"\n                fill-rule=\"evenodd\"\n              />\n            </g>\n            <g filter=\"url(#filter-46)\">\n              <path\n                clip-rule=\"evenodd\"\n                d=\"M36.9,21.3c-49.2,28.4-49.2,75,0,103.4c49.2,28.4,129.8,28.4,179,0\n\t\t\t\t\t\tc49.2-28.4,49.2-75,0-103.4C166.7-7.1,86.2-7.1,36.9,21.3z\"\n                fill-rule=\"evenodd\"\n              />\n            </g>\n          </g>\n        </g>\n        <g id=\"编组-3备份\" transform=\"translate(24.286045, 0.000000)\">\n          <g transform=\"translate(1.000000, 323.847957)\">\n            <g>\n              <linearGradient\n                id=\"SVGID_17_\"\n                gradientTransform=\"matrix(202.2884 0 0 -122.4968 42438.1328 178783.5781)\"\n                gradientUnits=\"userSpaceOnUse\"\n                x1=\"-209.8152\"\n                x2=\"-208.7883\"\n                y1=\"1459.0104\"\n                y2=\"1458.9812\"\n              >\n                <stop offset=\"0\" style=\"stop-color: #8dcbff\" />\n                <stop offset=\"0.1196\" style=\"stop-color: #cafbff\" />\n                <stop offset=\"0.768\" style=\"stop-color: #7feeff\" />\n                <stop offset=\"1\" style=\"stop-color: #adcbff\" />\n              </linearGradient>\n              <path\n                clip-rule=\"evenodd\"\n                d=\"M29.5,17.1c39.4-22.8,103.8-22.8,143.2,0\n\t\t\t\t\t\tc7.8,4.5,14,9.8,18.7,15.5l10.8,0v31.5l-0.1,0c-1.1,15.5-10.9,30.6-29.4,41.4c-39.4,22.8-103.8,22.8-143.2,0\n\t\t\t\t\t\tC11,94.7,1.2,79.5,0.1,64.1l-0.1,0V32.6l10.8,0C15.5,26.8,21.8,21.6,29.5,17.1z\"\n                fill=\"url(#SVGID_17_)\"\n                fill-rule=\"evenodd\"\n              />\n            </g>\n            <defs>\n              <filter\n                id=\"Adobe_OpacityMaskFilter_1_\"\n                filterUnits=\"userSpaceOnUse\"\n                height=\"69.1\"\n                width=\"18\"\n                x=\"14.6\"\n                y=\"47.8\"\n              >\n                <feColorMatrix\n                  type=\"matrix\"\n                  values=\"1 0 0 0 0  0 1 0 0 0  0 0 1 0 0  0 0 0 1 0\"\n                />\n              </filter>\n            </defs>\n            <mask\n              id=\"mask-49_1_\"\n              height=\"69.1\"\n              maskUnits=\"userSpaceOnUse\"\n              width=\"18\"\n              x=\"14.6\"\n              y=\"47.8\"\n            >\n              <g filter=\"url(#Adobe_OpacityMaskFilter_1_)\">\n                <path\n                  id=\"path-48_1_\"\n                  clip-rule=\"evenodd\"\n                  d=\"M29.5,17.1c39.4-22.8,103.8-22.8,143.2,0\n\t\t\t\t\t\t\tc7.8,4.5,14,9.8,18.7,15.5l10.8,0v31.5l-0.1,0c-1.1,15.5-10.9,30.6-29.4,41.4c-39.4,22.8-103.8,22.8-143.2,0\n\t\t\t\t\t\t\tC11,94.7,1.2,79.5,0.1,64.1l-0.1,0V32.6l10.8,0C15.5,26.8,21.8,21.6,29.5,17.1z\"\n                  fill=\"#FFFFFF\"\n                  fill-rule=\"evenodd\"\n                />\n              </g>\n            </mask>\n            <g filter=\"url(#filter-50)\" mask=\"url(#mask-49_1_)\">\n              <rect\n                clip-rule=\"evenodd\"\n                fill=\"#FFFFFF\"\n                fill-rule=\"evenodd\"\n                height=\"69.1\"\n                width=\"18\"\n                x=\"14.6\"\n                y=\"47.8\"\n              />\n            </g>\n          </g>\n          <g>\n            <g>\n              <linearGradient\n                id=\"path-52_1_\"\n                gradientTransform=\"matrix(202.2884 0 0 -116.8777 42236.8438 208684.4531)\"\n                gradientUnits=\"userSpaceOnUse\"\n                x1=\"-208.0996\"\n                x2=\"-208.5406\"\n                y1=\"1782.2883\"\n                y2=\"1782.6489\"\n              >\n                <stop offset=\"0\" style=\"stop-color: #4587ef\" />\n                <stop offset=\"1\" style=\"stop-color: #00209e\" />\n              </linearGradient>\n              <path\n                id=\"path-52\"\n                clip-rule=\"evenodd\"\n                d=\"M30.5,315.1c-39.4,22.8-39.4,60,0,82.7\n\t\t\t\t\t\tc39.4,22.8,103.8,22.8,143.2,0c39.4-22.8,39.4-60,0-82.7C134.4,292.3,69.9,292.3,30.5,315.1z\"\n                fill=\"url(#path-52_1_)\"\n                fill-rule=\"evenodd\"\n              />\n            </g>\n            <g filter=\"url(#filter-53)\">\n              <path\n                clip-rule=\"evenodd\"\n                d=\"M30.5,315.1c-39.4,22.8-39.4,60,0,82.7c39.4,22.8,103.8,22.8,143.2,0\n\t\t\t\t\t\tc39.4-22.8,39.4-60,0-82.7C134.4,292.3,69.9,292.3,30.5,315.1z\"\n                fill-rule=\"evenodd\"\n              />\n            </g>\n          </g>\n\n          <linearGradient\n            id=\"SVGID_18_\"\n            gradientTransform=\"matrix(202.5744 0 0 -414.8777 42296.1367 742296.8125)\"\n            gradientUnits=\"userSpaceOnUse\"\n            x1=\"-208.2896\"\n            x2=\"-208.2896\"\n            y1=\"1788.1943\"\n            y2=\"1789.1943\"\n          >\n            <stop offset=\"0\" style=\"stop-color: #45efed\" />\n            <stop offset=\"1\" style=\"stop-color: #d0ffed; stop-opacity: 0\" />\n          </linearGradient>\n          <path\n            clip-rule=\"evenodd\"\n            d=\"M202.7,0\n\t\t\t\tl0,350.2c3.1,17-6.5,34.7-29,47.6c-39.4,22.8-103.8,22.8-143.2,0C12,387.1,2.2,373.1,1.1,359l-0.4,0V0H202.7z\"\n            enable-background=\"new    \"\n            fill=\"url(#SVGID_18_)\"\n            fill-rule=\"evenodd\"\n            opacity=\"0.3\"\n          />\n        </g>\n      </g>\n      <g id=\"编组-11备份\" transform=\"translate(329.000000, 539.000000)\">\n        <g id=\"编组-6备份\" transform=\"translate(0.000000, 4.148936)\">\n          <g filter=\"url(#filter-56)\">\n            <linearGradient\n              id=\"SVGID_19_\"\n              gradientTransform=\"matrix(52.2766 0 0 -21.5745 11984.5537 26508.6328)\"\n              gradientUnits=\"userSpaceOnUse\"\n              x1=\"-227.9383\"\n              x2=\"-228.6138\"\n              y1=\"1226.9152\"\n              y2=\"1226.8395\"\n            >\n              <stop offset=\"0\" style=\"stop-color: #1b0f9c; stop-opacity: 0\" />\n              <stop offset=\"1\" style=\"stop-color: #060071\" />\n            </linearGradient>\n            <ellipse\n              clip-rule=\"evenodd\"\n              cx=\"41.9\"\n              cy=\"39.8\"\n              fill=\"url(#SVGID_19_)\"\n              fill-rule=\"evenodd\"\n              rx=\"26.1\"\n              ry=\"10.8\"\n            />\n          </g>\n\n          <radialGradient\n            id=\"SVGID_20_\"\n            cx=\"-228.8779\"\n            cy=\"1253.5115\"\n            gradientTransform=\"matrix(48.9574 0 0 -48.9574 11215.3408 61379.1055)\"\n            gradientUnits=\"userSpaceOnUse\"\n            r=\"0.9018\"\n          >\n            <stop offset=\"0\" style=\"stop-color: #dedbff\" />\n            <stop offset=\"0.3987\" style=\"stop-color: #4165fc\" />\n            <stop offset=\"1\" style=\"stop-color: #0a00c2\" />\n          </radialGradient>\n          <circle\n            clip-rule=\"evenodd\"\n            cx=\"24.5\"\n            cy=\"24.5\"\n            fill=\"url(#SVGID_20_)\"\n            fill-rule=\"evenodd\"\n            r=\"24.5\"\n          />\n        </g>\n        <g id=\"编组-6\" transform=\"translate(23.510638, 0.000000)\">\n          <g filter=\"url(#filter-59)\">\n            <linearGradient\n              id=\"SVGID_21_\"\n              gradientTransform=\"matrix(87.1277 0 0 -35.9574 21954.6836 45008.2383)\"\n              gradientUnits=\"userSpaceOnUse\"\n              x1=\"-250.6685\"\n              x2=\"-251.344\"\n              y1=\"1249.9197\"\n              y2=\"1249.844\"\n            >\n              <stop offset=\"0\" style=\"stop-color: #1b0f9c; stop-opacity: 0\" />\n              <stop offset=\"1\" style=\"stop-color: #060071\" />\n            </linearGradient>\n            <ellipse\n              clip-rule=\"evenodd\"\n              cx=\"69.8\"\n              cy=\"66.4\"\n              fill=\"url(#SVGID_21_)\"\n              fill-rule=\"evenodd\"\n              rx=\"43.6\"\n              ry=\"18\"\n            />\n          </g>\n\n          <radialGradient\n            id=\"SVGID_22_\"\n            cx=\"-251.5552\"\n            cy=\"1265.9697\"\n            gradientTransform=\"matrix(81.5957 0 0 -81.5957 20542.6016 103315.0391)\"\n            gradientUnits=\"userSpaceOnUse\"\n            r=\"0.9018\"\n          >\n            <stop offset=\"0\" style=\"stop-color: #dedbff\" />\n            <stop offset=\"0.3987\" style=\"stop-color: #4165fc\" />\n            <stop offset=\"1\" style=\"stop-color: #0a00c2\" />\n          </radialGradient>\n          <circle\n            clip-rule=\"evenodd\"\n            cx=\"40.8\"\n            cy=\"40.8\"\n            fill=\"url(#SVGID_22_)\"\n            fill-rule=\"evenodd\"\n            r=\"40.8\"\n          />\n        </g>\n      </g>\n\n      <g\n        id=\"编组备份-9\"\n        opacity=\"0.5\"\n        transform=\"translate(488.000000, 172.000000) scale(-1, 1) translate(-488.000000, -172.000000) translate(449.000000, 122.000000)\"\n      >\n        <linearGradient\n          id=\"SVGID_23_\"\n          gradientTransform=\"matrix(-75.8604 0 0 -99.5101 32139.0957 167716.875)\"\n          gradientUnits=\"userSpaceOnUse\"\n          x1=\"423.6246\"\n          x2=\"424.2521\"\n          y1=\"1683.8809\"\n          y2=\"1685.3489\"\n        >\n          <stop offset=\"0\" style=\"stop-color: #fff\" />\n          <stop offset=\"1\" style=\"stop-color: #00c6fb\" />\n        </linearGradient>\n        <path\n          d=\"M72.5,0l3.8,2.1L75.1,4l-1.6,0.6l-0.7,49.2c0,1.6-0.9,3.4-2.1,4.2l-0.2,0.1L5.7,95.5l0,0l-0.7,4\n\t\t\tl-3.7-2.1c-0.1,0-0.1-0.1-0.2-0.1l0,0c-0.4-0.3-0.7-0.9-0.7-1.8l0,0l0.8-51.8c0-1.7,1-3.6,2.3-4.3l0,0L71.2,0.3\n\t\t\tC71.7,0,72.1-0.1,72.5,0L72.5,0z\"\n          fill=\"url(#SVGID_23_)\"\n        />\n\n        <linearGradient\n          id=\"SVGID_24_\"\n          gradientTransform=\"matrix(-73.087 0 0 -97.5101 30963.9746 164327.5781)\"\n          gradientUnits=\"userSpaceOnUse\"\n          x1=\"423.5395\"\n          x2=\"424.1461\"\n          y1=\"1683.6631\"\n          y2=\"1685.1311\"\n        >\n          <stop offset=\"0\" style=\"stop-color: #005bea\" />\n          <stop offset=\"1\" style=\"stop-color: #00c6fb\" />\n        </linearGradient>\n        <path\n          d=\"M6.3,99.2l67.8-39.1c1.2-0.7,2.3-2.7,2.3-4.3L77.1,4c0-1.7-1-2.4-2.3-1.7L7,41.4\n\t\t\tc-1.2,0.7-2.3,2.7-2.3,4.3L4,97.5C4,99.2,5,100,6.3,99.2z\"\n          fill=\"url(#SVGID_24_)\"\n        />\n        <g\n          id=\"XMLID_902_\"\n          opacity=\"0.3\"\n          transform=\"translate(51.463000, 21.500000)\"\n        >\n          <path\n            id=\"XMLID_905_\"\n            d=\"M17.9,4.7L1.7,14.1c-0.5,0.3-1.2,0.1-1.5-0.4C0.1,13.5,0,13.3,0,13.1\n\t\t\t\tc0-1.2,0.6-2.2,1.6-2.8l16.3-9.4c0.5-0.3,1.2-0.1,1.5,0.4c0.1,0.2,0.1,0.4,0.1,0.5C19.5,3,18.9,4.1,17.9,4.7z\"\n            fill=\"#FFFFFF\"\n          />\n          <path\n            id=\"XMLID_905_备份\"\n            d=\"M18.2,14.6L5.5,21.9c-0.4,0.2-0.9,0.1-1.1-0.3c-0.1-0.1-0.1-0.3-0.1-0.4\n\t\t\t\tc0-0.9,0.5-1.7,1.3-2.2l12.6-7.3c0.4-0.2,0.9-0.1,1.1,0.3c0.1,0.1,0.1,0.3,0.1,0.4C19.4,13.3,18.9,14.2,18.2,14.6z\"\n            fill=\"#FFFFFF\"\n          />\n          <path\n            id=\"XMLID_905_备份-2\"\n            d=\"M18.2,22.3L5.5,29.6c-0.4,0.2-0.9,0.1-1.1-0.3c-0.1-0.1-0.1-0.3-0.1-0.4\n\t\t\t\tc0-0.9,0.5-1.7,1.3-2.2l12.6-7.3c0.4-0.2,0.9-0.1,1.1,0.3c0.1,0.1,0.1,0.3,0.1,0.4C19.4,21,18.9,21.8,18.2,22.3z\"\n            fill=\"#FFFFFF\"\n          />\n          <path\n            id=\"XMLID_905_备份-3\"\n            d=\"M18.2,29.9L5.5,37.2c-0.4,0.2-0.9,0.1-1.1-0.3c-0.1-0.1-0.1-0.3-0.1-0.4\n\t\t\t\tc0-0.9,0.5-1.7,1.3-2.2L18.2,27c0.4-0.2,0.9-0.1,1.1,0.3c0.1,0.1,0.1,0.3,0.1,0.4C19.4,28.7,18.9,29.5,18.2,29.9z\"\n            fill=\"#FFFFFF\"\n          />\n        </g>\n\n        <linearGradient\n          id=\"SVGID_25_\"\n          gradientTransform=\"matrix(-43.8881 0 0 -83.0886 18579.1777 139875)\"\n          gradientUnits=\"userSpaceOnUse\"\n          x1=\"422.0028\"\n          x2=\"421.7111\"\n          y1=\"1683.5527\"\n          y2=\"1682.4626\"\n        >\n          <stop offset=\"0\" style=\"stop-color: #fff\" />\n          <stop offset=\"1\" style=\"stop-color: #fff; stop-opacity: 0\" />\n        </linearGradient>\n        <polygon\n          enable-background=\"new    \"\n          fill=\"url(#SVGID_25_)\"\n          opacity=\"0.4\"\n          points=\"50,16.6 28.7,86.3 51,73.4 72.6,3.2 \t\t\"\n        />\n\n        <linearGradient\n          id=\"XMLID_22_\"\n          gradientTransform=\"matrix(-6.6055 0 0 -25.864 2720.3372 42898.332)\"\n          gradientUnits=\"userSpaceOnUse\"\n          x1=\"408.911\"\n          x2=\"408.7796\"\n          y1=\"1654.5455\"\n          y2=\"1655.6864\"\n        >\n          <stop offset=\"5.400000e-03\" style=\"stop-color: #130cb5\" />\n          <stop offset=\"0.2823\" style=\"stop-color: #285ecb\" />\n          <stop offset=\"0.538\" style=\"stop-color: #3aa3dd\" />\n          <stop offset=\"0.75\" style=\"stop-color: #47d5eb\" />\n          <stop offset=\"0.9102\" style=\"stop-color: #4ff4f3\" />\n          <stop offset=\"1\" style=\"stop-color: #52fff6\" />\n        </linearGradient>\n\n        <linearGradient\n          id=\"XMLID_23_\"\n          gradientTransform=\"matrix(-6.6055 0 0 -25.864 2720.3372 42898.332)\"\n          gradientUnits=\"userSpaceOnUse\"\n          x1=\"409.0583\"\n          x2=\"409.0583\"\n          y1=\"1656.3246\"\n          y2=\"1655.3246\"\n        >\n          <stop offset=\"0\" style=\"stop-color: #fff\" />\n          <stop offset=\"1\" style=\"stop-color: #fff; stop-opacity: 0\" />\n        </linearGradient>\n        <polygon\n          id=\"XMLID_875_\"\n          fill=\"url(#XMLID_22_)\"\n          points=\"21.6,81.2 15,85 15,63\n\t\t\t21.6,59.2 \t\t\"\n          stroke=\"url(#XMLID_23_)\"\n          stroke-width=\"0.5\"\n        />\n\n        <linearGradient\n          id=\"XMLID_24_\"\n          gradientTransform=\"matrix(-6.6055 0 0 -32.7555 2732.1479 54570.1641)\"\n          gradientUnits=\"userSpaceOnUse\"\n          x1=\"409.0159\"\n          x2=\"408.9963\"\n          y1=\"1663.0452\"\n          y2=\"1663.531\"\n        >\n          <stop offset=\"5.400000e-03\" style=\"stop-color: #130cb5\" />\n          <stop offset=\"0.2823\" style=\"stop-color: #285ecb\" />\n          <stop offset=\"0.538\" style=\"stop-color: #3aa3dd\" />\n          <stop offset=\"0.75\" style=\"stop-color: #47d5eb\" />\n          <stop offset=\"0.9102\" style=\"stop-color: #4ff4f3\" />\n          <stop offset=\"1\" style=\"stop-color: #52fff6\" />\n        </linearGradient>\n\n        <linearGradient\n          id=\"XMLID_25_\"\n          gradientTransform=\"matrix(-6.6055 0 0 -32.7555 2732.1479 54570.1641)\"\n          gradientUnits=\"userSpaceOnUse\"\n          x1=\"409.0583\"\n          x2=\"409.0583\"\n          y1=\"1664.5974\"\n          y2=\"1663.5974\"\n        >\n          <stop offset=\"0\" style=\"stop-color: #fff\" />\n          <stop offset=\"1\" style=\"stop-color: #fff; stop-opacity: 0\" />\n        </linearGradient>\n        <polygon\n          id=\"XMLID_874_\"\n          fill=\"url(#XMLID_24_)\"\n          points=\"33.4,74.4 26.8,78.2\n\t\t\t26.8,49.3 33.4,45.4 \t\t\"\n          stroke=\"url(#XMLID_25_)\"\n          stroke-width=\"0.5\"\n        />\n\n        <linearGradient\n          id=\"XMLID_26_\"\n          gradientTransform=\"matrix(-6.6055 0 0 -39.6465 2744.3577 66240.9141)\"\n          gradientUnits=\"userSpaceOnUse\"\n          x1=\"408.98\"\n          x2=\"408.9241\"\n          y1=\"1668.4514\"\n          y2=\"1669.1958\"\n        >\n          <stop offset=\"5.400000e-03\" style=\"stop-color: #130cb5\" />\n          <stop offset=\"0.2823\" style=\"stop-color: #285ecb\" />\n          <stop offset=\"0.538\" style=\"stop-color: #3aa3dd\" />\n          <stop offset=\"0.75\" style=\"stop-color: #47d5eb\" />\n          <stop offset=\"0.9102\" style=\"stop-color: #4ff4f3\" />\n          <stop offset=\"1\" style=\"stop-color: #52fff6\" />\n        </linearGradient>\n\n        <linearGradient\n          id=\"XMLID_27_\"\n          gradientTransform=\"matrix(-6.6055 0 0 -39.6465 2744.3577 66240.9141)\"\n          gradientUnits=\"userSpaceOnUse\"\n          x1=\"409.0583\"\n          x2=\"409.0583\"\n          y1=\"1669.994\"\n          y2=\"1668.994\"\n        >\n          <stop offset=\"0\" style=\"stop-color: #fff\" />\n          <stop offset=\"1\" style=\"stop-color: #fff; stop-opacity: 0\" />\n        </linearGradient>\n        <polygon\n          id=\"XMLID_873_\"\n          fill=\"url(#XMLID_26_)\"\n          points=\"45.6,67.3 39,71.1 39,35.3\n\t\t\t45.6,31.5 \t\t\"\n          stroke=\"url(#XMLID_27_)\"\n          stroke-width=\"0.5\"\n        />\n      </g>\n      <g id=\"人备份\" transform=\"translate(364.000000, 134.000000)\">\n        <path\n          id=\"Path\"\n          clip-rule=\"evenodd\"\n          d=\"M78,41.6c0,0,6.5-5.1,7.6-5.4c7.2-2.4,8-1,9.4,0.7\n\t\t\tc1.4,1.6-4,4.2-5.4,5.7c-0.7,0.8,4.5-1,3.9,1c-0.5,1.5-4.9,2.2-5.6,2.9s-5.3,3.8-5.3,3.8L78,41.6z\"\n          fill=\"#F7C19C\"\n          fill-rule=\"evenodd\"\n        />\n\n        <linearGradient\n          id=\"SVGID_26_\"\n          gradientTransform=\"matrix(66.553 0 0 -42.1921 17559.9336 70059.375)\"\n          gradientUnits=\"userSpaceOnUse\"\n          x1=\"-263.0341\"\n          x2=\"-263.2938\"\n          y1=\"1659.3563\"\n          y2=\"1659.0363\"\n        >\n          <stop offset=\"0\" style=\"stop-color: #3158d3\" />\n          <stop offset=\"1\" style=\"stop-color: #306ef3\" />\n        </linearGradient>\n        <path\n          clip-rule=\"evenodd\"\n          d=\"M53,81.7c5.3-2,34.6-33.6,34.6-33.6\n\t\t\tc-3.3-7.5-9.4-7.6-9.4-7.6L50.5,61.6c0,0-24.3-19.7-28.7-21.7s12.7,33,12.7,33S47.8,83.7,53,81.7z\"\n          fill=\"url(#SVGID_26_)\"\n          fill-rule=\"evenodd\"\n        />\n        <g\n          id=\"Oval\"\n          enable-background=\"new    \"\n          filter=\"url(#filter-71)\"\n          opacity=\"0.5267\"\n        >\n          <linearGradient\n            id=\"SVGID_27_\"\n            gradientTransform=\"matrix(125.5104 0 0 -59.4816 33000.125 99355.7422)\"\n            gradientUnits=\"userSpaceOnUse\"\n            x1=\"-261.8036\"\n            x2=\"-262.479\"\n            y1=\"1666.1232\"\n            y2=\"1666.0233\"\n          >\n            <stop offset=\"0\" style=\"stop-color: #1b0f9c; stop-opacity: 0\" />\n            <stop offset=\"1\" style=\"stop-color: #060071\" />\n          </linearGradient>\n          <ellipse\n            clip-rule=\"evenodd\"\n            cx=\"77.2\"\n            cy=\"256.5\"\n            fill=\"url(#SVGID_27_)\"\n            fill-rule=\"evenodd\"\n            rx=\"62.8\"\n            ry=\"29.7\"\n          />\n        </g>\n\n        <linearGradient\n          id=\"SVGID_28_\"\n          gradientTransform=\"matrix(37.5807 0 0 -16.9728 9948.6885 27801.6328)\"\n          gradientUnits=\"userSpaceOnUse\"\n          x1=\"-264.5023\"\n          x2=\"-262.5973\"\n          y1=\"1623.2751\"\n          y2=\"1622.7434\"\n        >\n          <stop offset=\"0\" style=\"stop-color: #434343\" />\n          <stop offset=\"1\" style=\"stop-color: #122142\" />\n        </linearGradient>\n        <path\n          clip-rule=\"evenodd\"\n          d=\"M6.3,242.5c-1.9,1.4-7.6,6.1-5.2,12.6\n\t\t\tc2.4,6.5,8.7,4.1,12.8,2.9c4.1-1.2,9.3-4.2,15.3-5.1c6-0.9,12.9-5.5,5.8-8.6C30.3,242.2,6.3,242.5,6.3,242.5z\"\n          fill=\"url(#SVGID_28_)\"\n          fill-rule=\"evenodd\"\n        />\n        <path\n          clip-rule=\"evenodd\"\n          d=\"M51.7,252.1l-2.2,9.5c0,0-5.1,0.3-8.5-1.4\n\t\t\tc-1.1-0.5-2-1.3-2.5-2.3l2.8-13.2l10.3-65.3l-9.3-33.8L16.9,246.6c0,0-3.1,1.6-6.5,0.4c-0.7-0.3-3.6-2-4.1-2.9l29.2-106.8l20,4.8\n\t\t\tl8.5,45L51.7,252.1z\"\n          fill=\"#EFAD81\"\n          fill-rule=\"evenodd\"\n        />\n        <path\n          clip-rule=\"evenodd\"\n          d=\"M34.8,39.4l1.4-8.4c0,0-2-21.1,3.8-23.2s18.3-0.3,18.4,10\n\t\t\tc0.1,10.4-0.1,19.3-4.6,19.1c-4.5-0.2-5.3-1-5.3-1l0.1,8.1L34.8,39.4z\"\n          fill=\"#F7C19C\"\n          fill-rule=\"evenodd\"\n        />\n\n        <linearGradient\n          id=\"SVGID_29_\"\n          gradientTransform=\"matrix(65.856 0 0 -139.1891 17361.0918 233445.5938)\"\n          gradientUnits=\"userSpaceOnUse\"\n          x1=\"-263.1151\"\n          x2=\"-263.0032\"\n          y1=\"1676.3293\"\n          y2=\"1675.5613\"\n        >\n          <stop offset=\"0\" style=\"stop-color: #021814\" />\n          <stop offset=\"1\" style=\"stop-color: #424fec\" />\n        </linearGradient>\n        <path\n          d=\"M17.5,117.5l1.9,57.1L4.8,238c0,0,3.3,5.5,18.1,4.1c0,0,16.4-56.8,18-59.2\n\t\t\tc1.6-2.4,4.4-21.7,4.4-21.7s2.5,29.8,1.7,32.9c-0.8,3.2-8.4,56.9-8.4,56.9s3.1,7.4,17,5.2c0,0,15-62,15-67.8v-62.1L17.5,117.5z\"\n          fill=\"url(#SVGID_29_)\"\n        />\n\n        <linearGradient\n          id=\"SVGID_30_\"\n          gradientTransform=\"matrix(38.3908 0 0 -17.2271 10193.9805 28243.3203)\"\n          gradientUnits=\"userSpaceOnUse\"\n          x1=\"-264.4597\"\n          x2=\"-262.5546\"\n          y1=\"1624.1052\"\n          y2=\"1623.5803\"\n        >\n          <stop offset=\"0\" style=\"stop-color: #434343\" />\n          <stop offset=\"1\" style=\"stop-color: #122142\" />\n        </linearGradient>\n        <path\n          clip-rule=\"evenodd\"\n          d=\"M50.9,258.4c0,0-2.9,1.7-6.7,0.7c-3.8-1-4.4-3-5.6-3.1\n\t\t\tc-1.2-0.1-5,6.2-5,11.4c0,3.4,4.2,6.7,13.4,5.6c10.7-1.3,9.7,1,19-1c9.3-1.9,6.1-7.6,0.7-8.2c-5.4-0.6-10.7-1.5-12.7-3\n\t\t\tC52.1,259.4,50.9,258.4,50.9,258.4z\"\n          fill=\"url(#SVGID_30_)\"\n          fill-rule=\"evenodd\"\n        />\n        <path\n          clip-rule=\"evenodd\"\n          d=\"M129.6,67.3c-0.6,0.2-1.7,0.5-2.9,0.8c-0.6-0.4-1.4-2.4-2-2.7\n\t\t\tc-1.2-0.5-3.1,3.8-3.9,4.9l-4.9,2.6l-0.9,9.2c2.5,0.1,5-1.1,7.5-1.4c1.4-0.3,13.7-6.6,15.5-10.8C139,67.4,133.6,65.7,129.6,67.3z\"\n          fill=\"#F7C19C\"\n          fill-rule=\"evenodd\"\n        />\n\n        <linearGradient\n          id=\"SVGID_31_\"\n          gradientTransform=\"matrix(107.5405 0 0 -93.2162 28291.4941 155960.2031)\"\n          gradientUnits=\"userSpaceOnUse\"\n          x1=\"-261.9086\"\n          x2=\"-262.8504\"\n          y1=\"1672.3391\"\n          y2=\"1672.2106\"\n        >\n          <stop offset=\"0\" style=\"stop-color: #77f7e1\" />\n          <stop offset=\"1\" style=\"stop-color: #424fec\" />\n        </linearGradient>\n        <path\n          d=\"M76.5,100.3c5.7,0.2,44.9-17.6,44.9-17.6c-0.2-8.2-5.8-10.7-5.8-10.7l-33.7,8.6\n\t\t\tc0,0-14.7-27.5-18-31.1C60.7,46,49.1,40,49.1,40S39,43.3,34.6,35c0,0-3.2-0.8-8.7-0.8S12.3,46,14.1,69.2s2,53,2,53\n\t\t\ts16.7,7.8,54.5,4.2l-7.9-41.5C62.8,84.9,70.9,100.2,76.5,100.3z\"\n          fill=\"url(#SVGID_31_)\"\n        />\n\n        <linearGradient\n          id=\"SVGID_32_\"\n          gradientTransform=\"matrix(33.4128 0 0 -34.3382 8885.7266 56796.8203)\"\n          gradientUnits=\"userSpaceOnUse\"\n          x1=\"-264.4242\"\n          x2=\"-265.0254\"\n          y1=\"1653.8715\"\n          y2=\"1653.0199\"\n        >\n          <stop offset=\"0\" style=\"stop-color: #253c6f\" />\n          <stop offset=\"1\" style=\"stop-color: #3068e8\" />\n        </linearGradient>\n        <path\n          clip-rule=\"evenodd\"\n          d=\"M53,18.9c0,0,10.3,1.5,10-5.4c-0.2-6.9-4.7-3.6-5.9-9.4\n\t\t\tc-1.4-7-11-2.3-22.3,0c-14.1,2.8,4.7,43.2,4.6,26.7s1.7-4.4,7.1-5.3c0.8-0.6-1.1-2.6,0-3.9C48.3,19.9,53,18.9,53,18.9z\"\n          fill=\"url(#SVGID_32_)\"\n          fill-rule=\"evenodd\"\n        />\n      </g>\n      <g id=\"Stroke-1备份-3\" opacity=\"0.2645\">\n        <g filter=\"url(#filter-79)\">\n          <path\n            id=\"path-78_1_\"\n            clip-rule=\"evenodd\"\n            d=\"M322.9,141.3c-49.2,28.4-49.2,75,0,103.4\n\t\t\t\tc49.2,28.4,129.8,28.4,179,0c49.2-28.4,49.2-75,0-103.4C452.7,112.9,372.2,112.9,322.9,141.3z\"\n            fill-rule=\"evenodd\"\n          />\n        </g>\n        <g>\n          <linearGradient\n            id=\"SVGID_33_\"\n            gradientTransform=\"matrix(252.8604 0 0 -146.0971 -25403.7617 264655.8438)\"\n            gradientUnits=\"userSpaceOnUse\"\n            x1=\"102.0966\"\n            x2=\"102.0966\"\n            y1=\"1810.3647\"\n            y2=\"1809.6846\"\n          >\n            <stop offset=\"0\" style=\"stop-color: #fff; stop-opacity: 0\" />\n            <stop offset=\"1\" style=\"stop-color: #fff\" />\n          </linearGradient>\n          <path\n            d=\"M322.9,141.3c-49.2,28.4-49.2,75,0,103.4c49.2,28.4,129.8,28.4,179,0\n\t\t\t\tc49.2-28.4,49.2-75,0-103.4C452.7,112.9,372.2,112.9,322.9,141.3z\"\n            fill=\"none\"\n            stroke=\"url(#SVGID_33_)\"\n          />\n        </g>\n      </g>\n\n      <linearGradient\n        id=\"Stroke-1备份-4_1_\"\n        gradientTransform=\"matrix(224 0 0 -130 -22445.5664 235373.9844)\"\n        gradientUnits=\"userSpaceOnUse\"\n        x1=\"102.0446\"\n        x2=\"102.0446\"\n        y1=\"1809.8226\"\n        y2=\"1809.215\"\n      >\n        <stop offset=\"0\" style=\"stop-color: #fff\" />\n        <stop offset=\"1\" style=\"stop-color: #fff; stop-opacity: 0\" />\n      </linearGradient>\n      <path\n        id=\"Stroke-1备份-4\"\n        d=\"\n\t\tM333.1,116c-43.6,25.3-43.6,66.7,0,92c43.6,25.3,115,25.3,158.6,0c43.6-25.3,43.6-66.7,0-92C448.1,90.7,376.8,90.7,333.1,116z\"\n        enable-background=\"new    \"\n        fill=\"none\"\n        opacity=\"0.2645\"\n        stroke=\"url(#Stroke-1备份-4_1_)\"\n      />\n\n      <linearGradient\n        id=\"Stroke-1备份-5_1_\"\n        gradientTransform=\"matrix(224 0 0 -130 -22445.5664 235434.9375)\"\n        gradientUnits=\"userSpaceOnUse\"\n        x1=\"102.0446\"\n        x2=\"102.0446\"\n        y1=\"1809.8226\"\n        y2=\"1809.215\"\n      >\n        <stop offset=\"0\" style=\"stop-color: #fff\" />\n        <stop offset=\"1\" style=\"stop-color: #fff; stop-opacity: 0\" />\n      </linearGradient>\n\n      <path\n        id=\"Stroke-1备份-5\"\n        d=\"\n\t\tM333.1,177c-43.6,25.3-43.6,66.7,0,92c43.6,25.3,115,25.3,158.6,0c43.6-25.3,43.6-66.7,0-92C448.1,151.7,376.8,151.7,333.1,177z\"\n        enable-background=\"new    \"\n        fill=\"none\"\n        opacity=\"7.956659e-02\"\n        stroke=\"url(#Stroke-1备份-5_1_)\"\n      />\n      <g id=\"编组-15\" transform=\"translate(286.000000, 69.000000)\">\n        <linearGradient\n          id=\"SVGID_34_\"\n          gradientTransform=\"matrix(84.647 0 0 -130.0737 15677.0488 226435.8125)\"\n          gradientUnits=\"userSpaceOnUse\"\n          x1=\"-184.6048\"\n          x2=\"-184.7916\"\n          y1=\"1739.7522\"\n          y2=\"1740.8326\"\n        >\n          <stop offset=\"0\" style=\"stop-color: #4587ef\" />\n          <stop offset=\"1\" style=\"stop-color: #00209e\" />\n        </linearGradient>\n        <path\n          clip-rule=\"evenodd\"\n          d=\"M84.6,54\n\t\t\tv71.1c0,3.8-2.9,5.8-6.4,4.6c-14-5-27.2-11.1-39.4-18.1C25.4,103.8,14,95.3,4.7,86.3C2.1,83.6,0,78.4,0,74.6V3.5\n\t\t\tc0-3.8,2.1-4.6,4.7-1.9c9.3,9,20.6,17.6,34.1,25.3C51,33.9,64.2,39.9,78.2,45C81.8,46.2,84.6,50.2,84.6,54z\"\n          enable-background=\"new    \"\n          fill=\"url(#SVGID_34_)\"\n          fill-rule=\"evenodd\"\n          opacity=\"0.5\"\n        />\n\n        <linearGradient\n          id=\"SVGID_35_\"\n          gradientTransform=\"matrix(42.8864 0 0 -55.1569 8005.1216 95474.0859)\"\n          gradientUnits=\"userSpaceOnUse\"\n          x1=\"-186.8636\"\n          x2=\"-185.8636\"\n          y1=\"1729.7074\"\n          y2=\"1729.7074\"\n        >\n          <stop offset=\"0\" style=\"stop-color: #4c83ff\" />\n          <stop offset=\"1\" style=\"stop-color: #2afadf\" />\n        </linearGradient>\n        <path\n          d=\"M33.5,44.1c11.8,6.8,21.4,23.4,21.4,37\n\t\t\tc0,13.6-9.7,19.1-21.5,12.3C21.6,86.6,12,70,12,56.4C12.1,42.8,21.7,37.3,33.5,44.1z M33.4,85.5c8,4.6,14.6,0.9,14.6-8.3\n\t\t\tc0-9.2-6.5-20.5-14.5-25.1c-8-4.6-14.6-0.9-14.6,8.3C18.9,69.6,25.4,80.9,33.4,85.5L33.4,85.5z\"\n          enable-background=\"new    \"\n          fill=\"url(#SVGID_35_)\"\n          opacity=\"0.4\"\n        />\n\n        <linearGradient\n          id=\"SVGID_36_\"\n          gradientTransform=\"matrix(21.3932 0 0 -37.017 4071.8147 63756.6836)\"\n          gradientUnits=\"userSpaceOnUse\"\n          x1=\"-189.5823\"\n          x2=\"-188.5823\"\n          y1=\"1720.6718\"\n          y2=\"1720.6718\"\n        >\n          <stop offset=\"0\" style=\"stop-color: #4c83ff\" />\n          <stop offset=\"1\" style=\"stop-color: #2afadf\" />\n        </linearGradient>\n        <path\n          clip-rule=\"evenodd\"\n          d=\"M33.5,44.1c11.8,6.8,21.4,23.4,21.4,37l-6.9-4\n\t\t\tc0-9.2-6.5-20.5-14.5-25.1L33.5,44.1z\"\n          fill=\"url(#SVGID_36_)\"\n          fill-rule=\"evenodd\"\n        />\n      </g>\n\n      <g\n        id=\"编组-15备份\"\n        transform=\"translate(524.500000, 179.500000) scale(-1, 1) translate(-524.500000, -179.500000) translate(482.000000, 114.000000)\"\n      >\n        <linearGradient\n          id=\"SVGID_37_\"\n          gradientTransform=\"matrix(-84.647 0 0 -130.0737 39258.8555 220582.5)\"\n          gradientUnits=\"userSpaceOnUse\"\n          x1=\"463.1948\"\n          x2=\"463.008\"\n          y1=\"1694.7522\"\n          y2=\"1695.8326\"\n        >\n          <stop offset=\"0\" style=\"stop-color: #4587ef\" />\n          <stop offset=\"1\" style=\"stop-color: #00209e\" />\n        </linearGradient>\n        <path\n          clip-rule=\"evenodd\"\n          d=\"M84.6,54\n\t\t\tv71.1c0,3.8-2.9,5.8-6.4,4.6c-14-5-27.2-11.1-39.4-18.1C25.4,103.8,14,95.3,4.7,86.3C2.1,83.6,0,78.4,0,74.6V3.5\n\t\t\tc0-3.8,2.1-4.6,4.7-1.9c9.3,9,20.6,17.6,34.1,25.3C51,33.9,64.2,39.9,78.2,45C81.8,46.2,84.6,50.2,84.6,54z\"\n          enable-background=\"new    \"\n          fill=\"url(#SVGID_37_)\"\n          fill-rule=\"evenodd\"\n          opacity=\"0.5\"\n        />\n\n        <linearGradient\n          id=\"SVGID_38_\"\n          gradientTransform=\"matrix(-42.8864 0 0 -55.1569 19852.209 92992.0234)\"\n          gradientUnits=\"userSpaceOnUse\"\n          x1=\"462.6216\"\n          x2=\"463.1363\"\n          y1=\"1684.7074\"\n          y2=\"1684.7074\"\n        >\n          <stop offset=\"0\" style=\"stop-color: #ccff70\" />\n          <stop offset=\"1\" style=\"stop-color: #2afadf\" />\n        </linearGradient>\n        <path\n          d=\"M33.5,44.1c11.8,6.8,21.4,23.4,21.4,37\n\t\t\tc0,13.6-9.7,19.1-21.5,12.3C21.6,86.6,12,70,12,56.4C12.1,42.8,21.7,37.3,33.5,44.1z M33.4,85.5c8,4.6,14.6,0.9,14.6-8.3\n\t\t\tc0-9.2-6.5-20.5-14.5-25.1c-8-4.6-14.6-0.9-14.6,8.3C18.9,69.6,25.4,80.9,33.4,85.5L33.4,85.5z\"\n          enable-background=\"new    \"\n          fill=\"url(#SVGID_38_)\"\n          opacity=\"0.4\"\n        />\n\n        <linearGradient\n          id=\"SVGID_39_\"\n          gradientTransform=\"matrix(-21.3932 0 0 -37.017 9879.3115 62090.918)\"\n          gradientUnits=\"userSpaceOnUse\"\n          x1=\"461.0466\"\n          x2=\"462.0466\"\n          y1=\"1675.6718\"\n          y2=\"1675.6718\"\n        >\n          <stop offset=\"0\" style=\"stop-color: #4c83ff\" />\n          <stop offset=\"1\" style=\"stop-color: #2afa91\" />\n        </linearGradient>\n        <path\n          clip-rule=\"evenodd\"\n          d=\"M33.5,44.1c11.8,6.8,21.4,23.4,21.4,37l-6.9-4\n\t\t\tc0-9.2-6.5-20.5-14.5-25.1L33.5,44.1z\"\n          fill=\"url(#SVGID_39_)\"\n          fill-rule=\"evenodd\"\n        />\n      </g>\n      <g id=\"编组\" transform=\"translate(457.000000, 235.000000)\">\n        <linearGradient\n          id=\"SVGID_40_\"\n          gradientTransform=\"matrix(94.2244 0 0 -135.8154 33551.6523 213971.6875)\"\n          gradientUnits=\"userSpaceOnUse\"\n          x1=\"-355.4666\"\n          x2=\"-355.6789\"\n          y1=\"1574.0848\"\n          y2=\"1575.1653\"\n        >\n          <stop offset=\"0\" style=\"stop-color: #4587ef\" />\n          <stop offset=\"1\" style=\"stop-color: #00209e\" />\n        </linearGradient>\n        <path\n          clip-rule=\"evenodd\"\n          d=\"M36.3,166.1\n\t\t\tc-9.3,4.2-19.3,7.7-29.7,10.5c-3.6,1-6.6-1.4-6.6-5.1v-44.7c0-3.8,3-7.5,6.6-8.4C17,115.5,27,112,36.3,107.8\n\t\t\tc4.8-2.2,9.5-4.6,13.9-7.2c28.4-16.5,43.1-38,44-59.6v54.4c0.8,23-13.8,46-44,63.6C45.8,161.5,41.1,163.9,36.3,166.1z\"\n          enable-background=\"new    \"\n          fill=\"url(#SVGID_40_)\"\n          fill-rule=\"evenodd\"\n          opacity=\"0.5\"\n        />\n\n        <linearGradient\n          id=\"SVGID_41_\"\n          gradientTransform=\"matrix(19.3679 0 0 -68.3208 7052.4458 107110.6953)\"\n          gradientUnits=\"userSpaceOnUse\"\n          x1=\"-359.7519\"\n          x2=\"-359.7874\"\n          y1=\"1566.6797\"\n          y2=\"1567.7601\"\n        >\n          <stop offset=\"0\" style=\"stop-color: #4587ef\" />\n          <stop offset=\"1\" style=\"stop-color: #00209e\" />\n        </linearGradient>\n        <path\n          clip-rule=\"evenodd\"\n          d=\"M79.4,59.9\n\t\t\tc-2.4-2.9-4.6-8.1-4.6-11.8V3.5c0-3.8,2.2-4.6,4.6-1.8c17.4,20.9,19.5,45,6.1,66.6C83.7,65.5,81.7,62.7,79.4,59.9z\"\n          enable-background=\"new    \"\n          fill=\"url(#SVGID_41_)\"\n          fill-rule=\"evenodd\"\n          opacity=\"0.5\"\n        />\n\n        <linearGradient\n          id=\"SVGID_42_\"\n          gradientTransform=\"matrix(5.707 0 0 -15.38 2179.3494 23459.5469)\"\n          gradientUnits=\"userSpaceOnUse\"\n          x1=\"-372.3728\"\n          x2=\"-372.3728\"\n          y1=\"1517.4354\"\n          y2=\"1516.2238\"\n        >\n          <stop offset=\"0\" style=\"stop-color: #4c83ff\" />\n          <stop offset=\"1\" style=\"stop-color: #2afadf\" />\n        </linearGradient>\n        <polygon\n          clip-rule=\"evenodd\"\n          fill=\"url(#SVGID_42_)\"\n          fill-rule=\"evenodd\"\n          points=\"57.1,146.8 57,135.5 51.4,138.7 51.4,150.9\n\t\t\t\"\n        />\n\n        <linearGradient\n          id=\"Path备份_1_\"\n          gradientTransform=\"matrix(5.707 0 0 -15.38 2199.9822 23445.7344)\"\n          gradientUnits=\"userSpaceOnUse\"\n          x1=\"-372.3728\"\n          x2=\"-372.3728\"\n          y1=\"1517.4354\"\n          y2=\"1516.2238\"\n        >\n          <stop offset=\"0\" style=\"stop-color: #4c83ff\" />\n          <stop offset=\"1\" style=\"stop-color: #2afadf\" />\n        </linearGradient>\n        <polygon\n          id=\"Path备份\"\n          clip-rule=\"evenodd\"\n          fill=\"url(#Path备份_1_)\"\n          fill-rule=\"evenodd\"\n          points=\"77.7,133 77.7,121.7\n\t\t\t72,124.9 72,137 \t\t\"\n        />\n\n        <linearGradient\n          id=\"SVGID_43_\"\n          gradientTransform=\"matrix(5.791 0 0 -44.69 2175.166 69834.2344)\"\n          gradientUnits=\"userSpaceOnUse\"\n          x1=\"-372.1319\"\n          x2=\"-372.1109\"\n          y1=\"1562.8905\"\n          y2=\"1558.8889\"\n        >\n          <stop offset=\"0\" style=\"stop-color: #4c83ff\" />\n          <stop offset=\"1\" style=\"stop-color: #2afadf\" />\n        </linearGradient>\n        <path\n          clip-rule=\"evenodd\"\n          d=\"M23.2,165.4c-1.9,0.7-3.8,1.4-5.7,2.1l-0.1-42.8\n\t\t\tc1.9-0.6,3.8-1.3,5.7-1.9L23.2,165.4z\"\n          fill=\"url(#SVGID_43_)\"\n          fill-rule=\"evenodd\"\n        />\n\n        <linearGradient\n          id=\"SVGID_44_\"\n          gradientTransform=\"matrix(5.754 0 0 -21.635 2173.4099 33364.9844)\"\n          gradientUnits=\"userSpaceOnUse\"\n          x1=\"-372.2268\"\n          x2=\"-372.2268\"\n          y1=\"1536.7212\"\n          y2=\"1535.3361\"\n        >\n          <stop offset=\"0\" style=\"stop-color: #4c83ff\" />\n          <stop offset=\"1\" style=\"stop-color: #2afadf\" />\n        </linearGradient>\n        <path\n          clip-rule=\"evenodd\"\n          d=\"M34.5,160.5c-1,0.5-2,1-3.1,1.4\n\t\t\tc-0.8,0.4-1.7,0.8-2.6,1.1L28.7,144c0,0,0.1,0,0.1,0c0.9-0.4,1.7-0.8,2.6-1.1c1-0.5,2-0.9,3-1.4L34.5,160.5z\"\n          fill=\"url(#SVGID_44_)\"\n          fill-rule=\"evenodd\"\n        />\n\n        <linearGradient\n          id=\"SVGID_45_\"\n          gradientTransform=\"matrix(5.78 0 0 -42.615 2194.2666 66542.2656)\"\n          gradientUnits=\"userSpaceOnUse\"\n          x1=\"-372.1471\"\n          x2=\"-372.1471\"\n          y1=\"1559.7903\"\n          y2=\"1558.4763\"\n        >\n          <stop offset=\"0\" style=\"stop-color: #4c83ff\" />\n          <stop offset=\"1\" style=\"stop-color: #2afadf\" />\n        </linearGradient>\n        <path\n          clip-rule=\"evenodd\"\n          d=\"M46.1,154.2c-0.6,0.4-1.2,0.7-1.9,1.1\n\t\t\tc-1.2,0.7-2.5,1.4-3.8,2.1l-0.1-39.3l5.7-3.3L46.1,154.2z\"\n          fill=\"url(#SVGID_45_)\"\n          fill-rule=\"evenodd\"\n        />\n\n        <linearGradient\n          id=\"Path备份-2_1_\"\n          gradientTransform=\"matrix(5.78 0 0 -28.2102 2214.8997 43745.1641)\"\n          gradientUnits=\"userSpaceOnUse\"\n          x1=\"-372.1471\"\n          x2=\"-372.1471\"\n          y1=\"1547.6044\"\n          y2=\"1546.2904\"\n        >\n          <stop offset=\"0\" style=\"stop-color: #4c83ff\" />\n          <stop offset=\"1\" style=\"stop-color: #2afadf\" />\n        </linearGradient>\n        <path\n          id=\"Path备份-2\"\n          clip-rule=\"evenodd\"\n          d=\"M66.8,140.4\n\t\t\tc-0.6,0.4-1.2,0.7-1.9,1.1c-1.2,0.7-2.5,1.4-3.8,2.1L61,118.7l5.7-3.3L66.8,140.4z\"\n          fill=\"url(#Path备份-2_1_)\"\n          fill-rule=\"evenodd\"\n        />\n      </g>\n\n      <g\n        id=\"编组备份\"\n        transform=\"translate(316.500000, 268.500000) scale(-1, 1) translate(-316.500000, -268.500000) translate(269.000000, 180.000000)\"\n      >\n        <linearGradient\n          id=\"SVGID_46_\"\n          gradientTransform=\"matrix(-94.2244 0 0 -135.8154 24584.7852 221441.5469)\"\n          gradientUnits=\"userSpaceOnUse\"\n          x1=\"260.3016\"\n          x2=\"260.0893\"\n          y1=\"1629.0848\"\n          y2=\"1630.1653\"\n        >\n          <stop offset=\"0\" style=\"stop-color: #4587ef\" />\n          <stop offset=\"1\" style=\"stop-color: #00209e\" />\n        </linearGradient>\n        <path\n          clip-rule=\"evenodd\"\n          d=\"M36.3,166.1\n\t\t\tc-9.3,4.2-19.3,7.7-29.7,10.5c-3.6,1-6.6-1.4-6.6-5.1v-44.7c0-3.8,3-7.5,6.6-8.4C17,115.5,27,112,36.3,107.8\n\t\t\tc4.8-2.2,9.5-4.6,13.9-7.2c28.4-16.5,43.1-38,44-59.6v54.4c0.8,23-13.8,46-44,63.6C45.8,161.5,41.1,163.9,36.3,166.1z\"\n          enable-background=\"new    \"\n          fill=\"url(#SVGID_46_)\"\n          fill-rule=\"evenodd\"\n          opacity=\"0.5\"\n        />\n\n        <linearGradient\n          id=\"SVGID_47_\"\n          gradientTransform=\"matrix(-19.3679 0 0 -68.3208 5047.2349 110868.3359)\"\n          gradientUnits=\"userSpaceOnUse\"\n          x1=\"256.219\"\n          x2=\"256.1836\"\n          y1=\"1621.6797\"\n          y2=\"1622.7601\"\n        >\n          <stop offset=\"0\" style=\"stop-color: #4587ef\" />\n          <stop offset=\"1\" style=\"stop-color: #00209e\" />\n        </linearGradient>\n        <path\n          clip-rule=\"evenodd\"\n          d=\"M79.4,59.9\n\t\t\tc-2.4-2.9-4.6-8.1-4.6-11.8V3.5c0-3.8,2.2-4.6,4.6-1.8c17.4,20.9,19.5,45,6.1,66.6C83.7,65.5,81.7,62.7,79.4,59.9z\"\n          enable-background=\"new    \"\n          fill=\"url(#SVGID_47_)\"\n          fill-rule=\"evenodd\"\n          opacity=\"0.5\"\n        />\n\n        <linearGradient\n          id=\"路径_1_\"\n          gradientTransform=\"matrix(-65.0245 0 0 -44.112 16953.9395 71322.6641)\"\n          gradientUnits=\"userSpaceOnUse\"\n          x1=\"259.3418\"\n          x2=\"258.6121\"\n          y1=\"1614.2808\"\n          y2=\"1614.0671\"\n        >\n          <stop offset=\"0\" style=\"stop-color: #f8ff24\" />\n          <stop offset=\"1\" style=\"stop-color: #fff\" />\n        </linearGradient>\n\n        <path\n          id=\"路径\"\n          d=\"\n\t\t\tM84.5,110.9c-11.6-15.5-17.4-14.7-17.4,2.4c0,25.7-11.4,29.9-19.6,17.1c-8.2-12.9-23.3-14.6-28,13.6\"\n          enable-background=\"new    \"\n          fill=\"none\"\n          opacity=\"0.6\"\n          stroke=\"url(#路径_1_)\"\n          stroke-linecap=\"round\"\n          stroke-width=\"3.5247\"\n        />\n      </g>\n      <g id=\"编组-11\" transform=\"translate(558.000000, 306.000000)\">\n        <g id=\"product3\" transform=\"translate(11.447727, 0.000000)\">\n          <g filter=\"url(#filter-95)\">\n            <linearGradient\n              id=\"SVGID_48_\"\n              gradientTransform=\"matrix(174.1648 0 0 -101.4708 81515.7969 152491.3594)\"\n              gradientUnits=\"userSpaceOnUse\"\n              x1=\"-467.0351\"\n              x2=\"-467.7105\"\n              y1=\"1501.2344\"\n              y2=\"1501.0835\"\n            >\n              <stop offset=\"0\" style=\"stop-color: #1b0f9c; stop-opacity: 0\" />\n              <stop offset=\"1\" style=\"stop-color: #060071\" />\n            </linearGradient>\n            <path\n              clip-rule=\"evenodd\"\n              d=\"M171.8,157.9c4.3,2.5,4.3,6.5,0,8.9L81.2,220\n\t\t\t\t\tc-4.8,2.5-10.6,2.5-15.4,0L4.1,184.4c-4.3-2.5-4.3-6.5,0-8.9l90.5-53.2c4.8-2.5,10.6-2.5,15.4,0L171.8,157.9z\"\n              fill=\"url(#SVGID_48_)\"\n              fill-rule=\"evenodd\"\n            />\n          </g>\n          <g id=\"编组-2\" transform=\"translate(0.862170, 124.812382)\">\n            <linearGradient\n              id=\"SVGID_49_\"\n              gradientTransform=\"matrix(145.0025 0 0 -83.8574 68008.0859 115279.1875)\"\n              gradientUnits=\"userSpaceOnUse\"\n              x1=\"-469.0133\"\n              x2=\"-468.0133\"\n              y1=\"1374.2056\"\n              y2=\"1374.2056\"\n            >\n              <stop offset=\"0\" style=\"stop-color: #6ecaf9\" />\n              <stop offset=\"0.35\" style=\"stop-color: #a1ddfb\" />\n              <stop offset=\"1\" style=\"stop-color: #fff\" />\n            </linearGradient>\n            <path\n              clip-rule=\"evenodd\"\n              d=\"M141.8,37.5c4.3,2.5,4.3,6.5,0,8.9L80.4,82\n\t\t\t\t\tc-4.8,2.5-10.6,2.5-15.4,0L3.2,46.4c-4.3-2.5-4.3-6.5,0-8.9L64.6,1.8c4.8-2.5,10.6-2.5,15.4,0L141.8,37.5z\"\n              fill=\"url(#SVGID_49_)\"\n              fill-rule=\"evenodd\"\n            />\n\n            <linearGradient\n              id=\"SVGID_50_\"\n              gradientTransform=\"matrix(132.2205 0 0 -73.006 62028.5859 100235.5703)\"\n              gradientUnits=\"userSpaceOnUse\"\n              x1=\"-469.0813\"\n              x2=\"-468.0813\"\n              y1=\"1372.403\"\n              y2=\"1372.403\"\n            >\n              <stop offset=\"0.12\" style=\"stop-color: #5c90fe\" />\n              <stop offset=\"0.56\" style=\"stop-color: #466cf5\" />\n              <stop offset=\"0.69\" style=\"stop-color: #4265f3\" />\n              <stop offset=\"0.87\" style=\"stop-color: #3853ef\" />\n              <stop offset=\"1\" style=\"stop-color: #2e42eb\" />\n            </linearGradient>\n            <path\n              clip-rule=\"evenodd\"\n              d=\"M72.8,78.5c-1.7,0.1-3.4-0.3-5-1.1L6.4,42L67.3,6.5\n\t\t\t\t\tc1.5-0.8,3.2-1.1,5-1.1c1.7-0.1,3.4,0.3,5,1.1L138.6,42L77.7,77.3C76.2,78.1,74.5,78.5,72.8,78.5z\"\n              fill=\"url(#SVGID_50_)\"\n              fill-rule=\"evenodd\"\n            />\n\n            <linearGradient\n              id=\"SVGID_51_\"\n              gradientTransform=\"matrix(144.9687 0 0 -54.9245 67992.2734 75196.0781)\"\n              gradientUnits=\"userSpaceOnUse\"\n              x1=\"-469.0135\"\n              x2=\"-468.0135\"\n              y1=\"1367.8169\"\n              y2=\"1367.8169\"\n            >\n              <stop offset=\"0.12\" style=\"stop-color: #5c90fe\" />\n              <stop offset=\"0.56\" style=\"stop-color: #466cf5\" />\n              <stop offset=\"0.69\" style=\"stop-color: #4265f3\" />\n              <stop offset=\"0.87\" style=\"stop-color: #3853ef\" />\n              <stop offset=\"1\" style=\"stop-color: #2e42eb\" />\n            </linearGradient>\n            <path\n              clip-rule=\"evenodd\"\n              d=\"M145,42c0,0.1,0,0.2,0,0.3s0,0.2,0,0.3s0,0.2-0.1,0.3\n\t\t\t\t\tc-0.1,0.1-0.1,0.2-0.1,0.3s-0.1,0.2-0.1,0.3c0,0.1-0.1,0.2-0.2,0.3l-0.2,0.2c-0.1,0.1-0.2,0.3-0.3,0.4l-0.2,0.2\n\t\t\t\t\tc-0.1,0.2-0.3,0.3-0.5,0.5l-0.2,0.2l-0.3,0.3l-0.3,0.2l-0.4,0.3l-0.4,0.3L80.5,82c-0.4,0.2-0.7,0.4-1.1,0.5l-0.3,0.1\n\t\t\t\t\tc-0.3,0.1-0.6,0.2-0.9,0.3l-0.3,0.1l-0.6,0.2l-0.4,0.1l-0.6,0.1l-0.4,0.1c-0.4,0.1-0.8,0.1-1.2,0.2h-0.4h-0.7H72h-0.6h-0.5\n\t\t\t\t\tl-0.6,0l-0.5-0.1l-0.6-0.1l-0.5-0.1l-0.7-0.2l-0.4-0.1c-0.4-0.1-0.8-0.2-1.2-0.4l-0.2-0.1c-0.5-0.2-1-0.4-1.5-0.7L3.2,46.5\n\t\t\t\t\tC1.1,45.2,0,43.6,0,42v13.3c0,1.6,1.1,3.2,3.2,4.5L65,95.4c0.5,0.3,1,0.5,1.5,0.7h0.1h0.1c0.4,0.2,0.8,0.3,1.2,0.4h0.1l0.3,0.1\n\t\t\t\t\tl0.7,0.2h0.2h0.3l0.6,0.1h0.2h0.3l0.5,0.1h0.3h0.3H72h1.6h0.7h0.3h0.1c0.4,0,0.8-0.1,1.2-0.2H76l0.4-0.1l0.5,0.1l0.4-0.1\n\t\t\t\t\tl0.6-0.2H78l0.2-0.1c0.3-0.1,0.6-0.2,0.9-0.3l0.3-0.1c0.4-0.2,0.7-0.3,1.1-0.5l61.4-35.6l0.1-0.1l0.3-0.2l0.4-0.3l0.3-0.2\n\t\t\t\t\tl0.3-0.3l0.1-0.1l0.1-0.1c0.2-0.2,0.3-0.3,0.5-0.5L144,58l0.1-0.1c0.1-0.1,0.2-0.2,0.3-0.4l0.1-0.2v-0.1\n\t\t\t\t\tc0.1-0.1,0.1-0.2,0.2-0.3s0.1-0.1,0.1-0.2v-0.1c0-0.1,0.1-0.2,0.1-0.3c0-0.1,0-0.2,0.1-0.2c0.1-0.1,0,0,0-0.1s0-0.2,0-0.3\n\t\t\t\t\ts0-0.2,0-0.2c0-0.1,0-0.1,0-0.1l0,0L145,42z\"\n              fill=\"url(#SVGID_51_)\"\n              fill-rule=\"evenodd\"\n            />\n          </g>\n          <g id=\"编组-2备份\" transform=\"translate(8.400000, 103.200000)\">\n            <g\n              id=\"Path备份-3\"\n              enable-background=\"new    \"\n              filter=\"url(#filter-100)\"\n              opacity=\"0.6817\"\n            >\n              <linearGradient\n                id=\"SVGID_52_\"\n                gradientTransform=\"matrix(130.5022 0 0 -75.4716 62201.1797 105298.6953)\"\n                gradientUnits=\"userSpaceOnUse\"\n                x1=\"-475.6305\"\n                x2=\"-476.3059\"\n                y1=\"1394.5802\"\n                y2=\"1394.4315\"\n              >\n                <stop offset=\"0\" style=\"stop-color: #1b0f9c; stop-opacity: 0\" />\n                <stop offset=\"1\" style=\"stop-color: #060071\" />\n              </linearGradient>\n              <path\n                clip-rule=\"evenodd\"\n                d=\"M127.6,51.7c3.9,2.2,3.9,5.8,0,8L72.3,91.8\n\t\t\t\t\t\tc-4.4,2.2-9.5,2.2-13.9,0L2.9,59.7c-3.8-2.2-3.9-5.8,0-8l55.2-32.1c4.4-2.2,9.5-2.2,13.9,0L127.6,51.7z\"\n                fill=\"url(#SVGID_52_)\"\n                fill-rule=\"evenodd\"\n              />\n            </g>\n\n            <linearGradient\n              id=\"SVGID_53_\"\n              gradientTransform=\"matrix(130.5022 0 0 -75.4716 62201.1797 105280.6953)\"\n              gradientUnits=\"userSpaceOnUse\"\n              x1=\"-476.6293\"\n              x2=\"-475.6293\"\n              y1=\"1394.4705\"\n              y2=\"1394.4705\"\n            >\n              <stop offset=\"0\" style=\"stop-color: #6ecaf9\" />\n              <stop offset=\"0.35\" style=\"stop-color: #a1ddfb\" />\n              <stop offset=\"1\" style=\"stop-color: #fff\" />\n            </linearGradient>\n            <path\n              clip-rule=\"evenodd\"\n              d=\"M127.6,33.7c3.9,2.2,3.9,5.8,0,8L72.3,73.8\n\t\t\t\t\tc-4.4,2.2-9.5,2.2-13.9,0L2.9,41.7c-3.8-2.2-3.9-5.8,0-8L58.1,1.7c4.4-2.2,9.5-2.2,13.9,0L127.6,33.7z\"\n              fill=\"url(#SVGID_53_)\"\n              fill-rule=\"evenodd\"\n            />\n\n            <linearGradient\n              id=\"SVGID_54_\"\n              gradientTransform=\"matrix(118.9985 0 0 -65.7054 56732.918 91530.375)\"\n              gradientUnits=\"userSpaceOnUse\"\n              x1=\"-476.7049\"\n              x2=\"-475.7049\"\n              y1=\"1392.4675\"\n              y2=\"1392.4675\"\n            >\n              <stop offset=\"0.12\" style=\"stop-color: #5c90fe\" />\n              <stop offset=\"0.56\" style=\"stop-color: #466cf5\" />\n              <stop offset=\"0.69\" style=\"stop-color: #4265f3\" />\n              <stop offset=\"0.87\" style=\"stop-color: #3853ef\" />\n              <stop offset=\"1\" style=\"stop-color: #2e42eb\" />\n            </linearGradient>\n            <path\n              clip-rule=\"evenodd\"\n              d=\"M65.5,70.6c-1.6,0.1-3.1-0.3-4.5-1L5.8,37.8L60.6,5.9\n\t\t\t\t\tc1.4-0.7,2.9-1,4.5-1c1.6,0,3.1,0.3,4.5,1l55.2,31.9L69.9,69.6C68.6,70.3,67,70.6,65.5,70.6z\"\n              fill=\"url(#SVGID_54_)\"\n              fill-rule=\"evenodd\"\n            />\n\n            <linearGradient\n              id=\"SVGID_55_\"\n              gradientTransform=\"matrix(130.4718 0 0 -49.432 62186.7266 68643.1094)\"\n              gradientUnits=\"userSpaceOnUse\"\n              x1=\"-476.6295\"\n              x2=\"-475.6295\"\n              y1=\"1387.3719\"\n              y2=\"1387.3719\"\n            >\n              <stop offset=\"0.12\" style=\"stop-color: #5c90fe\" />\n              <stop offset=\"0.56\" style=\"stop-color: #466cf5\" />\n              <stop offset=\"0.69\" style=\"stop-color: #4265f3\" />\n              <stop offset=\"0.87\" style=\"stop-color: #3853ef\" />\n              <stop offset=\"1\" style=\"stop-color: #2e42eb\" />\n            </linearGradient>\n            <path\n              clip-rule=\"evenodd\"\n              d=\"M130.5,37.8c0,0.1,0,0.2,0,0.3c0,0.1,0,0.2,0,0.3\n\t\t\t\t\ts0,0.2-0.1,0.3c-0.1,0.1,0,0.2-0.1,0.3s-0.1,0.2-0.1,0.2c0,0.1-0.1,0.2-0.1,0.3l-0.1,0.2c-0.1,0.1-0.1,0.2-0.2,0.3l-0.2,0.2\n\t\t\t\t\tc-0.1,0.2-0.3,0.3-0.4,0.4l-0.2,0.2l-0.3,0.3l-0.3,0.2l-0.3,0.2l-0.4,0.2L72.4,73.8c-0.3,0.2-0.6,0.3-1,0.5l-0.3,0.1\n\t\t\t\t\tc-0.3,0.1-0.5,0.2-0.8,0.3l-0.3,0.1l-0.5,0.1L69.2,75l-0.5,0.1l-0.4,0.1c-0.4,0.1-0.7,0.1-1.1,0.2h-0.3h-0.7h-1.4h-0.5h-0.5\n\t\t\t\t\tl-0.5,0l-0.4-0.1l-0.5-0.1L62,75.2L61.4,75L61,74.9c-0.4-0.1-0.7-0.2-1-0.4l-0.1-0.1c-0.5-0.2-0.9-0.4-1.3-0.6L2.9,41.8\n\t\t\t\t\tC1,40.7,0,39.3,0,37.8v12c0,1.5,1,2.9,2.9,4l55.6,32.1c0.4,0.2,0.9,0.5,1.3,0.6h0.1H60c0.3,0.1,0.7,0.3,1,0.3h0.1l0.3,0.1\n\t\t\t\t\tl0.6,0.1h0.2h0.2l0.5,0.1h0.2h0.2l0.5,0.1h0.2h0.3h0.5h1.4h0.7h0.3h0.1c0.4,0,0.7-0.1,1.1-0.2h0.1l0.3-0.1l0.4,0.1l0.4-0.1\n\t\t\t\t\tl0.5-0.1h0.1l0.2-0.1c0.3-0.1,0.5-0.2,0.8-0.3l0.3-0.1c0.3-0.1,0.7-0.3,1-0.5l55.2-32.1l0.1-0.1l0.3-0.2l0.3-0.2l0.3-0.2\n\t\t\t\t\tl0.3-0.3l0.1,0l0.1-0.1c0.1-0.1,0.3-0.3,0.4-0.4l0.1-0.1l0.1-0.1c0.1-0.1,0.2-0.2,0.2-0.3l0.1-0.1v-0.1c0.1-0.1,0.1-0.2,0.1-0.3\n\t\t\t\t\tc0-0.1,0.1-0.1,0.1-0.2V51c0-0.1,0.1-0.2,0.1-0.3s0-0.1,0.1-0.2c0.1-0.1,0,0,0,0s0-0.2,0-0.3c0-0.1,0-0.1,0-0.2s0-0.1,0-0.1l0,0\n\t\t\t\t\tL130.5,37.8z\"\n              fill=\"url(#SVGID_55_)\"\n              fill-rule=\"evenodd\"\n            />\n          </g>\n          <path\n            clip-rule=\"evenodd\"\n            d=\"M63.9,130.7l0.1,0.4\n\t\t\t\tL63.9,130.7z\"\n            fill=\"#666666\"\n            fill-rule=\"evenodd\"\n            stroke=\"#000000\"\n            stroke-width=\"0.6\"\n          />\n          <path\n            clip-rule=\"evenodd\"\n            d=\"M63.8,130l0.1,0.4\n\t\t\t\tL63.8,130z\"\n            fill=\"#666666\"\n            fill-rule=\"evenodd\"\n            stroke=\"#000000\"\n            stroke-width=\"0.6\"\n          />\n\n          <linearGradient\n            id=\"SVGID_56_\"\n            gradientTransform=\"matrix(132.2205 0 0 -178.9849 61915.457 269544.7812)\"\n            gradientUnits=\"userSpaceOnUse\"\n            x1=\"-467.7177\"\n            x2=\"-467.7209\"\n            y1=\"1504.9636\"\n            y2=\"1506.2531\"\n          >\n            <stop offset=\"0\" style=\"stop-color: #00dcf3\" />\n            <stop\n              offset=\"1.000000e-02\"\n              style=\"stop-color: #00dbf3; stop-opacity: 0.97\"\n            />\n            <stop\n              offset=\"8.000000e-02\"\n              style=\"stop-color: #00d0ef; stop-opacity: 0.77\"\n            />\n            <stop\n              offset=\"0.16\"\n              style=\"stop-color: #00c7ec; stop-opacity: 0.59\"\n            />\n            <stop\n              offset=\"0.24\"\n              style=\"stop-color: #00beea; stop-opacity: 0.43\"\n            />\n            <stop\n              offset=\"0.32\"\n              style=\"stop-color: #00b7e8; stop-opacity: 0.3\"\n            />\n            <stop\n              offset=\"0.42\"\n              style=\"stop-color: #00b2e6; stop-opacity: 0.19\"\n            />\n            <stop\n              offset=\"0.52\"\n              style=\"stop-color: #00ade5; stop-opacity: 0.1\"\n            />\n            <stop\n              offset=\"0.63\"\n              style=\"stop-color: #00aae4; stop-opacity: 4e-2\"\n            />\n            <stop\n              offset=\"0.77\"\n              style=\"stop-color: #00a9e3; stop-opacity: 1e-2\"\n            />\n            <stop offset=\"1\" style=\"stop-color: #00a8e3; stop-opacity: 0\" />\n          </linearGradient>\n          <path\n            clip-rule=\"evenodd\"\n            d=\"M7.3,0.1v142.4l61.4,35.4c1.5,0.8,3.3,1.1,5,1.1\n\t\t\t\tc1.7,0.1,3.4-0.3,5-1.1l60.9-35.4V0L7.3,0.1z\"\n            fill=\"url(#SVGID_56_)\"\n            fill-rule=\"evenodd\"\n          />\n        </g>\n        <g id=\"编组备份-8\" transform=\"translate(47.447727, 63.600000)\">\n          <g\n            enable-background=\"new    \"\n            filter=\"url(#filter-106)\"\n            opacity=\"0.3003\"\n          >\n            <linearGradient\n              id=\"SVGID_57_\"\n              gradientTransform=\"matrix(99.0186 0 0 -57.6674 49956.1875 82510.0469)\"\n              gradientUnits=\"userSpaceOnUse\"\n              x1=\"-503.4795\"\n              x2=\"-504.155\"\n              y1=\"1430.0212\"\n              y2=\"1429.8705\"\n            >\n              <stop offset=\"0\" style=\"stop-color: #1b0f9c; stop-opacity: 0\" />\n              <stop offset=\"1\" style=\"stop-color: #060071\" />\n            </linearGradient>\n            <polygon\n              clip-rule=\"evenodd\"\n              fill=\"url(#SVGID_57_)\"\n              fill-rule=\"evenodd\"\n              points=\"3.5,59.5 38.6,79.7 102.5,42.3 67.4,22.1\n\t\t\t\t\t\"\n            />\n          </g>\n          <g>\n            <g>\n              <linearGradient\n                id=\"path-108_1_\"\n                gradientTransform=\"matrix(70.176 0 0 -40.5146 35432.1289 57650.0859)\"\n                gradientUnits=\"userSpaceOnUse\"\n                x1=\"-504.9012\"\n                x2=\"-503.9012\"\n                y1=\"1422.4436\"\n                y2=\"1422.4436\"\n              >\n                <stop offset=\"0\" style=\"stop-color: #fff\" />\n                <stop offset=\"1\" style=\"stop-color: #00a2ff\" />\n              </linearGradient>\n              <polygon\n                id=\"path-108\"\n                clip-rule=\"evenodd\"\n                fill=\"url(#path-108_1_)\"\n                fill-rule=\"evenodd\"\n                points=\"0.2,20.4 35.3,40.6\n\t\t\t\t\t\t70.4,20.4 35.3,0.1 \t\t\t\t\t\"\n              />\n            </g>\n            <g filter=\"url(#filter-109)\">\n              <polygon\n                clip-rule=\"evenodd\"\n                fill-rule=\"evenodd\"\n                points=\"0.2,20.4 35.3,40.6 70.4,20.4 35.3,0.1 \t\t\t\t\t\"\n              />\n            </g>\n          </g>\n\n          <linearGradient\n            id=\"SVGID_58_\"\n            gradientTransform=\"matrix(35.088 0 0 -60.7744 17767.0781 87007.4453)\"\n            gradientUnits=\"userSpaceOnUse\"\n            x1=\"-505.8155\"\n            x2=\"-505.9297\"\n            y1=\"1431.3086\"\n            y2=\"1429.8619\"\n          >\n            <stop offset=\"0\" style=\"stop-color: #7bedff\" />\n            <stop offset=\"1\" style=\"stop-color: #060071\" />\n          </linearGradient>\n          <polygon\n            clip-rule=\"evenodd\"\n            fill=\"url(#SVGID_58_)\"\n            fill-rule=\"evenodd\"\n            points=\"35.2,81.1 35.2,40.6 0.1,20.4 0.1,60.9 \t\t\t\"\n          />\n          <g id=\"编组-10\" transform=\"translate(4.431004, 28.063025)\">\n            <g id=\"编组-8\">\n              <g id=\"Shape备份-3\" transform=\"translate(0.000000, -0.000000)\">\n                <g id=\"蒙版\">\n                  <linearGradient\n                    id=\"SVGID_59_\"\n                    gradientTransform=\"matrix(6.3405 0 0 -10.9369 3322.1904 14513.207)\"\n                    gradientUnits=\"userSpaceOnUse\"\n                    x1=\"-523.204\"\n                    x2=\"-524.0609\"\n                    y1=\"1326.9464\"\n                    y2=\"1325.4684\"\n                  >\n                    <stop offset=\"5.400000e-03\" style=\"stop-color: #fb6583\" />\n                    <stop offset=\"0.1302\" style=\"stop-color: #fc7688\" />\n                    <stop offset=\"0.5277\" style=\"stop-color: #fda797\" />\n                    <stop offset=\"0.8302\" style=\"stop-color: #ffc6a0\" />\n                    <stop offset=\"1\" style=\"stop-color: #ffd1a3\" />\n                  </linearGradient>\n                  <polygon\n                    fill=\"url(#SVGID_59_)\"\n                    points=\"0,7.3 6.3,10.9 6.3,3.7 0,0 \t\t\t\t\t\t\t\"\n                  />\n                </g>\n                <defs>\n                  <filter\n                    id=\"Adobe_OpacityMaskFilter_2_\"\n                    filterUnits=\"userSpaceOnUse\"\n                    height=\"10.9\"\n                    width=\"6.3\"\n                    x=\"0\"\n                    y=\"3.6\"\n                  >\n                    <feColorMatrix\n                      type=\"matrix\"\n                      values=\"1 0 0 0 0  0 1 0 0 0  0 0 1 0 0  0 0 0 1 0\"\n                    />\n                  </filter>\n                </defs>\n                <mask\n                  id=\"mask-113_1_\"\n                  height=\"10.9\"\n                  maskUnits=\"userSpaceOnUse\"\n                  width=\"6.3\"\n                  x=\"0\"\n                  y=\"3.6\"\n                >\n                  <g filter=\"url(#Adobe_OpacityMaskFilter_2_)\">\n                    <polygon\n                      id=\"path-112_1_\"\n                      clip-rule=\"evenodd\"\n                      fill=\"#FFFFFF\"\n                      fill-rule=\"evenodd\"\n                      points=\"0,7.3 6.3,10.9 6.3,3.7 0,0\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"\n                    />\n                  </g>\n                </mask>\n\n                <linearGradient\n                  id=\"SVGID_60_\"\n                  gradientTransform=\"matrix(-6.3405 0 0 -10.9369 -3111.8501 14516.8223)\"\n                  gradientUnits=\"userSpaceOnUse\"\n                  x1=\"-491.1827\"\n                  x2=\"-491.4043\"\n                  y1=\"1326.7101\"\n                  y2=\"1326.2715\"\n                >\n                  <stop offset=\"0\" style=\"stop-color: #fb6583\" />\n                  <stop offset=\"1\" style=\"stop-color: #ffd1a3\" />\n                </linearGradient>\n                <polygon\n                  fill=\"url(#SVGID_60_)\"\n                  mask=\"url(#mask-113_1_)\"\n                  points=\"6.3,10.9 0,14.6 0,7.3 6.3,3.6 \t\t\t\t\t\t\"\n                />\n              </g>\n              <g id=\"Shape备份-4\" transform=\"translate(9.630049, 5.468450)\">\n                <g>\n                  <linearGradient\n                    id=\"SVGID_61_\"\n                    gradientTransform=\"matrix(6.3405 0 0 -10.9369 3383.2495 14453.3994)\"\n                    gradientUnits=\"userSpaceOnUse\"\n                    x1=\"-532.7115\"\n                    x2=\"-533.1328\"\n                    y1=\"1319.3007\"\n                    y2=\"1321.1917\"\n                  >\n                    <stop offset=\"5.400000e-03\" style=\"stop-color: #130cb5\" />\n                    <stop offset=\"0.2823\" style=\"stop-color: #285ecb\" />\n                    <stop offset=\"0.538\" style=\"stop-color: #3aa3dd\" />\n                    <stop offset=\"0.75\" style=\"stop-color: #47d5eb\" />\n                    <stop offset=\"0.9102\" style=\"stop-color: #4ff4f3\" />\n                    <stop offset=\"1\" style=\"stop-color: #52fff6\" />\n                  </linearGradient>\n                  <polygon\n                    fill=\"url(#SVGID_61_)\"\n                    points=\"0,7.3 6.3,10.9 6.3,3.7 0,0 \t\t\t\t\t\t\t\"\n                  />\n                </g>\n                <defs>\n                  <filter\n                    id=\"Adobe_OpacityMaskFilter_3_\"\n                    filterUnits=\"userSpaceOnUse\"\n                    height=\"10.9\"\n                    width=\"6.3\"\n                    x=\"0\"\n                    y=\"3.6\"\n                  >\n                    <feColorMatrix\n                      type=\"matrix\"\n                      values=\"1 0 0 0 0  0 1 0 0 0  0 0 1 0 0  0 0 0 1 0\"\n                    />\n                  </filter>\n                </defs>\n                <mask\n                  id=\"mask-117_1_\"\n                  height=\"10.9\"\n                  maskUnits=\"userSpaceOnUse\"\n                  width=\"6.3\"\n                  x=\"0\"\n                  y=\"3.6\"\n                >\n                  <g filter=\"url(#Adobe_OpacityMaskFilter_3_)\">\n                    <polygon\n                      id=\"path-116_1_\"\n                      clip-rule=\"evenodd\"\n                      fill=\"#FFFFFF\"\n                      fill-rule=\"evenodd\"\n                      points=\"0,7.3 6.3,10.9 6.3,3.7 0,0\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"\n                    />\n                  </g>\n                </mask>\n\n                <linearGradient\n                  id=\"SVGID_62_\"\n                  gradientTransform=\"matrix(-6.3405 0 0 -10.9369 -3172.9092 14457.0146)\"\n                  gradientUnits=\"userSpaceOnUse\"\n                  x1=\"-500.7126\"\n                  x2=\"-501.1153\"\n                  y1=\"1321.2085\"\n                  y2=\"1320.8575\"\n                >\n                  <stop offset=\"0\" style=\"stop-color: #00c4ba\" />\n                  <stop offset=\"1\" style=\"stop-color: #52fff6\" />\n                </linearGradient>\n                <polygon\n                  fill=\"url(#SVGID_62_)\"\n                  mask=\"url(#mask-117_1_)\"\n                  points=\"6.3,10.9 0,14.6 0,7.3 6.3,3.6 \t\t\t\t\t\t\"\n                />\n              </g>\n              <g id=\"Shape备份-5\" transform=\"translate(19.201017, 11.077510)\">\n                <g>\n                  <linearGradient\n                    id=\"SVGID_63_\"\n                    gradientTransform=\"matrix(6.3405 0 0 -10.9369 3443.9341 14392.0537)\"\n                    gradientUnits=\"userSpaceOnUse\"\n                    x1=\"-542.2824\"\n                    x2=\"-542.7037\"\n                    y1=\"1313.6917\"\n                    y2=\"1315.5826\"\n                  >\n                    <stop offset=\"5.400000e-03\" style=\"stop-color: #130cb5\" />\n                    <stop offset=\"0.2823\" style=\"stop-color: #285ecb\" />\n                    <stop offset=\"0.538\" style=\"stop-color: #3aa3dd\" />\n                    <stop offset=\"0.75\" style=\"stop-color: #47d5eb\" />\n                    <stop offset=\"0.9102\" style=\"stop-color: #4ff4f3\" />\n                    <stop offset=\"1\" style=\"stop-color: #52fff6\" />\n                  </linearGradient>\n                  <polygon\n                    fill=\"url(#SVGID_63_)\"\n                    points=\"0,7.3 6.3,10.9 6.3,3.7 0,0 \t\t\t\t\t\t\t\"\n                  />\n                </g>\n                <defs>\n                  <filter\n                    id=\"Adobe_OpacityMaskFilter_4_\"\n                    filterUnits=\"userSpaceOnUse\"\n                    height=\"10.9\"\n                    width=\"6.3\"\n                    x=\"0\"\n                    y=\"3.6\"\n                  >\n                    <feColorMatrix\n                      type=\"matrix\"\n                      values=\"1 0 0 0 0  0 1 0 0 0  0 0 1 0 0  0 0 0 1 0\"\n                    />\n                  </filter>\n                </defs>\n                <mask\n                  id=\"mask-120_1_\"\n                  height=\"10.9\"\n                  maskUnits=\"userSpaceOnUse\"\n                  width=\"6.3\"\n                  x=\"0\"\n                  y=\"3.6\"\n                >\n                  <g filter=\"url(#Adobe_OpacityMaskFilter_4_)\">\n                    <polygon\n                      id=\"path-119_1_\"\n                      clip-rule=\"evenodd\"\n                      fill=\"#FFFFFF\"\n                      fill-rule=\"evenodd\"\n                      points=\"0,7.3 6.3,10.9 6.3,3.7 0,0\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"\n                    />\n                  </g>\n                </mask>\n\n                <linearGradient\n                  id=\"SVGID_64_\"\n                  gradientTransform=\"matrix(-6.3405 0 0 -10.9369 -3233.5935 14395.6689)\"\n                  gradientUnits=\"userSpaceOnUse\"\n                  x1=\"-510.2835\"\n                  x2=\"-510.6862\"\n                  y1=\"1315.5994\"\n                  y2=\"1315.2484\"\n                >\n                  <stop offset=\"0\" style=\"stop-color: #00c4ba\" />\n                  <stop offset=\"1\" style=\"stop-color: #52fff6\" />\n                </linearGradient>\n                <polygon\n                  fill=\"url(#SVGID_64_)\"\n                  mask=\"url(#mask-120_1_)\"\n                  points=\"6.3,10.9 0,14.6 0,7.3 6.3,3.6 \t\t\t\t\t\t\"\n                />\n              </g>\n            </g>\n            <g id=\"编组-8备份\" transform=\"translate(0.000000, 11.077510)\">\n              <g transform=\"translate(0.000000, -0.000000)\">\n                <g>\n                  <linearGradient\n                    id=\"SVGID_65_\"\n                    gradientTransform=\"matrix(6.3405 0 0 -10.9369 3322.1904 14392.0537)\"\n                    gradientUnits=\"userSpaceOnUse\"\n                    x1=\"-523.204\"\n                    x2=\"-524.0609\"\n                    y1=\"1315.8689\"\n                    y2=\"1314.3909\"\n                  >\n                    <stop offset=\"5.400000e-03\" style=\"stop-color: #fb6583\" />\n                    <stop offset=\"0.1302\" style=\"stop-color: #fc7688\" />\n                    <stop offset=\"0.5277\" style=\"stop-color: #fda797\" />\n                    <stop offset=\"0.8302\" style=\"stop-color: #ffc6a0\" />\n                    <stop offset=\"1\" style=\"stop-color: #ffd1a3\" />\n                  </linearGradient>\n                  <polygon\n                    fill=\"url(#SVGID_65_)\"\n                    points=\"0,7.3 6.3,10.9 6.3,3.7 0,0 \t\t\t\t\t\t\t\"\n                  />\n                </g>\n                <defs>\n                  <filter\n                    id=\"Adobe_OpacityMaskFilter_5_\"\n                    filterUnits=\"userSpaceOnUse\"\n                    height=\"10.9\"\n                    width=\"6.3\"\n                    x=\"0\"\n                    y=\"3.6\"\n                  >\n                    <feColorMatrix\n                      type=\"matrix\"\n                      values=\"1 0 0 0 0  0 1 0 0 0  0 0 1 0 0  0 0 0 1 0\"\n                    />\n                  </filter>\n                </defs>\n                <mask\n                  id=\"mask-122_1_\"\n                  height=\"10.9\"\n                  maskUnits=\"userSpaceOnUse\"\n                  width=\"6.3\"\n                  x=\"0\"\n                  y=\"3.6\"\n                >\n                  <g filter=\"url(#Adobe_OpacityMaskFilter_5_)\">\n                    <polygon\n                      id=\"path-121_1_\"\n                      clip-rule=\"evenodd\"\n                      fill=\"#FFFFFF\"\n                      fill-rule=\"evenodd\"\n                      points=\"0,7.3 6.3,10.9 6.3,3.7 0,0\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"\n                    />\n                  </g>\n                </mask>\n\n                <linearGradient\n                  id=\"SVGID_66_\"\n                  gradientTransform=\"matrix(-6.3405 0 0 -10.9369 -3111.8501 14395.6689)\"\n                  gradientUnits=\"userSpaceOnUse\"\n                  x1=\"-491.1827\"\n                  x2=\"-491.4043\"\n                  y1=\"1315.6326\"\n                  y2=\"1315.194\"\n                >\n                  <stop offset=\"0\" style=\"stop-color: #fb6583\" />\n                  <stop offset=\"1\" style=\"stop-color: #ffd1a3\" />\n                </linearGradient>\n                <polygon\n                  fill=\"url(#SVGID_66_)\"\n                  mask=\"url(#mask-122_1_)\"\n                  points=\"6.3,10.9 0,14.6 0,7.3 6.3,3.6 \t\t\t\t\t\t\"\n                />\n              </g>\n              <g transform=\"translate(9.630049, 5.468450)\">\n                <g>\n                  <linearGradient\n                    id=\"SVGID_67_\"\n                    gradientTransform=\"matrix(6.3405 0 0 -10.9369 3383.2495 14332.2451)\"\n                    gradientUnits=\"userSpaceOnUse\"\n                    x1=\"-532.7115\"\n                    x2=\"-533.1328\"\n                    y1=\"1308.2231\"\n                    y2=\"1310.1143\"\n                  >\n                    <stop offset=\"5.400000e-03\" style=\"stop-color: #130cb5\" />\n                    <stop offset=\"0.2823\" style=\"stop-color: #285ecb\" />\n                    <stop offset=\"0.538\" style=\"stop-color: #3aa3dd\" />\n                    <stop offset=\"0.75\" style=\"stop-color: #47d5eb\" />\n                    <stop offset=\"0.9102\" style=\"stop-color: #4ff4f3\" />\n                    <stop offset=\"1\" style=\"stop-color: #52fff6\" />\n                  </linearGradient>\n                  <polygon\n                    fill=\"url(#SVGID_67_)\"\n                    points=\"0,7.3 6.3,10.9 6.3,3.7 0,0 \t\t\t\t\t\t\t\"\n                  />\n                </g>\n                <defs>\n                  <filter\n                    id=\"Adobe_OpacityMaskFilter_6_\"\n                    filterUnits=\"userSpaceOnUse\"\n                    height=\"10.9\"\n                    width=\"6.3\"\n                    x=\"0\"\n                    y=\"3.6\"\n                  >\n                    <feColorMatrix\n                      type=\"matrix\"\n                      values=\"1 0 0 0 0  0 1 0 0 0  0 0 1 0 0  0 0 0 1 0\"\n                    />\n                  </filter>\n                </defs>\n                <mask\n                  id=\"mask-124_1_\"\n                  height=\"10.9\"\n                  maskUnits=\"userSpaceOnUse\"\n                  width=\"6.3\"\n                  x=\"0\"\n                  y=\"3.6\"\n                >\n                  <g filter=\"url(#Adobe_OpacityMaskFilter_6_)\">\n                    <polygon\n                      id=\"path-123_1_\"\n                      clip-rule=\"evenodd\"\n                      fill=\"#FFFFFF\"\n                      fill-rule=\"evenodd\"\n                      points=\"0,7.3 6.3,10.9 6.3,3.7 0,0\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"\n                    />\n                  </g>\n                </mask>\n\n                <linearGradient\n                  id=\"SVGID_68_\"\n                  gradientTransform=\"matrix(-6.3405 0 0 -10.9369 -3172.9092 14335.8613)\"\n                  gradientUnits=\"userSpaceOnUse\"\n                  x1=\"-500.7126\"\n                  x2=\"-501.1153\"\n                  y1=\"1310.131\"\n                  y2=\"1309.78\"\n                >\n                  <stop offset=\"0\" style=\"stop-color: #00c4ba\" />\n                  <stop offset=\"1\" style=\"stop-color: #52fff6\" />\n                </linearGradient>\n                <polygon\n                  fill=\"url(#SVGID_68_)\"\n                  mask=\"url(#mask-124_1_)\"\n                  points=\"6.3,10.9 0,14.6 0,7.3 6.3,3.6 \t\t\t\t\t\t\"\n                />\n              </g>\n              <g transform=\"translate(19.201017, 11.077510)\">\n                <g>\n                  <linearGradient\n                    id=\"SVGID_69_\"\n                    gradientTransform=\"matrix(6.3405 0 0 -10.9369 3443.9341 14270.8994)\"\n                    gradientUnits=\"userSpaceOnUse\"\n                    x1=\"-542.2824\"\n                    x2=\"-542.7037\"\n                    y1=\"1302.6141\"\n                    y2=\"1304.5051\"\n                  >\n                    <stop offset=\"5.400000e-03\" style=\"stop-color: #130cb5\" />\n                    <stop offset=\"0.2823\" style=\"stop-color: #285ecb\" />\n                    <stop offset=\"0.538\" style=\"stop-color: #3aa3dd\" />\n                    <stop offset=\"0.75\" style=\"stop-color: #47d5eb\" />\n                    <stop offset=\"0.9102\" style=\"stop-color: #4ff4f3\" />\n                    <stop offset=\"1\" style=\"stop-color: #52fff6\" />\n                  </linearGradient>\n                  <polygon\n                    fill=\"url(#SVGID_69_)\"\n                    points=\"0,7.3 6.3,10.9 6.3,3.7 0,0 \t\t\t\t\t\t\t\"\n                  />\n                </g>\n                <defs>\n                  <filter\n                    id=\"Adobe_OpacityMaskFilter_7_\"\n                    filterUnits=\"userSpaceOnUse\"\n                    height=\"10.9\"\n                    width=\"6.3\"\n                    x=\"0\"\n                    y=\"3.6\"\n                  >\n                    <feColorMatrix\n                      type=\"matrix\"\n                      values=\"1 0 0 0 0  0 1 0 0 0  0 0 1 0 0  0 0 0 1 0\"\n                    />\n                  </filter>\n                </defs>\n                <mask\n                  id=\"mask-126_1_\"\n                  height=\"10.9\"\n                  maskUnits=\"userSpaceOnUse\"\n                  width=\"6.3\"\n                  x=\"0\"\n                  y=\"3.6\"\n                >\n                  <g filter=\"url(#Adobe_OpacityMaskFilter_7_)\">\n                    <polygon\n                      id=\"path-125_1_\"\n                      clip-rule=\"evenodd\"\n                      fill=\"#FFFFFF\"\n                      fill-rule=\"evenodd\"\n                      points=\"0,7.3 6.3,10.9 6.3,3.7 0,0\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"\n                    />\n                  </g>\n                </mask>\n\n                <linearGradient\n                  id=\"SVGID_70_\"\n                  gradientTransform=\"matrix(-6.3405 0 0 -10.9369 -3233.5935 14274.5156)\"\n                  gradientUnits=\"userSpaceOnUse\"\n                  x1=\"-510.2835\"\n                  x2=\"-510.6862\"\n                  y1=\"1304.5219\"\n                  y2=\"1304.1709\"\n                >\n                  <stop offset=\"0\" style=\"stop-color: #00c4ba\" />\n                  <stop offset=\"1\" style=\"stop-color: #52fff6\" />\n                </linearGradient>\n                <polygon\n                  fill=\"url(#SVGID_70_)\"\n                  mask=\"url(#mask-126_1_)\"\n                  points=\"6.3,10.9 0,14.6 0,7.3 6.3,3.6 \t\t\t\t\t\t\"\n                />\n              </g>\n            </g>\n            <g id=\"编组-8备份-2\" transform=\"translate(0.000000, 22.155020)\">\n              <g transform=\"translate(0.000000, -0.000000)\">\n                <g>\n                  <linearGradient\n                    id=\"SVGID_71_\"\n                    gradientTransform=\"matrix(6.3405 0 0 -10.9369 3322.1904 14270.8994)\"\n                    gradientUnits=\"userSpaceOnUse\"\n                    x1=\"-523.204\"\n                    x2=\"-524.0609\"\n                    y1=\"1304.7914\"\n                    y2=\"1303.3134\"\n                  >\n                    <stop offset=\"5.400000e-03\" style=\"stop-color: #fb6583\" />\n                    <stop offset=\"0.1302\" style=\"stop-color: #fc7688\" />\n                    <stop offset=\"0.5277\" style=\"stop-color: #fda797\" />\n                    <stop offset=\"0.8302\" style=\"stop-color: #ffc6a0\" />\n                    <stop offset=\"1\" style=\"stop-color: #ffd1a3\" />\n                  </linearGradient>\n                  <polygon\n                    fill=\"url(#SVGID_71_)\"\n                    points=\"0,7.3 6.3,10.9 6.3,3.7 0,0 \t\t\t\t\t\t\t\"\n                  />\n                </g>\n                <defs>\n                  <filter\n                    id=\"Adobe_OpacityMaskFilter_8_\"\n                    filterUnits=\"userSpaceOnUse\"\n                    height=\"10.9\"\n                    width=\"6.3\"\n                    x=\"0\"\n                    y=\"3.6\"\n                  >\n                    <feColorMatrix\n                      type=\"matrix\"\n                      values=\"1 0 0 0 0  0 1 0 0 0  0 0 1 0 0  0 0 0 1 0\"\n                    />\n                  </filter>\n                </defs>\n                <mask\n                  id=\"mask-128_1_\"\n                  height=\"10.9\"\n                  maskUnits=\"userSpaceOnUse\"\n                  width=\"6.3\"\n                  x=\"0\"\n                  y=\"3.6\"\n                >\n                  <g filter=\"url(#Adobe_OpacityMaskFilter_8_)\">\n                    <polygon\n                      id=\"path-127_1_\"\n                      clip-rule=\"evenodd\"\n                      fill=\"#FFFFFF\"\n                      fill-rule=\"evenodd\"\n                      points=\"0,7.3 6.3,10.9 6.3,3.7 0,0\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"\n                    />\n                  </g>\n                </mask>\n\n                <linearGradient\n                  id=\"SVGID_72_\"\n                  gradientTransform=\"matrix(-6.3405 0 0 -10.9369 -3111.8501 14274.5156)\"\n                  gradientUnits=\"userSpaceOnUse\"\n                  x1=\"-491.1827\"\n                  x2=\"-491.4043\"\n                  y1=\"1304.5552\"\n                  y2=\"1304.1165\"\n                >\n                  <stop offset=\"0\" style=\"stop-color: #fb6583\" />\n                  <stop offset=\"1\" style=\"stop-color: #ffd1a3\" />\n                </linearGradient>\n                <polygon\n                  fill=\"url(#SVGID_72_)\"\n                  mask=\"url(#mask-128_1_)\"\n                  points=\"6.3,10.9 0,14.6 0,7.3 6.3,3.6 \t\t\t\t\t\t\"\n                />\n              </g>\n              <g transform=\"translate(9.630049, 5.468450)\">\n                <g>\n                  <linearGradient\n                    id=\"SVGID_73_\"\n                    gradientTransform=\"matrix(6.3405 0 0 -10.9369 3383.2495 14211.0918)\"\n                    gradientUnits=\"userSpaceOnUse\"\n                    x1=\"-532.7115\"\n                    x2=\"-533.1328\"\n                    y1=\"1297.1456\"\n                    y2=\"1299.0367\"\n                  >\n                    <stop offset=\"5.400000e-03\" style=\"stop-color: #130cb5\" />\n                    <stop offset=\"0.2823\" style=\"stop-color: #285ecb\" />\n                    <stop offset=\"0.538\" style=\"stop-color: #3aa3dd\" />\n                    <stop offset=\"0.75\" style=\"stop-color: #47d5eb\" />\n                    <stop offset=\"0.9102\" style=\"stop-color: #4ff4f3\" />\n                    <stop offset=\"1\" style=\"stop-color: #52fff6\" />\n                  </linearGradient>\n                  <polygon\n                    fill=\"url(#SVGID_73_)\"\n                    points=\"0,7.3 6.3,10.9 6.3,3.7 0,0 \t\t\t\t\t\t\t\"\n                  />\n                </g>\n                <defs>\n                  <filter\n                    id=\"Adobe_OpacityMaskFilter_9_\"\n                    filterUnits=\"userSpaceOnUse\"\n                    height=\"10.9\"\n                    width=\"6.3\"\n                    x=\"0\"\n                    y=\"3.6\"\n                  >\n                    <feColorMatrix\n                      type=\"matrix\"\n                      values=\"1 0 0 0 0  0 1 0 0 0  0 0 1 0 0  0 0 0 1 0\"\n                    />\n                  </filter>\n                </defs>\n                <mask\n                  id=\"mask-130_1_\"\n                  height=\"10.9\"\n                  maskUnits=\"userSpaceOnUse\"\n                  width=\"6.3\"\n                  x=\"0\"\n                  y=\"3.6\"\n                >\n                  <g filter=\"url(#Adobe_OpacityMaskFilter_9_)\">\n                    <polygon\n                      id=\"path-129_1_\"\n                      clip-rule=\"evenodd\"\n                      fill=\"#FFFFFF\"\n                      fill-rule=\"evenodd\"\n                      points=\"0,7.3 6.3,10.9 6.3,3.7 0,0\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"\n                    />\n                  </g>\n                </mask>\n\n                <linearGradient\n                  id=\"SVGID_74_\"\n                  gradientTransform=\"matrix(-6.3405 0 0 -10.9369 -3172.9092 14214.708)\"\n                  gradientUnits=\"userSpaceOnUse\"\n                  x1=\"-500.7126\"\n                  x2=\"-501.1153\"\n                  y1=\"1299.0535\"\n                  y2=\"1298.7025\"\n                >\n                  <stop offset=\"0\" style=\"stop-color: #00c4ba\" />\n                  <stop offset=\"1\" style=\"stop-color: #52fff6\" />\n                </linearGradient>\n                <polygon\n                  fill=\"url(#SVGID_74_)\"\n                  mask=\"url(#mask-130_1_)\"\n                  points=\"6.3,10.9 0,14.6 0,7.3 6.3,3.6 \t\t\t\t\t\t\"\n                />\n              </g>\n              <g transform=\"translate(19.201017, 11.077510)\">\n                <g>\n                  <linearGradient\n                    id=\"SVGID_75_\"\n                    gradientTransform=\"matrix(6.3405 0 0 -10.9369 3443.9341 14149.7461)\"\n                    gradientUnits=\"userSpaceOnUse\"\n                    x1=\"-542.2824\"\n                    x2=\"-542.7037\"\n                    y1=\"1291.5366\"\n                    y2=\"1293.4276\"\n                  >\n                    <stop offset=\"5.400000e-03\" style=\"stop-color: #130cb5\" />\n                    <stop offset=\"0.2823\" style=\"stop-color: #285ecb\" />\n                    <stop offset=\"0.538\" style=\"stop-color: #3aa3dd\" />\n                    <stop offset=\"0.75\" style=\"stop-color: #47d5eb\" />\n                    <stop offset=\"0.9102\" style=\"stop-color: #4ff4f3\" />\n                    <stop offset=\"1\" style=\"stop-color: #52fff6\" />\n                  </linearGradient>\n                  <polygon\n                    fill=\"url(#SVGID_75_)\"\n                    points=\"0,7.3 6.3,10.9 6.3,3.7 0,0 \t\t\t\t\t\t\t\"\n                  />\n                </g>\n                <defs>\n                  <filter\n                    id=\"Adobe_OpacityMaskFilter_10_\"\n                    filterUnits=\"userSpaceOnUse\"\n                    height=\"10.9\"\n                    width=\"6.3\"\n                    x=\"0\"\n                    y=\"3.6\"\n                  >\n                    <feColorMatrix\n                      type=\"matrix\"\n                      values=\"1 0 0 0 0  0 1 0 0 0  0 0 1 0 0  0 0 0 1 0\"\n                    />\n                  </filter>\n                </defs>\n                <mask\n                  id=\"mask-132_1_\"\n                  height=\"10.9\"\n                  maskUnits=\"userSpaceOnUse\"\n                  width=\"6.3\"\n                  x=\"0\"\n                  y=\"3.6\"\n                >\n                  <g filter=\"url(#Adobe_OpacityMaskFilter_10_)\">\n                    <polygon\n                      id=\"path-131_1_\"\n                      clip-rule=\"evenodd\"\n                      fill=\"#FFFFFF\"\n                      fill-rule=\"evenodd\"\n                      points=\"0,7.3 6.3,10.9 6.3,3.7 0,0\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"\n                    />\n                  </g>\n                </mask>\n\n                <linearGradient\n                  id=\"SVGID_76_\"\n                  gradientTransform=\"matrix(-6.3405 0 0 -10.9369 -3233.5935 14153.3623)\"\n                  gradientUnits=\"userSpaceOnUse\"\n                  x1=\"-510.2835\"\n                  x2=\"-510.6862\"\n                  y1=\"1293.4443\"\n                  y2=\"1293.0934\"\n                >\n                  <stop offset=\"0\" style=\"stop-color: #00c4ba\" />\n                  <stop offset=\"1\" style=\"stop-color: #52fff6\" />\n                </linearGradient>\n                <polygon\n                  fill=\"url(#SVGID_76_)\"\n                  mask=\"url(#mask-132_1_)\"\n                  points=\"6.3,10.9 0,14.6 0,7.3 6.3,3.6 \t\t\t\t\t\t\"\n                />\n              </g>\n            </g>\n          </g>\n\n          <linearGradient\n            id=\"SVGID_77_\"\n            gradientTransform=\"matrix(35.088 0 0 -60.7744 17802.2441 87007.4453)\"\n            gradientUnits=\"userSpaceOnUse\"\n            x1=\"-505.8547\"\n            x2=\"-505.8547\"\n            y1=\"1431.6647\"\n            y2=\"1430.4622\"\n          >\n            <stop offset=\"0\" style=\"stop-color: #7bedff\" />\n            <stop offset=\"1\" style=\"stop-color: #060071\" />\n          </linearGradient>\n          <polygon\n            clip-rule=\"evenodd\"\n            fill=\"url(#SVGID_77_)\"\n            fill-rule=\"evenodd\"\n            points=\"35.3,81.1 35.3,40.6 70.4,20.4 70.4,60.9\n\t\t\t\t\"\n          />\n\n          <g\n            id=\"编组-10备份-4\"\n            transform=\"translate(53.703560, 51.063025) scale(-1, 1) translate(-53.703560, -51.063025) translate(40.203560, 28.063025)\"\n          >\n            <g>\n              <g transform=\"translate(0.000000, -0.000000)\">\n                <g>\n                  <linearGradient\n                    id=\"SVGID_78_\"\n                    gradientTransform=\"matrix(-6.3405 0 0 -10.9369 3516.198 14513.207)\"\n                    gradientUnits=\"userSpaceOnUse\"\n                    x1=\"553.8022\"\n                    x2=\"552.9454\"\n                    y1=\"1326.9464\"\n                    y2=\"1325.4684\"\n                  >\n                    <stop offset=\"5.400000e-03\" style=\"stop-color: #fb6583\" />\n                    <stop offset=\"0.1302\" style=\"stop-color: #fc7688\" />\n                    <stop offset=\"0.5277\" style=\"stop-color: #fda797\" />\n                    <stop offset=\"0.8302\" style=\"stop-color: #ffc6a0\" />\n                    <stop offset=\"1\" style=\"stop-color: #ffd1a3\" />\n                  </linearGradient>\n                  <polygon\n                    fill=\"url(#SVGID_78_)\"\n                    points=\"0,7.3 6.3,10.9 6.3,3.7 0,0 \t\t\t\t\t\t\t\"\n                  />\n                </g>\n                <defs>\n                  <filter\n                    id=\"Adobe_OpacityMaskFilter_11_\"\n                    filterUnits=\"userSpaceOnUse\"\n                  >\n                    <feColorMatrix\n                      type=\"matrix\"\n                      values=\"1 0 0 0 0  0 1 0 0 0  0 0 1 0 0  0 0 0 1 0\"\n                    />\n                  </filter>\n                </defs>\n                <mask id=\"mask-135_1_\" maskUnits=\"userSpaceOnUse\">\n                  <g filter=\"url(#Adobe_OpacityMaskFilter_11_)\">\n                    <polygon\n                      id=\"path-134_1_\"\n                      clip-rule=\"evenodd\"\n                      fill=\"#FFFFFF\"\n                      fill-rule=\"evenodd\"\n                      points=\"0,7.3 6.3,10.9 6.3,3.7 0,0\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"\n                    />\n                  </g>\n                </mask>\n\n                <linearGradient\n                  id=\"SVGID_79_\"\n                  gradientTransform=\"matrix(6.3405 0 0 -10.9369 -3713.8577 14516.8223)\"\n                  gradientUnits=\"userSpaceOnUse\"\n                  x1=\"586.1296\"\n                  x2=\"585.908\"\n                  y1=\"1326.7101\"\n                  y2=\"1326.2715\"\n                >\n                  <stop offset=\"0\" style=\"stop-color: #fb6583\" />\n                  <stop offset=\"1\" style=\"stop-color: #ffd1a3\" />\n                </linearGradient>\n                <polygon\n                  fill=\"url(#SVGID_79_)\"\n                  mask=\"url(#mask-135_1_)\"\n                  points=\"6.3,10.9 0,14.6 0,7.3 6.3,3.6 \t\t\t\t\t\t\"\n                />\n              </g>\n              <g transform=\"translate(9.630049, 5.468450)\">\n                <g>\n                  <linearGradient\n                    id=\"SVGID_80_\"\n                    gradientTransform=\"matrix(-6.3405 0 0 -10.9369 3455.1392 14453.3994)\"\n                    gradientUnits=\"userSpaceOnUse\"\n                    x1=\"544.0497\"\n                    x2=\"543.6284\"\n                    y1=\"1319.3007\"\n                    y2=\"1321.1917\"\n                  >\n                    <stop offset=\"5.400000e-03\" style=\"stop-color: #130cb5\" />\n                    <stop offset=\"0.2823\" style=\"stop-color: #285ecb\" />\n                    <stop offset=\"0.538\" style=\"stop-color: #3aa3dd\" />\n                    <stop offset=\"0.75\" style=\"stop-color: #47d5eb\" />\n                    <stop offset=\"0.9102\" style=\"stop-color: #4ff4f3\" />\n                    <stop offset=\"1\" style=\"stop-color: #52fff6\" />\n                  </linearGradient>\n                  <polygon\n                    fill=\"url(#SVGID_80_)\"\n                    points=\"0,7.3 6.3,10.9 6.3,3.7 0,0 \t\t\t\t\t\t\t\"\n                  />\n                </g>\n                <defs>\n                  <filter\n                    id=\"Adobe_OpacityMaskFilter_12_\"\n                    filterUnits=\"userSpaceOnUse\"\n                  >\n                    <feColorMatrix\n                      type=\"matrix\"\n                      values=\"1 0 0 0 0  0 1 0 0 0  0 0 1 0 0  0 0 0 1 0\"\n                    />\n                  </filter>\n                </defs>\n                <mask id=\"mask-137_1_\" maskUnits=\"userSpaceOnUse\">\n                  <g filter=\"url(#Adobe_OpacityMaskFilter_12_)\">\n                    <polygon\n                      id=\"path-136_1_\"\n                      clip-rule=\"evenodd\"\n                      fill=\"#FFFFFF\"\n                      fill-rule=\"evenodd\"\n                      points=\"0,7.3 6.3,10.9 6.3,3.7 0,0\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"\n                    />\n                  </g>\n                </mask>\n\n                <linearGradient\n                  id=\"SVGID_81_\"\n                  gradientTransform=\"matrix(6.3405 0 0 -10.9369 -3652.7986 14457.0146)\"\n                  gradientUnits=\"userSpaceOnUse\"\n                  x1=\"576.3993\"\n                  x2=\"575.9966\"\n                  y1=\"1321.2085\"\n                  y2=\"1320.8575\"\n                >\n                  <stop offset=\"0\" style=\"stop-color: #00c4ba\" />\n                  <stop offset=\"1\" style=\"stop-color: #52fff6\" />\n                </linearGradient>\n                <polygon\n                  fill=\"url(#SVGID_81_)\"\n                  mask=\"url(#mask-137_1_)\"\n                  points=\"6.3,10.9 0,14.6 0,7.3 6.3,3.6 \t\t\t\t\t\t\"\n                />\n              </g>\n              <g transform=\"translate(19.201017, 11.077510)\">\n                <g>\n                  <linearGradient\n                    id=\"SVGID_82_\"\n                    gradientTransform=\"matrix(-6.3405 0 0 -10.9369 3394.4546 14392.0537)\"\n                    gradientUnits=\"userSpaceOnUse\"\n                    x1=\"534.4787\"\n                    x2=\"534.0574\"\n                    y1=\"1313.6917\"\n                    y2=\"1315.5826\"\n                  >\n                    <stop offset=\"5.400000e-03\" style=\"stop-color: #130cb5\" />\n                    <stop offset=\"0.2823\" style=\"stop-color: #285ecb\" />\n                    <stop offset=\"0.538\" style=\"stop-color: #3aa3dd\" />\n                    <stop offset=\"0.75\" style=\"stop-color: #47d5eb\" />\n                    <stop offset=\"0.9102\" style=\"stop-color: #4ff4f3\" />\n                    <stop offset=\"1\" style=\"stop-color: #52fff6\" />\n                  </linearGradient>\n                  <polygon\n                    fill=\"url(#SVGID_82_)\"\n                    points=\"0,7.3 6.3,10.9 6.3,3.7 0,0 \t\t\t\t\t\t\t\"\n                  />\n                </g>\n                <defs>\n                  <filter\n                    id=\"Adobe_OpacityMaskFilter_13_\"\n                    filterUnits=\"userSpaceOnUse\"\n                  >\n                    <feColorMatrix\n                      type=\"matrix\"\n                      values=\"1 0 0 0 0  0 1 0 0 0  0 0 1 0 0  0 0 0 1 0\"\n                    />\n                  </filter>\n                </defs>\n                <mask id=\"mask-139_1_\" maskUnits=\"userSpaceOnUse\">\n                  <g filter=\"url(#Adobe_OpacityMaskFilter_13_)\">\n                    <polygon\n                      id=\"path-138_1_\"\n                      clip-rule=\"evenodd\"\n                      fill=\"#FFFFFF\"\n                      fill-rule=\"evenodd\"\n                      points=\"0,7.3 6.3,10.9 6.3,3.7 0,0\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"\n                    />\n                  </g>\n                </mask>\n\n                <linearGradient\n                  id=\"SVGID_83_\"\n                  gradientTransform=\"matrix(6.3405 0 0 -10.9369 -3592.114 14395.6689)\"\n                  gradientUnits=\"userSpaceOnUse\"\n                  x1=\"566.8283\"\n                  x2=\"566.4257\"\n                  y1=\"1315.5994\"\n                  y2=\"1315.2484\"\n                >\n                  <stop offset=\"0\" style=\"stop-color: #00c4ba\" />\n                  <stop offset=\"1\" style=\"stop-color: #52fff6\" />\n                </linearGradient>\n                <polygon\n                  fill=\"url(#SVGID_83_)\"\n                  mask=\"url(#mask-139_1_)\"\n                  points=\"6.3,10.9 0,14.6 0,7.3 6.3,3.6 \t\t\t\t\t\t\"\n                />\n              </g>\n            </g>\n            <g transform=\"translate(0.000000, 11.077510)\">\n              <g transform=\"translate(0.000000, -0.000000)\">\n                <g>\n                  <linearGradient\n                    id=\"SVGID_84_\"\n                    gradientTransform=\"matrix(-6.3405 0 0 -10.9369 3516.198 14392.0537)\"\n                    gradientUnits=\"userSpaceOnUse\"\n                    x1=\"553.8022\"\n                    x2=\"552.9454\"\n                    y1=\"1315.8689\"\n                    y2=\"1314.3909\"\n                  >\n                    <stop offset=\"5.400000e-03\" style=\"stop-color: #fb6583\" />\n                    <stop offset=\"0.1302\" style=\"stop-color: #fc7688\" />\n                    <stop offset=\"0.5277\" style=\"stop-color: #fda797\" />\n                    <stop offset=\"0.8302\" style=\"stop-color: #ffc6a0\" />\n                    <stop offset=\"1\" style=\"stop-color: #ffd1a3\" />\n                  </linearGradient>\n                  <polygon\n                    fill=\"url(#SVGID_84_)\"\n                    points=\"0,7.3 6.3,10.9 6.3,3.7 0,0 \t\t\t\t\t\t\t\"\n                  />\n                </g>\n                <defs>\n                  <filter\n                    id=\"Adobe_OpacityMaskFilter_14_\"\n                    filterUnits=\"userSpaceOnUse\"\n                  >\n                    <feColorMatrix\n                      type=\"matrix\"\n                      values=\"1 0 0 0 0  0 1 0 0 0  0 0 1 0 0  0 0 0 1 0\"\n                    />\n                  </filter>\n                </defs>\n                <mask id=\"mask-141_1_\" maskUnits=\"userSpaceOnUse\">\n                  <g filter=\"url(#Adobe_OpacityMaskFilter_14_)\">\n                    <polygon\n                      id=\"path-140_1_\"\n                      clip-rule=\"evenodd\"\n                      fill=\"#FFFFFF\"\n                      fill-rule=\"evenodd\"\n                      points=\"0,7.3 6.3,10.9 6.3,3.7 0,0\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"\n                    />\n                  </g>\n                </mask>\n\n                <linearGradient\n                  id=\"SVGID_85_\"\n                  gradientTransform=\"matrix(6.3405 0 0 -10.9369 -3713.8577 14395.6689)\"\n                  gradientUnits=\"userSpaceOnUse\"\n                  x1=\"586.1296\"\n                  x2=\"585.908\"\n                  y1=\"1315.6326\"\n                  y2=\"1315.194\"\n                >\n                  <stop offset=\"0\" style=\"stop-color: #fb6583\" />\n                  <stop offset=\"1\" style=\"stop-color: #ffd1a3\" />\n                </linearGradient>\n                <polygon\n                  fill=\"url(#SVGID_85_)\"\n                  mask=\"url(#mask-141_1_)\"\n                  points=\"6.3,10.9 0,14.6 0,7.3 6.3,3.6 \t\t\t\t\t\t\"\n                />\n              </g>\n              <g transform=\"translate(9.630049, 5.468450)\">\n                <g>\n                  <linearGradient\n                    id=\"SVGID_86_\"\n                    gradientTransform=\"matrix(-6.3405 0 0 -10.9369 3455.1392 14332.2451)\"\n                    gradientUnits=\"userSpaceOnUse\"\n                    x1=\"544.0497\"\n                    x2=\"543.6284\"\n                    y1=\"1308.2231\"\n                    y2=\"1310.1143\"\n                  >\n                    <stop offset=\"5.400000e-03\" style=\"stop-color: #130cb5\" />\n                    <stop offset=\"0.2823\" style=\"stop-color: #285ecb\" />\n                    <stop offset=\"0.538\" style=\"stop-color: #3aa3dd\" />\n                    <stop offset=\"0.75\" style=\"stop-color: #47d5eb\" />\n                    <stop offset=\"0.9102\" style=\"stop-color: #4ff4f3\" />\n                    <stop offset=\"1\" style=\"stop-color: #52fff6\" />\n                  </linearGradient>\n                  <polygon\n                    fill=\"url(#SVGID_86_)\"\n                    points=\"0,7.3 6.3,10.9 6.3,3.7 0,0 \t\t\t\t\t\t\t\"\n                  />\n                </g>\n                <defs>\n                  <filter\n                    id=\"Adobe_OpacityMaskFilter_15_\"\n                    filterUnits=\"userSpaceOnUse\"\n                  >\n                    <feColorMatrix\n                      type=\"matrix\"\n                      values=\"1 0 0 0 0  0 1 0 0 0  0 0 1 0 0  0 0 0 1 0\"\n                    />\n                  </filter>\n                </defs>\n                <mask id=\"mask-143_1_\" maskUnits=\"userSpaceOnUse\">\n                  <g filter=\"url(#Adobe_OpacityMaskFilter_15_)\">\n                    <polygon\n                      id=\"path-142_1_\"\n                      clip-rule=\"evenodd\"\n                      fill=\"#FFFFFF\"\n                      fill-rule=\"evenodd\"\n                      points=\"0,7.3 6.3,10.9 6.3,3.7 0,0\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"\n                    />\n                  </g>\n                </mask>\n\n                <linearGradient\n                  id=\"SVGID_87_\"\n                  gradientTransform=\"matrix(6.3405 0 0 -10.9369 -3652.7986 14335.8613)\"\n                  gradientUnits=\"userSpaceOnUse\"\n                  x1=\"576.3993\"\n                  x2=\"575.9966\"\n                  y1=\"1310.131\"\n                  y2=\"1309.78\"\n                >\n                  <stop offset=\"0\" style=\"stop-color: #00c4ba\" />\n                  <stop offset=\"1\" style=\"stop-color: #52fff6\" />\n                </linearGradient>\n                <polygon\n                  fill=\"url(#SVGID_87_)\"\n                  mask=\"url(#mask-143_1_)\"\n                  points=\"6.3,10.9 0,14.6 0,7.3 6.3,3.6 \t\t\t\t\t\t\"\n                />\n              </g>\n              <g transform=\"translate(19.201017, 11.077510)\">\n                <g>\n                  <linearGradient\n                    id=\"SVGID_88_\"\n                    gradientTransform=\"matrix(-6.3405 0 0 -10.9369 3394.4546 14270.8994)\"\n                    gradientUnits=\"userSpaceOnUse\"\n                    x1=\"534.4787\"\n                    x2=\"534.0574\"\n                    y1=\"1302.6141\"\n                    y2=\"1304.5051\"\n                  >\n                    <stop offset=\"5.400000e-03\" style=\"stop-color: #130cb5\" />\n                    <stop offset=\"0.2823\" style=\"stop-color: #285ecb\" />\n                    <stop offset=\"0.538\" style=\"stop-color: #3aa3dd\" />\n                    <stop offset=\"0.75\" style=\"stop-color: #47d5eb\" />\n                    <stop offset=\"0.9102\" style=\"stop-color: #4ff4f3\" />\n                    <stop offset=\"1\" style=\"stop-color: #52fff6\" />\n                  </linearGradient>\n                  <polygon\n                    fill=\"url(#SVGID_88_)\"\n                    points=\"0,7.3 6.3,10.9 6.3,3.7 0,0 \t\t\t\t\t\t\t\"\n                  />\n                </g>\n                <defs>\n                  <filter\n                    id=\"Adobe_OpacityMaskFilter_16_\"\n                    filterUnits=\"userSpaceOnUse\"\n                  >\n                    <feColorMatrix\n                      type=\"matrix\"\n                      values=\"1 0 0 0 0  0 1 0 0 0  0 0 1 0 0  0 0 0 1 0\"\n                    />\n                  </filter>\n                </defs>\n                <mask id=\"mask-145_1_\" maskUnits=\"userSpaceOnUse\">\n                  <g filter=\"url(#Adobe_OpacityMaskFilter_16_)\">\n                    <polygon\n                      id=\"path-144_1_\"\n                      clip-rule=\"evenodd\"\n                      fill=\"#FFFFFF\"\n                      fill-rule=\"evenodd\"\n                      points=\"0,7.3 6.3,10.9 6.3,3.7 0,0\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"\n                    />\n                  </g>\n                </mask>\n\n                <linearGradient\n                  id=\"SVGID_89_\"\n                  gradientTransform=\"matrix(6.3405 0 0 -10.9369 -3592.114 14274.5156)\"\n                  gradientUnits=\"userSpaceOnUse\"\n                  x1=\"566.8283\"\n                  x2=\"566.4257\"\n                  y1=\"1304.5219\"\n                  y2=\"1304.1709\"\n                >\n                  <stop offset=\"0\" style=\"stop-color: #00c4ba\" />\n                  <stop offset=\"1\" style=\"stop-color: #52fff6\" />\n                </linearGradient>\n                <polygon\n                  fill=\"url(#SVGID_89_)\"\n                  mask=\"url(#mask-145_1_)\"\n                  points=\"6.3,10.9 0,14.6 0,7.3 6.3,3.6 \t\t\t\t\t\t\"\n                />\n              </g>\n            </g>\n            <g transform=\"translate(0.000000, 22.155020)\">\n              <g transform=\"translate(0.000000, -0.000000)\">\n                <g>\n                  <linearGradient\n                    id=\"SVGID_90_\"\n                    gradientTransform=\"matrix(-6.3405 0 0 -10.9369 3516.198 14270.8994)\"\n                    gradientUnits=\"userSpaceOnUse\"\n                    x1=\"553.8022\"\n                    x2=\"552.9454\"\n                    y1=\"1304.7914\"\n                    y2=\"1303.3134\"\n                  >\n                    <stop offset=\"5.400000e-03\" style=\"stop-color: #fb6583\" />\n                    <stop offset=\"0.1302\" style=\"stop-color: #fc7688\" />\n                    <stop offset=\"0.5277\" style=\"stop-color: #fda797\" />\n                    <stop offset=\"0.8302\" style=\"stop-color: #ffc6a0\" />\n                    <stop offset=\"1\" style=\"stop-color: #ffd1a3\" />\n                  </linearGradient>\n                  <polygon\n                    fill=\"url(#SVGID_90_)\"\n                    points=\"0,7.3 6.3,10.9 6.3,3.7 0,0 \t\t\t\t\t\t\t\"\n                  />\n                </g>\n                <defs>\n                  <filter\n                    id=\"Adobe_OpacityMaskFilter_17_\"\n                    filterUnits=\"userSpaceOnUse\"\n                  >\n                    <feColorMatrix\n                      type=\"matrix\"\n                      values=\"1 0 0 0 0  0 1 0 0 0  0 0 1 0 0  0 0 0 1 0\"\n                    />\n                  </filter>\n                </defs>\n                <mask id=\"mask-147_1_\" maskUnits=\"userSpaceOnUse\">\n                  <g filter=\"url(#Adobe_OpacityMaskFilter_17_)\">\n                    <polygon\n                      id=\"path-146_1_\"\n                      clip-rule=\"evenodd\"\n                      fill=\"#FFFFFF\"\n                      fill-rule=\"evenodd\"\n                      points=\"0,7.3 6.3,10.9 6.3,3.7 0,0\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"\n                    />\n                  </g>\n                </mask>\n\n                <linearGradient\n                  id=\"SVGID_91_\"\n                  gradientTransform=\"matrix(6.3405 0 0 -10.9369 -3713.8577 14274.5156)\"\n                  gradientUnits=\"userSpaceOnUse\"\n                  x1=\"586.1296\"\n                  x2=\"585.908\"\n                  y1=\"1304.5552\"\n                  y2=\"1304.1165\"\n                >\n                  <stop offset=\"0\" style=\"stop-color: #fb6583\" />\n                  <stop offset=\"1\" style=\"stop-color: #ffd1a3\" />\n                </linearGradient>\n                <polygon\n                  fill=\"url(#SVGID_91_)\"\n                  mask=\"url(#mask-147_1_)\"\n                  points=\"6.3,10.9 0,14.6 0,7.3 6.3,3.6 \t\t\t\t\t\t\"\n                />\n              </g>\n              <g transform=\"translate(9.630049, 5.468450)\">\n                <g>\n                  <linearGradient\n                    id=\"SVGID_92_\"\n                    gradientTransform=\"matrix(-6.3405 0 0 -10.9369 3455.1392 14211.0918)\"\n                    gradientUnits=\"userSpaceOnUse\"\n                    x1=\"544.0497\"\n                    x2=\"543.6284\"\n                    y1=\"1297.1456\"\n                    y2=\"1299.0367\"\n                  >\n                    <stop offset=\"5.400000e-03\" style=\"stop-color: #130cb5\" />\n                    <stop offset=\"0.2823\" style=\"stop-color: #285ecb\" />\n                    <stop offset=\"0.538\" style=\"stop-color: #3aa3dd\" />\n                    <stop offset=\"0.75\" style=\"stop-color: #47d5eb\" />\n                    <stop offset=\"0.9102\" style=\"stop-color: #4ff4f3\" />\n                    <stop offset=\"1\" style=\"stop-color: #52fff6\" />\n                  </linearGradient>\n                  <polygon\n                    fill=\"url(#SVGID_92_)\"\n                    points=\"0,7.3 6.3,10.9 6.3,3.7 0,0 \t\t\t\t\t\t\t\"\n                  />\n                </g>\n                <defs>\n                  <filter\n                    id=\"Adobe_OpacityMaskFilter_18_\"\n                    filterUnits=\"userSpaceOnUse\"\n                  >\n                    <feColorMatrix\n                      type=\"matrix\"\n                      values=\"1 0 0 0 0  0 1 0 0 0  0 0 1 0 0  0 0 0 1 0\"\n                    />\n                  </filter>\n                </defs>\n                <mask id=\"mask-149_1_\" maskUnits=\"userSpaceOnUse\">\n                  <g filter=\"url(#Adobe_OpacityMaskFilter_18_)\">\n                    <polygon\n                      id=\"path-148_1_\"\n                      clip-rule=\"evenodd\"\n                      fill=\"#FFFFFF\"\n                      fill-rule=\"evenodd\"\n                      points=\"0,7.3 6.3,10.9 6.3,3.7 0,0\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"\n                    />\n                  </g>\n                </mask>\n\n                <linearGradient\n                  id=\"SVGID_93_\"\n                  gradientTransform=\"matrix(6.3405 0 0 -10.9369 -3652.7986 14214.708)\"\n                  gradientUnits=\"userSpaceOnUse\"\n                  x1=\"576.3993\"\n                  x2=\"575.9966\"\n                  y1=\"1299.0535\"\n                  y2=\"1298.7025\"\n                >\n                  <stop offset=\"0\" style=\"stop-color: #00c4ba\" />\n                  <stop offset=\"1\" style=\"stop-color: #52fff6\" />\n                </linearGradient>\n                <polygon\n                  fill=\"url(#SVGID_93_)\"\n                  mask=\"url(#mask-149_1_)\"\n                  points=\"6.3,10.9 0,14.6 0,7.3 6.3,3.6 \t\t\t\t\t\t\"\n                />\n              </g>\n              <g transform=\"translate(19.201017, 11.077510)\">\n                <g>\n                  <linearGradient\n                    id=\"SVGID_94_\"\n                    gradientTransform=\"matrix(-6.3405 0 0 -10.9369 3394.4546 14149.7461)\"\n                    gradientUnits=\"userSpaceOnUse\"\n                    x1=\"534.4787\"\n                    x2=\"534.0574\"\n                    y1=\"1291.5366\"\n                    y2=\"1293.4276\"\n                  >\n                    <stop offset=\"5.400000e-03\" style=\"stop-color: #130cb5\" />\n                    <stop offset=\"0.2823\" style=\"stop-color: #285ecb\" />\n                    <stop offset=\"0.538\" style=\"stop-color: #3aa3dd\" />\n                    <stop offset=\"0.75\" style=\"stop-color: #47d5eb\" />\n                    <stop offset=\"0.9102\" style=\"stop-color: #4ff4f3\" />\n                    <stop offset=\"1\" style=\"stop-color: #52fff6\" />\n                  </linearGradient>\n                  <polygon\n                    fill=\"url(#SVGID_94_)\"\n                    points=\"0,7.3 6.3,10.9 6.3,3.7 0,0 \t\t\t\t\t\t\t\"\n                  />\n                </g>\n                <defs>\n                  <filter\n                    id=\"Adobe_OpacityMaskFilter_19_\"\n                    filterUnits=\"userSpaceOnUse\"\n                  >\n                    <feColorMatrix\n                      type=\"matrix\"\n                      values=\"1 0 0 0 0  0 1 0 0 0  0 0 1 0 0  0 0 0 1 0\"\n                    />\n                  </filter>\n                </defs>\n                <mask id=\"mask-151_1_\" maskUnits=\"userSpaceOnUse\">\n                  <g filter=\"url(#Adobe_OpacityMaskFilter_19_)\">\n                    <polygon\n                      id=\"path-150_1_\"\n                      clip-rule=\"evenodd\"\n                      fill=\"#FFFFFF\"\n                      fill-rule=\"evenodd\"\n                      points=\"0,7.3 6.3,10.9 6.3,3.7 0,0\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"\n                    />\n                  </g>\n                </mask>\n\n                <linearGradient\n                  id=\"SVGID_95_\"\n                  gradientTransform=\"matrix(6.3405 0 0 -10.9369 -3592.114 14153.3623)\"\n                  gradientUnits=\"userSpaceOnUse\"\n                  x1=\"566.8283\"\n                  x2=\"566.4257\"\n                  y1=\"1293.4443\"\n                  y2=\"1293.0934\"\n                >\n                  <stop offset=\"0\" style=\"stop-color: #00c4ba\" />\n                  <stop offset=\"1\" style=\"stop-color: #52fff6\" />\n                </linearGradient>\n                <polygon\n                  fill=\"url(#SVGID_95_)\"\n                  mask=\"url(#mask-151_1_)\"\n                  points=\"6.3,10.9 0,14.6 0,7.3 6.3,3.6 \t\t\t\t\t\t\"\n                />\n              </g>\n            </g>\n          </g>\n        </g>\n\n        <g\n          id=\"椭圆\"\n          transform=\"translate(83.218192, 86.783330) rotate(5.000000) translate(-83.218192, -86.783330) translate(7.218192, 10.783330)\"\n        >\n          <g id=\"编组-5\" transform=\"translate(8.161827, 36.521835)\">\n            <linearGradient\n              id=\"SVGID_96_\"\n              gradientTransform=\"matrix(124.0018 47.351 0.1479 -31.3606 42600.2344 62547.3867)\"\n              gradientUnits=\"userSpaceOnUse\"\n              x1=\"-344.7103\"\n              x2=\"-344.7103\"\n              y1=\"1473.182\"\n              y2=\"1472.182\"\n            >\n              <stop\n                offset=\"0\"\n                style=\"stop-color: #5cd7e8; stop-opacity: 0.1893\"\n              />\n              <stop offset=\"1\" style=\"stop-color: #fff\" />\n            </linearGradient>\n\n            <ellipse\n              cx=\"68\"\n              cy=\"38.7\"\n              fill=\"none\"\n              rx=\"14.6\"\n              ry=\"66.6\"\n              stroke=\"url(#SVGID_96_)\"\n              stroke-width=\"0.6\"\n              transform=\"matrix(-0.3746 0.9272 -0.9272 -0.3746 129.3914 -9.7912)\"\n            />\n\n            <ellipse\n              clip-rule=\"evenodd\"\n              cx=\"60.3\"\n              cy=\"51.5\"\n              fill=\"#FFFFFF\"\n              fill-rule=\"evenodd\"\n              rx=\"1.7\"\n              ry=\"1.7\"\n              transform=\"matrix(2.765398e-06 1 -1 2.765398e-06 111.8035 -8.8797)\"\n            />\n\n            <ellipse\n              id=\"椭圆形备份\"\n              clip-rule=\"evenodd\"\n              cx=\"86\"\n              cy=\"30.8\"\n              enable-background=\"new    \"\n              fill=\"#FFFFFF\"\n              fill-rule=\"evenodd\"\n              opacity=\"0.2662\"\n              rx=\"1.1\"\n              ry=\"1\"\n              transform=\"matrix(2.765396e-06 1 -1 2.765396e-06 116.7571 -55.2401)\"\n            />\n          </g>\n\n          <g\n            transform=\"translate(76.180160, 76.074544) rotate(-45.000000) translate(-76.180160, -76.074544) translate(8.680160, 37.074544)\"\n          >\n            <linearGradient\n              id=\"SVGID_97_\"\n              gradientTransform=\"matrix(87.4224 55.5937 87.656 11.2551 42330.7227 60598.5664)\"\n              gradientUnits=\"userSpaceOnUse\"\n              x1=\"-1242.9404\"\n              x2=\"-1242.9404\"\n              y1=\"757.5463\"\n              y2=\"756.5463\"\n            >\n              <stop\n                offset=\"0\"\n                style=\"stop-color: #5cd7e8; stop-opacity: 0.1893\"\n              />\n              <stop offset=\"1\" style=\"stop-color: #fff\" />\n            </linearGradient>\n\n            <ellipse\n              cx=\"67.9\"\n              cy=\"38.7\"\n              fill=\"none\"\n              rx=\"66.5\"\n              ry=\"14.6\"\n              stroke=\"url(#SVGID_97_)\"\n              stroke-width=\"0.6\"\n              transform=\"matrix(-0.9272 -0.3746 0.3746 -0.9272 116.2823 100.0875)\"\n            />\n\n            <ellipse\n              clip-rule=\"evenodd\"\n              cx=\"31.9\"\n              cy=\"37\"\n              fill=\"#FFFFFF\"\n              fill-rule=\"evenodd\"\n              rx=\"1.7\"\n              ry=\"1.7\"\n              transform=\"matrix(-1 -5.213811e-07 5.213811e-07 -1 63.8725 74.0324)\"\n            />\n\n            <ellipse\n              clip-rule=\"evenodd\"\n              cx=\"85.8\"\n              cy=\"30.8\"\n              enable-background=\"new    \"\n              fill=\"#FFFFFF\"\n              fill-rule=\"evenodd\"\n              opacity=\"0.2662\"\n              rx=\"1\"\n              ry=\"1.1\"\n              transform=\"matrix(-1 2.765399e-06 -2.765399e-06 -1 171.6696 61.5172)\"\n            />\n          </g>\n        </g>\n      </g>\n\n      <g\n        id=\"编组备份-11\"\n        transform=\"translate(694.000000, 158.500000) scale(-1, 1) translate(-694.000000, -158.500000) translate(622.000000, 65.000000)\"\n      >\n        <linearGradient\n          id=\"SVGID_98_\"\n          gradientTransform=\"matrix(-142.0107 0 0 -186.2829 94193.1797 325470.9375)\"\n          gradientUnits=\"userSpaceOnUse\"\n          x1=\"663.2509\"\n          x2=\"663.8784\"\n          y1=\"1745.6415\"\n          y2=\"1747.1095\"\n        >\n          <stop offset=\"0\" style=\"stop-color: #fff\" />\n          <stop offset=\"1\" style=\"stop-color: #00c6fb\" />\n        </linearGradient>\n        <path\n          d=\"M134.9,0l7.1,4l-2.2,3.6l-3,1.2l-1.4,92c0,3-1.8,6.5-4,8l-0.2,0.1L10,178.8l0,0l-1.2,7.5l-7-3.9\n\t\t\tc-0.1-0.1-0.3-0.1-0.4-0.2l0,0c-0.8-0.6-1.3-1.7-1.3-3.3l0,0l1.4-97.1c0-3.1,1.9-6.7,4.2-8.1l0,0L132.6,0.5\n\t\t\tC133.5,0,134.3-0.1,134.9,0L134.9,0z\"\n          fill=\"url(#SVGID_98_)\"\n        />\n\n        <linearGradient\n          id=\"SVGID_99_\"\n          gradientTransform=\"matrix(-136.8189 0 0 -182.5389 90752.4922 318912.7812)\"\n          gradientUnits=\"userSpaceOnUse\"\n          x1=\"662.8271\"\n          x2=\"662.9656\"\n          y1=\"1746.7982\"\n          y2=\"1746.3707\"\n        >\n          <stop offset=\"0\" style=\"stop-color: #276cfc\" />\n          <stop offset=\"1\" style=\"stop-color: #84ffd1\" />\n        </linearGradient>\n        <path\n          clip-rule=\"evenodd\"\n          d=\"M11,185.8l126.9-73.2c2.3-1.4,4.2-5,4.2-8.1l1.4-97.1\n\t\t\tc0-3.1-1.9-4.6-4.2-3.2L12.4,77.4c-2.3,1.4-4.2,5-4.2,8.1l-1.4,97.1C6.8,185.7,8.7,187.1,11,185.8z\"\n          fill=\"url(#SVGID_99_)\"\n          fill-rule=\"evenodd\"\n        />\n        <g opacity=\"0.3\" transform=\"translate(95.618736, 40.248000)\">\n          <path\n            d=\"M33.5,8.7L3.1,26.3c-1,0.6-2.2,0.2-2.8-0.7\n\t\t\t\tc-0.2-0.3-0.3-0.7-0.3-1c0-2.2,1.2-4.2,3-5.2L33.5,1.7c1-0.6,2.2-0.2,2.8,0.7c0.2,0.3,0.3,0.7,0.3,1C36.6,5.7,35.4,7.7,33.5,8.7z\n\t\t\t\t\"\n            enable-background=\"new    \"\n            fill=\"#FFFFFF\"\n            opacity=\"0.3007\"\n          />\n          <path\n            d=\"M34,27.4L10.3,41c-0.8,0.4-1.7,0.2-2.1-0.6\n\t\t\t\tC8,40.2,8,40,8,39.7c0-1.7,0.9-3.2,2.4-4.1L34,22c0.8-0.4,1.7-0.2,2.1,0.6c0.1,0.2,0.2,0.5,0.2,0.8C36.3,25,35.4,26.5,34,27.4z\"\n            enable-background=\"new    \"\n            fill=\"#FFFFFF\"\n            opacity=\"0.3007\"\n          />\n          <path\n            d=\"M34,41.7L10.3,55.4c-0.8,0.4-1.7,0.2-2.1-0.6\n\t\t\t\tC8,54.6,8,54.3,8,54c0-1.7,0.9-3.2,2.4-4.1L34,36.3c0.8-0.4,1.7-0.2,2.1,0.6c0.1,0.2,0.2,0.5,0.2,0.8\n\t\t\t\tC36.3,39.3,35.4,40.9,34,41.7z\"\n            enable-background=\"new    \"\n            fill=\"#FFFFFF\"\n            opacity=\"0.3007\"\n          />\n          <path\n            d=\"M34,56L10.3,69.7c-0.8,0.4-1.7,0.2-2.1-0.6\n\t\t\t\tC8,68.9,8,68.6,8,68.3c0-1.7,0.9-3.2,2.4-4.1L34,50.6c0.8-0.4,1.7-0.2,2.1,0.6c0.1,0.2,0.2,0.5,0.2,0.8\n\t\t\t\tC36.3,53.6,35.4,55.2,34,56z\"\n            enable-background=\"new    \"\n            fill=\"#FFFFFF\"\n            opacity=\"0.3007\"\n          />\n        </g>\n\n        <linearGradient\n          id=\"SVGID_100_\"\n          gradientTransform=\"matrix(-82.1586 0 0 -155.5418 54504.3555 271598.7188)\"\n          gradientUnits=\"userSpaceOnUse\"\n          x1=\"662.0853\"\n          x2=\"661.7936\"\n          y1=\"1746.2543\"\n          y2=\"1745.1642\"\n        >\n          <stop offset=\"0\" style=\"stop-color: #fff\" />\n          <stop offset=\"1\" style=\"stop-color: #fff; stop-opacity: 0\" />\n        </linearGradient>\n        <polygon\n          enable-background=\"new    \"\n          fill=\"url(#SVGID_100_)\"\n          opacity=\"0.4\"\n          points=\"93,31 53,161.5 94.8,137.4 135.2,6 \t\t\"\n        />\n\n        <linearGradient\n          id=\"SVGID_101_\"\n          gradientTransform=\"matrix(-12.3655 0 0 -48.4174 8136.0479 83952.2812)\"\n          gradientUnits=\"userSpaceOnUse\"\n          x1=\"655.1039\"\n          x2=\"654.9725\"\n          y1=\"1729.8617\"\n          y2=\"1731.0027\"\n        >\n          <stop offset=\"5.400000e-03\" style=\"stop-color: #130cb5\" />\n          <stop offset=\"0.2823\" style=\"stop-color: #285ecb\" />\n          <stop offset=\"0.538\" style=\"stop-color: #3aa3dd\" />\n          <stop offset=\"0.75\" style=\"stop-color: #47d5eb\" />\n          <stop offset=\"0.9102\" style=\"stop-color: #4ff4f3\" />\n          <stop offset=\"1\" style=\"stop-color: #52fff6\" />\n        </linearGradient>\n\n        <linearGradient\n          id=\"SVGID_102_\"\n          gradientTransform=\"matrix(-12.3655 0 0 -48.4174 8136.0479 83952.2812)\"\n          gradientUnits=\"userSpaceOnUse\"\n          x1=\"655.2512\"\n          x2=\"655.2512\"\n          y1=\"1731.6409\"\n          y2=\"1730.6409\"\n        >\n          <stop offset=\"0\" style=\"stop-color: #fff\" />\n          <stop offset=\"1\" style=\"stop-color: #fff; stop-opacity: 0\" />\n        </linearGradient>\n\n        <polygon\n          enable-background=\"new    \"\n          fill=\"url(#SVGID_101_)\"\n          opacity=\"0.3007\"\n          points=\"\n\t\t\t39.7,152 27.4,159.1 27.4,117.9 39.7,110.7 \t\t\"\n          stroke=\"url(#SVGID_102_)\"\n          stroke-width=\"0.5\"\n        />\n\n        <g\n          transform=\"translate(72.980000, 101.800000) scale(-1, 1) translate(-72.980000, -101.800000) translate(13.480000, 28.800000)\"\n        >\n          <linearGradient\n            id=\"SVGID_103_\"\n            gradientTransform=\"matrix(116.289 0 0 -102.7515 61913.2812 176111.3281)\"\n            gradientUnits=\"userSpaceOnUse\"\n            x1=\"-531.9647\"\n            x2=\"-531.5446\"\n            y1=\"1713.5308\"\n            y2=\"1713.0157\"\n          >\n            <stop offset=\"0\" style=\"stop-color: #fff\" />\n            <stop offset=\"1\" style=\"stop-color: #fff\" />\n          </linearGradient>\n\n          <linearGradient\n            id=\"SVGID_104_\"\n            gradientTransform=\"matrix(116.289 0 0 -102.7515 61913.2812 176111.3281)\"\n            gradientUnits=\"userSpaceOnUse\"\n            x1=\"-531.3857\"\n            x2=\"-532.1153\"\n            y1=\"1713.6992\"\n            y2=\"1713.3367\"\n          >\n            <stop offset=\"0\" style=\"stop-color: #f8ff24\" />\n            <stop offset=\"1\" style=\"stop-color: #fff\" />\n          </linearGradient>\n          <path\n            clip-rule=\"evenodd\"\n            d=\"\n\t\t\t\tM117.6,103.3c-6-3.4-9-14.2-11.9-24.6c-2.9-10.2-5.7-20.7-11.4-24c-5.7-3.3-8.6,3.9-11.4,10.8c-2.9,7.1-5.9,14.4-11.9,10.9\n\t\t\t\tc-6-3.5-9-14.2-11.8-24.6c-2.9-10.2-5.8-20.7-11.4-24s-8.6,3.9-11.4,10.8c-2.9,7.1-5.9,14.4-11.9,10.9c-6-3.4-9-14.2-11.8-24.6\n\t\t\t\tC9.9,14.9,7,4.3,1.3,1.1V0.5c6,3.4,9,14.2,11.9,24.6c2.8,10.2,5.7,20.7,11.4,24c5.7,3.3,8.6-3.9,11.4-10.8\n\t\t\t\tc2.9-7.1,5.9-14.4,11.8-10.9s9,14.2,11.9,24.6c2.9,10.2,5.7,20.7,11.4,24c5.7,3.2,8.6-3.9,11.4-10.8c2.9-7.1,5.9-14.4,11.8-10.9\n\t\t\t\tc6,3.5,9,14.2,11.9,24.6c2.9,10.2,5.8,20.7,11.4,24V103.3z\"\n            fill=\"url(#SVGID_103_)\"\n            fill-rule=\"evenodd\"\n            stroke=\"url(#SVGID_104_)\"\n            stroke-width=\"1.4256\"\n          />\n\n          <linearGradient\n            id=\"路径-2_1_\"\n            gradientTransform=\"matrix(117.4617 0 0 -144.8008 62535.8438 248597.6562)\"\n            gradientUnits=\"userSpaceOnUse\"\n            x1=\"-531.8884\"\n            x2=\"-531.8884\"\n            y1=\"1716.8223\"\n            y2=\"1715.8223\"\n          >\n            <stop offset=\"0\" style=\"stop-color: #fff\" />\n            <stop offset=\"1\" style=\"stop-color: #fff; stop-opacity: 0\" />\n          </linearGradient>\n          <path\n            id=\"路径-2\"\n            clip-rule=\"evenodd\"\n            d=\"M0.6,0.5v77.2l117,67.6l0.5-42\n\t\t\t\tc-3.3-3.2-5.5-5.6-6.5-7.3c-4.4-7.6-5.6-14.4-6.1-18.2c-1.1-7.7-9.1-26.4-15.2-23.3c-6.1,3.1-10.8,27.4-17.9,23.3\n\t\t\t\tc-7-4-15.5-32-17.3-39.4S46,22.8,41.4,29.7c-1,1.5-4.9,9.1-6,11.5c-7.7,15.9-13.7,7.5-19.1-5C14.1,31.1,9.8,15.4,6.2,6.8\n\t\t\t\tC5,3.9,3.2,1.8,0.6,0.5z\"\n            fill=\"url(#路径-2_1_)\"\n            fill-rule=\"evenodd\"\n          />\n          <g id=\"编组-7\" transform=\"translate(41.723113, 19.522022)\">\n            <path\n              id=\"Fill-11\"\n              clip-rule=\"evenodd\"\n              d=\"\n\t\t\t\t\tM5.4,0.8c3,1.9,5.4,6.4,5.4,10.1c0,3.7-2.4,5.2-5.4,3.4C2.4,12.4,0,7.9,0,4.2C0,0.4,2.4-1.1,5.4,0.8\"\n              enable-background=\"new    \"\n              fill=\"#AA69FF\"\n              fill-rule=\"evenodd\"\n              opacity=\"0.6\"\n            />\n            <path\n              clip-rule=\"evenodd\"\n              d=\"M9.7,10.7\n\t\t\t\t\tc0,3.2-2,4.5-4.3,2.9C3,12,1.1,8.1,1.1,4.9c0-3.2,2-4.5,4.3-2.9S9.7,7.5,9.7,10.7z\"\n              enable-background=\"new    \"\n              fill=\"#7005FF\"\n              fill-rule=\"evenodd\"\n              opacity=\"0.6\"\n            />\n            <path\n              clip-rule=\"evenodd\"\n              d=\"M7.4,9c0,1.6-1,2.2-2.2,1.4C4.1,9.6,3.1,7.6,3.1,6\n\t\t\t\t\tc0-1.6,1-2.2,2.2-1.4S7.4,7.4,7.4,9z\"\n              fill=\"#E7C769\"\n              fill-rule=\"evenodd\"\n            />\n          </g>\n        </g>\n\n        <linearGradient\n          id=\"SVGID_105_\"\n          gradientTransform=\"matrix(-12.3655 0 0 -61.3183 8158.1587 106537.3281)\"\n          gradientUnits=\"userSpaceOnUse\"\n          x1=\"655.2088\"\n          x2=\"655.1892\"\n          y1=\"1734.5078\"\n          y2=\"1734.9935\"\n        >\n          <stop offset=\"5.400000e-03\" style=\"stop-color: #130cb5\" />\n          <stop offset=\"0.2823\" style=\"stop-color: #285ecb\" />\n          <stop offset=\"0.538\" style=\"stop-color: #3aa3dd\" />\n          <stop offset=\"0.75\" style=\"stop-color: #47d5eb\" />\n          <stop offset=\"0.9102\" style=\"stop-color: #4ff4f3\" />\n          <stop offset=\"1\" style=\"stop-color: #52fff6\" />\n        </linearGradient>\n\n        <linearGradient\n          id=\"SVGID_106_\"\n          gradientTransform=\"matrix(-12.3655 0 0 -61.3183 8158.1587 106537.3281)\"\n          gradientUnits=\"userSpaceOnUse\"\n          x1=\"655.2512\"\n          x2=\"655.2512\"\n          y1=\"1736.0601\"\n          y2=\"1735.0601\"\n        >\n          <stop offset=\"0\" style=\"stop-color: #fff\" />\n          <stop offset=\"1\" style=\"stop-color: #fff; stop-opacity: 0\" />\n        </linearGradient>\n\n        <polygon\n          enable-background=\"new    \"\n          fill=\"url(#SVGID_105_)\"\n          opacity=\"0.3007\"\n          points=\"\n\t\t\t61.8,139.2 49.5,146.4 49.5,92.2 61.8,85.1 \t\t\"\n          stroke=\"url(#SVGID_106_)\"\n          stroke-width=\"0.5\"\n        />\n\n        <linearGradient\n          id=\"SVGID_107_\"\n          gradientTransform=\"matrix(-12.3655 0 0 -74.2182 8181.0142 129120.2578)\"\n          gradientUnits=\"userSpaceOnUse\"\n          x1=\"655.1729\"\n          x2=\"655.117\"\n          y1=\"1737.4004\"\n          y2=\"1738.1447\"\n        >\n          <stop offset=\"5.400000e-03\" style=\"stop-color: #130cb5\" />\n          <stop offset=\"0.2823\" style=\"stop-color: #285ecb\" />\n          <stop offset=\"0.538\" style=\"stop-color: #3aa3dd\" />\n          <stop offset=\"0.75\" style=\"stop-color: #47d5eb\" />\n          <stop offset=\"0.9102\" style=\"stop-color: #4ff4f3\" />\n          <stop offset=\"1\" style=\"stop-color: #52fff6\" />\n        </linearGradient>\n\n        <linearGradient\n          id=\"SVGID_108_\"\n          gradientTransform=\"matrix(-12.3655 0 0 -74.2182 8181.0142 129120.2578)\"\n          gradientUnits=\"userSpaceOnUse\"\n          x1=\"655.2512\"\n          x2=\"655.2512\"\n          y1=\"1738.9429\"\n          y2=\"1737.9429\"\n        >\n          <stop offset=\"0\" style=\"stop-color: #fff\" />\n          <stop offset=\"1\" style=\"stop-color: #fff; stop-opacity: 0\" />\n        </linearGradient>\n\n        <polygon\n          enable-background=\"new    \"\n          fill=\"url(#SVGID_107_)\"\n          opacity=\"0.3007\"\n          points=\"\n\t\t\t84.7,126 72.3,133.2 72.3,66.1 84.7,59 \t\t\"\n          stroke=\"url(#SVGID_108_)\"\n          stroke-width=\"0.5\"\n        />\n      </g>\n      <g id=\"编组-16\" transform=\"translate(51.000000, 152.000000)\">\n        <g id=\"云\">\n          <g transform=\"translate(0.000000, 0.000000)\">\n            <g>\n              <linearGradient\n                id=\"SVGID_109_\"\n                gradientTransform=\"matrix(100.2748 0 0 -95.0757 -5012.0137 157345.375)\"\n                gradientUnits=\"userSpaceOnUse\"\n                x1=\"49.8504\"\n                x2=\"50.8986\"\n                y1=\"1654.3923\"\n                y2=\"1654.5082\"\n              >\n                <stop offset=\"0\" style=\"stop-color: #544cde\" />\n                <stop offset=\"0.5868\" style=\"stop-color: #a7a0ff\" />\n                <stop offset=\"1\" style=\"stop-color: #7b74ff\" />\n              </linearGradient>\n              <path\n                clip-rule=\"evenodd\"\n                d=\"M70.3,14.7c0,0.3,0,0.5,0,0.7\n\t\t\t\t\t\tc1.5-1.4,3.2-2.6,4.8-3.5c5.6-3.2,10.6-3.3,14-0.8l11.1,6.6l-7.1,11c-2.1,10-8.9,20.5-17.2,25.7L23.9,84.6L18,95.1L6.2,88l0,0\n\t\t\t\t\t\tC2.4,85.8,0,81.2,0,74.5c0-13.1,9.2-29.2,20.6-35.8c3-1.7,5.9-2.7,8.4-2.8c1.1-12.6,9.9-26.9,20.5-33\n\t\t\t\t\t\tC61-3.8,70.3,1.6,70.3,14.7z\"\n                fill=\"url(#SVGID_109_)\"\n                fill-rule=\"evenodd\"\n              />\n            </g>\n            <defs>\n              <filter\n                id=\"Adobe_OpacityMaskFilter_20_\"\n                filterUnits=\"userSpaceOnUse\"\n                height=\"67.1\"\n                width=\"76.3\"\n                x=\"58.3\"\n                y=\"-13.8\"\n              >\n                <feColorMatrix\n                  type=\"matrix\"\n                  values=\"1 0 0 0 0  0 1 0 0 0  0 0 1 0 0  0 0 0 1 0\"\n                />\n              </filter>\n            </defs>\n            <mask\n              id=\"mask-168_1_\"\n              height=\"67.1\"\n              maskUnits=\"userSpaceOnUse\"\n              width=\"76.3\"\n              x=\"58.3\"\n              y=\"-13.8\"\n            >\n              <g filter=\"url(#Adobe_OpacityMaskFilter_20_)\">\n                <path\n                  id=\"path-167_1_\"\n                  clip-rule=\"evenodd\"\n                  d=\"M70.3,14.7c0,0.3,0,0.5,0,0.7\n\t\t\t\t\t\t\tc1.5-1.4,3.2-2.6,4.8-3.5c5.6-3.2,10.6-3.3,14-0.8l11.1,6.6l-7.1,11c-2.1,10-8.9,20.5-17.2,25.7L23.9,84.6L18,95.1L6.2,88l0,0\n\t\t\t\t\t\t\tC2.4,85.8,0,81.2,0,74.5c0-13.1,9.2-29.2,20.6-35.8c3-1.7,5.9-2.7,8.4-2.8c1.1-12.6,9.9-26.9,20.5-33\n\t\t\t\t\t\t\tC61-3.8,70.3,1.6,70.3,14.7z\"\n                  fill=\"#FFFFFF\"\n                  fill-rule=\"evenodd\"\n                />\n              </g>\n            </mask>\n\n            <linearGradient\n              id=\"矩形备份-3_1_\"\n              gradientTransform=\"matrix(76.2914 0 0 -67.1042 -3730.5828 110741.1094)\"\n              gradientUnits=\"userSpaceOnUse\"\n              x1=\"49.8816\"\n              x2=\"50.4973\"\n              y1=\"1649.8344\"\n              y2=\"1650.4406\"\n            >\n              <stop offset=\"0\" style=\"stop-color: #4f3eff\" />\n              <stop offset=\"0.4096\" style=\"stop-color: #9c92ff\" />\n              <stop offset=\"0.7979\" style=\"stop-color: #2519da\" />\n              <stop offset=\"1\" style=\"stop-color: #0d04ab\" />\n            </linearGradient>\n\n            <path\n              id=\"矩形备份-3\"\n              clip-rule=\"evenodd\"\n              d=\"\n\t\t\t\t\tM100.1-13.4l29.3,15.6l5.2,15.3C112,43,96.4,54.9,87.8,53.2S69.3,34.5,58.3,2.2c10.3-7,18.4-11.6,24.2-13.7\n\t\t\t\t\tS94.1-14.3,100.1-13.4z\"\n              fill=\"url(#矩形备份-3_1_)\"\n              fill-rule=\"evenodd\"\n              mask=\"url(#mask-168_1_)\"\n            />\n            <defs>\n              <filter\n                id=\"Adobe_OpacityMaskFilter_21_\"\n                filterUnits=\"userSpaceOnUse\"\n                height=\"67.1\"\n                width=\"76.3\"\n                x=\"0\"\n                y=\"-13.8\"\n              >\n                <feColorMatrix\n                  type=\"matrix\"\n                  values=\"1 0 0 0 0  0 1 0 0 0  0 0 1 0 0  0 0 0 1 0\"\n                />\n              </filter>\n            </defs>\n            <mask\n              id=\"mask-168_2_\"\n              height=\"67.1\"\n              maskUnits=\"userSpaceOnUse\"\n              width=\"76.3\"\n              x=\"0\"\n              y=\"-13.8\"\n            >\n              <g filter=\"url(#Adobe_OpacityMaskFilter_21_)\">\n                <path\n                  id=\"path-167_2_\"\n                  clip-rule=\"evenodd\"\n                  d=\"M70.3,14.7c0,0.3,0,0.5,0,0.7\n\t\t\t\t\t\t\tc1.5-1.4,3.2-2.6,4.8-3.5c5.6-3.2,10.6-3.3,14-0.8l11.1,6.6l-7.1,11c-2.1,10-8.9,20.5-17.2,25.7L23.9,84.6L18,95.1L6.2,88l0,0\n\t\t\t\t\t\t\tC2.4,85.8,0,81.2,0,74.5c0-13.1,9.2-29.2,20.6-35.8c3-1.7,5.9-2.7,8.4-2.8c1.1-12.6,9.9-26.9,20.5-33\n\t\t\t\t\t\t\tC61-3.8,70.3,1.6,70.3,14.7z\"\n                  fill=\"#FFFFFF\"\n                  fill-rule=\"evenodd\"\n                />\n              </g>\n            </mask>\n\n            <linearGradient\n              id=\"矩形备份-2_1_\"\n              gradientTransform=\"matrix(76.2914 0 0 -67.1042 -3788.8608 110741.1094)\"\n              gradientUnits=\"userSpaceOnUse\"\n              x1=\"49.8816\"\n              x2=\"50.4973\"\n              y1=\"1649.8344\"\n              y2=\"1650.4406\"\n            >\n              <stop offset=\"0\" style=\"stop-color: #4f3eff\" />\n              <stop offset=\"0.4096\" style=\"stop-color: #9c92ff\" />\n              <stop offset=\"0.7979\" style=\"stop-color: #2519da\" />\n              <stop offset=\"1\" style=\"stop-color: #0d04ab\" />\n            </linearGradient>\n\n            <path\n              id=\"矩形备份-2\"\n              clip-rule=\"evenodd\"\n              d=\"\n\t\t\t\t\tM41.8-13.4L71.1,2.2l5.2,15.3C53.7,43,38.1,54.9,29.5,53.2S11,34.5,0,2.2c10.3-7,18.4-11.6,24.2-13.7S35.8-14.3,41.8-13.4z\"\n              fill=\"url(#矩形备份-2_1_)\"\n              fill-rule=\"evenodd\"\n              mask=\"url(#mask-168_2_)\"\n            />\n            <defs>\n              <filter\n                id=\"Adobe_OpacityMaskFilter_22_\"\n                filterUnits=\"userSpaceOnUse\"\n                height=\"67.1\"\n                width=\"76.3\"\n                x=\"-12.7\"\n                y=\"35.5\"\n              >\n                <feColorMatrix\n                  type=\"matrix\"\n                  values=\"1 0 0 0 0  0 1 0 0 0  0 0 1 0 0  0 0 0 1 0\"\n                />\n              </filter>\n            </defs>\n            <mask\n              id=\"mask-168_3_\"\n              height=\"67.1\"\n              maskUnits=\"userSpaceOnUse\"\n              width=\"76.3\"\n              x=\"-12.7\"\n              y=\"35.5\"\n            >\n              <g filter=\"url(#Adobe_OpacityMaskFilter_22_)\">\n                <path\n                  id=\"path-167_3_\"\n                  clip-rule=\"evenodd\"\n                  d=\"M70.3,14.7c0,0.3,0,0.5,0,0.7\n\t\t\t\t\t\t\tc1.5-1.4,3.2-2.6,4.8-3.5c5.6-3.2,10.6-3.3,14-0.8l11.1,6.6l-7.1,11c-2.1,10-8.9,20.5-17.2,25.7L23.9,84.6L18,95.1L6.2,88l0,0\n\t\t\t\t\t\t\tC2.4,85.8,0,81.2,0,74.5c0-13.1,9.2-29.2,20.6-35.8c3-1.7,5.9-2.7,8.4-2.8c1.1-12.6,9.9-26.9,20.5-33\n\t\t\t\t\t\t\tC61-3.8,70.3,1.6,70.3,14.7z\"\n                  fill=\"#FFFFFF\"\n                  fill-rule=\"evenodd\"\n                />\n              </g>\n            </mask>\n\n            <linearGradient\n              id=\"SVGID_110_\"\n              gradientTransform=\"matrix(76.2914 0 0 -67.1042 -3801.5762 110790.3594)\"\n              gradientUnits=\"userSpaceOnUse\"\n              x1=\"49.8816\"\n              x2=\"50.4973\"\n              y1=\"1649.8344\"\n              y2=\"1650.4406\"\n            >\n              <stop offset=\"0\" style=\"stop-color: #4f3eff\" />\n              <stop offset=\"0.4096\" style=\"stop-color: #9c92ff\" />\n              <stop offset=\"0.7979\" style=\"stop-color: #2519da\" />\n              <stop offset=\"1\" style=\"stop-color: #0d04ab\" />\n            </linearGradient>\n            <path\n              clip-rule=\"evenodd\"\n              d=\"M29.1,35.9l29.3,15.6\n\t\t\t\t\tl5.2,15.3C41,92.3,25.4,104.1,16.8,102.4c-8.7-1.7-18.5-18.7-29.5-50.9c10.3-7,18.4-11.6,24.2-13.7S23.1,35,29.1,35.9z\"\n              fill=\"url(#SVGID_110_)\"\n              fill-rule=\"evenodd\"\n              mask=\"url(#mask-168_3_)\"\n            />\n          </g>\n\n          <linearGradient\n            id=\"Fill-1备份_1_\"\n            gradientTransform=\"matrix(93.8386 0 0 -89.2609 -4672.3262 147666.9375)\"\n            gradientUnits=\"userSpaceOnUse\"\n            x1=\"50.6106\"\n            x2=\"50.1002\"\n            y1=\"1654.2188\"\n            y2=\"1652.9083\"\n          >\n            <stop offset=\"0\" style=\"stop-color: #5e51e4\" />\n            <stop offset=\"1\" style=\"stop-color: #060071\" />\n          </linearGradient>\n          <path\n            id=\"Fill-1备份\"\n            clip-rule=\"evenodd\"\n            d=\"M86.6,18.8\n\t\t\t\tc10.3-6,18.6-1.1,18.7,10.7c0,11.6-7.9,25.6-17.9,31.8l-55.2,32l-0.4,0.2c-11.3,6.3-20.3,0.9-20.4-12.1\n\t\t\t\tc0-13.1,9.2-29.2,20.6-35.8c3-1.7,5.9-2.7,8.4-2.8c1.1-12.6,9.9-26.9,20.5-33c11.4-6.6,20.7-1.3,20.8,11.9c0,0.3,0,0.5,0,0.7\n\t\t\t\tC83.3,21,84.9,19.7,86.6,18.8\"\n            fill=\"url(#Fill-1备份_1_)\"\n            fill-rule=\"evenodd\"\n          />\n        </g>\n      </g>\n    </g>\n  </svg>\n</template>\n"
  },
  {
    "path": "packages/effects/layouts/src/authentication/index.ts",
    "content": "export { default as AuthPageLayout } from './authentication.vue';\nexport * from './types';\n"
  },
  {
    "path": "packages/effects/layouts/src/authentication/toolbar.vue",
    "content": "<script setup lang=\"ts\">\nimport type { ToolbarType } from './types';\n\nimport { computed } from 'vue';\n\nimport { preferences } from '@vben/preferences';\n\nimport {\n  AuthenticationColorToggle,\n  AuthenticationLayoutToggle,\n  LanguageToggle,\n  ThemeToggle,\n} from '../widgets';\n\ninterface Props {\n  toolbarList?: ToolbarType[];\n}\n\ndefineOptions({\n  name: 'AuthenticationToolbar',\n});\n\nconst props = withDefaults(defineProps<Props>(), {\n  toolbarList: () => ['color', 'language', 'layout', 'theme'],\n});\n\nconst showColor = computed(() => props.toolbarList.includes('color'));\nconst showLayout = computed(() => props.toolbarList.includes('layout'));\nconst showLanguage = computed(() => props.toolbarList.includes('language'));\nconst showTheme = computed(() => props.toolbarList.includes('theme'));\n</script>\n\n<template>\n  <div\n    :class=\"{\n      'bg-accent rounded-3xl px-3 py-1': toolbarList.length > 1,\n    }\"\n    class=\"flex-center absolute right-2 top-4 z-10\"\n  >\n    <!-- Only show on medium and larger screens -->\n    <div class=\"hidden md:flex\">\n      <AuthenticationColorToggle v-if=\"showColor\" />\n      <AuthenticationLayoutToggle v-if=\"showLayout\" />\n    </div>\n    <!-- Always show Language and Theme toggles -->\n    <LanguageToggle v-if=\"showLanguage && preferences.widget.languageToggle\" />\n    <ThemeToggle v-if=\"showTheme && preferences.widget.themeToggle\" />\n  </div>\n</template>\n"
  },
  {
    "path": "packages/effects/layouts/src/authentication/types.ts",
    "content": "export type ToolbarType = 'color' | 'language' | 'layout' | 'theme';\n"
  },
  {
    "path": "packages/effects/layouts/src/basic/README.md",
    "content": "## layout\n\n### header\n\n- 支持N个自定义插槽，命名方式：header-right-n，header-left-n\n- header-left-n ，排序方式：0-19 ,breadcrumb 21-x\n- header-right-n ，排序方式：0-49，global-search，51-59，theme-toggle，61-69，language-toggle，71-79，fullscreen，81-89，notification，91-149，user-dropdown，151-x\n"
  },
  {
    "path": "packages/effects/layouts/src/basic/content/content-spinner.vue",
    "content": "<script lang=\"ts\" setup>\nimport { VbenSpinner } from '@vben-core/shadcn-ui';\n\nimport { useContentSpinner } from './use-content-spinner';\n\ndefineOptions({ name: 'LayoutContentSpinner' });\n\nconst { spinning } = useContentSpinner();\n</script>\n<template>\n  <VbenSpinner :spinning=\"spinning\" />\n</template>\n"
  },
  {
    "path": "packages/effects/layouts/src/basic/content/content.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { VNode } from 'vue';\nimport type {\n  RouteLocationNormalizedLoaded,\n  RouteLocationNormalizedLoadedGeneric,\n} from 'vue-router';\n\nimport { computed } from 'vue';\nimport { RouterView } from 'vue-router';\n\nimport { preferences, usePreferences } from '@vben/preferences';\nimport { getTabKey, storeToRefs, useTabbarStore } from '@vben/stores';\n\nimport { IFrameRouterView } from '../../iframe';\n\ndefineOptions({ name: 'LayoutContent' });\n\nconst tabbarStore = useTabbarStore();\nconst { keepAlive } = usePreferences();\n\nconst { getCachedTabs, getExcludeCachedTabs, renderRouteView } =\n  storeToRefs(tabbarStore);\n\n/**\n * 是否使用动画\n */\nconst getEnabledTransition = computed(() => {\n  const { transition } = preferences;\n  const transitionName = transition.name;\n  return transitionName && transition.enable;\n});\n\n// 页面切换动画\nfunction getTransitionName(_route: RouteLocationNormalizedLoaded) {\n  // 如果偏好设置未设置，则不使用动画\n  const { tabbar, transition } = preferences;\n  const transitionName = transition.name;\n  if (!transitionName || !transition.enable) {\n    return;\n  }\n\n  // 标签页未启用或者未开启缓存，则使用全局配置动画\n  if (!tabbar.enable || !keepAlive) {\n    return transitionName;\n  }\n\n  // 如果页面已经加载过，则不使用动画\n  // if (route.meta.loaded) {\n  //   return;\n  // }\n  // 已经打开且已经加载过的页面不使用动画\n  // const inTabs = getCachedTabs.value.includes(route.name as string);\n\n  // return inTabs && route.meta.loaded ? undefined : transitionName;\n  return transitionName;\n}\n\n/**\n * 转换组件，自动添加 name\n * @param component\n */\nfunction transformComponent(\n  component: VNode,\n  route: RouteLocationNormalizedLoadedGeneric,\n) {\n  // 组件视图未找到，如果有设置后备视图，则返回后备视图，如果没有，则抛出错误\n  if (!component) {\n    console.error(\n      'Component view not found，please check the route configuration',\n    );\n    return undefined;\n  }\n\n  const routeName = route.name as string;\n  // 如果组件没有 name，则直接返回\n  if (!routeName) {\n    return component;\n  }\n  const componentName = (component?.type as any)?.name;\n\n  // 已经设置过 name，则直接返回\n  if (componentName) {\n    return component;\n  }\n\n  // componentName 与 routeName 一致，则直接返回\n  if (componentName === routeName) {\n    return component;\n  }\n\n  // 设置 name\n  component.type ||= {};\n  (component.type as any).name = routeName;\n\n  return component;\n}\n</script>\n\n<template>\n  <div class=\"relative h-full\">\n    <IFrameRouterView />\n    <RouterView v-slot=\"{ Component, route }\">\n      <Transition\n        v-if=\"getEnabledTransition\"\n        :name=\"getTransitionName(route)\"\n        appear\n        mode=\"out-in\"\n      >\n        <KeepAlive\n          v-if=\"keepAlive\"\n          :exclude=\"getExcludeCachedTabs\"\n          :include=\"getCachedTabs\"\n        >\n          <component\n            :is=\"transformComponent(Component, route)\"\n            v-if=\"renderRouteView\"\n            v-show=\"!route.meta.iframeSrc\"\n            :key=\"getTabKey(route)\"\n          />\n        </KeepAlive>\n        <component\n          :is=\"Component\"\n          v-else-if=\"renderRouteView\"\n          :key=\"getTabKey(route)\"\n        />\n      </Transition>\n      <template v-else>\n        <KeepAlive\n          v-if=\"keepAlive\"\n          :exclude=\"getExcludeCachedTabs\"\n          :include=\"getCachedTabs\"\n        >\n          <component\n            :is=\"transformComponent(Component, route)\"\n            v-if=\"renderRouteView\"\n            v-show=\"!route.meta.iframeSrc\"\n            :key=\"getTabKey(route)\"\n          />\n        </KeepAlive>\n        <component\n          :is=\"Component\"\n          v-else-if=\"renderRouteView\"\n          :key=\"getTabKey(route)\"\n        />\n      </template>\n    </RouterView>\n  </div>\n</template>\n"
  },
  {
    "path": "packages/effects/layouts/src/basic/content/index.ts",
    "content": "export { default as LayoutContent } from './content.vue';\nexport { default as LayoutContentSpinner } from './content-spinner.vue';\n"
  },
  {
    "path": "packages/effects/layouts/src/basic/content/use-content-spinner.ts",
    "content": "import { computed, ref } from 'vue';\nimport { useRouter } from 'vue-router';\n\nimport { preferences } from '@vben/preferences';\n\nfunction useContentSpinner() {\n  const spinning = ref(false);\n  const startTime = ref(0);\n  const router = useRouter();\n  const minShowTime = 500; // 最小显示时间\n  const enableLoading = computed(() => preferences.transition.loading);\n\n  // 结束加载动画\n  const onEnd = () => {\n    if (!enableLoading.value) {\n      return;\n    }\n    const processTime = performance.now() - startTime.value;\n    if (processTime < minShowTime) {\n      setTimeout(() => {\n        spinning.value = false;\n      }, minShowTime - processTime);\n    } else {\n      spinning.value = false;\n    }\n  };\n\n  // 路由前置守卫\n  router.beforeEach((to) => {\n    if (to.meta.loaded || !enableLoading.value || to.meta.iframeSrc) {\n      return true;\n    }\n    startTime.value = performance.now();\n    spinning.value = true;\n    return true;\n  });\n\n  // 路由后置守卫\n  router.afterEach((to) => {\n    if (to.meta.loaded || !enableLoading.value || to.meta.iframeSrc) {\n      return true;\n    }\n    onEnd();\n    return true;\n  });\n\n  return { spinning };\n}\n\nexport { useContentSpinner };\n"
  },
  {
    "path": "packages/effects/layouts/src/basic/copyright/copyright.vue",
    "content": "<script lang=\"ts\" setup>\ninterface Props {\n  companyName?: string;\n  companySiteLink?: string;\n  date?: string;\n  icp?: string;\n  icpLink?: string;\n}\n\ndefineOptions({\n  name: 'Copyright',\n});\n\nwithDefaults(defineProps<Props>(), {\n  companyName: 'Vben Admin',\n  companySiteLink: '',\n  date: '2026',\n  icp: '',\n  icpLink: '',\n});\n</script>\n\n<template>\n  <div class=\"text-md flex-center\">\n    <!-- ICP Link -->\n    <a\n      v-if=\"icp\"\n      :href=\"icpLink || 'javascript:void(0)'\"\n      class=\"hover:text-primary-hover mx-1\"\n      target=\"_blank\"\n    >\n      {{ icp }}\n    </a>\n\n    <!-- Copyright Text -->\n    Copyright © {{ date }}\n\n    <!-- Company Link -->\n    <a\n      v-if=\"companyName\"\n      :href=\"companySiteLink || 'javascript:void(0)'\"\n      class=\"hover:text-primary-hover mx-1\"\n      target=\"_blank\"\n    >\n      {{ companyName }}\n    </a>\n  </div>\n</template>\n"
  },
  {
    "path": "packages/effects/layouts/src/basic/copyright/index.ts",
    "content": "export { default as Copyright } from './copyright.vue';\n"
  },
  {
    "path": "packages/effects/layouts/src/basic/footer/footer.vue",
    "content": "<script lang=\"ts\" setup>\ndefineOptions({\n  name: 'LayoutFooter',\n});\n</script>\n\n<template>\n  <div class=\"flex-center text-muted-foreground relative h-full w-full text-xs\">\n    <slot></slot>\n  </div>\n</template>\n"
  },
  {
    "path": "packages/effects/layouts/src/basic/footer/index.ts",
    "content": "export { default as LayoutFooter } from './footer.vue';\n"
  },
  {
    "path": "packages/effects/layouts/src/basic/header/header.vue",
    "content": "<script lang=\"ts\" setup>\nimport { useRefresh } from '@vben/hooks';\nimport { RotateCw } from '@vben/icons';\nimport { preferences, usePreferences } from '@vben/preferences';\nimport { useAccessStore } from '@vben/stores';\nimport { VbenFullScreen, VbenIconButton } from '@vben-core/shadcn-ui';\nimport { computed, useSlots } from 'vue';\n\nimport {\n  GlobalSearch,\n  LanguageToggle,\n  PreferencesButton,\n  ThemeToggle,\n} from '../../widgets';\n\ninterface Props {\n  /**\n   * Logo 主题\n   */\n  theme?: string;\n}\n\ndefineOptions({\n  name: 'LayoutHeader',\n});\n\nwithDefaults(defineProps<Props>(), {\n  theme: 'light',\n});\n\nconst emit = defineEmits<{ clearPreferencesAndLogout: [] }>();\n\nconst REFERENCE_VALUE = 50;\n\nconst accessStore = useAccessStore();\nconst { globalSearchShortcutKey, preferencesButtonPosition } = usePreferences();\nconst slots = useSlots();\nconst { refresh } = useRefresh();\n\nconst rightSlots = computed(() => {\n  const list = [{ index: REFERENCE_VALUE + 100, name: 'user-dropdown' }];\n  if (preferences.widget.globalSearch) {\n    list.push({\n      index: REFERENCE_VALUE,\n      name: 'global-search',\n    });\n  }\n\n  if (preferencesButtonPosition.value.header) {\n    list.push({\n      index: REFERENCE_VALUE + 10,\n      name: 'preferences',\n    });\n  }\n  if (preferences.widget.themeToggle) {\n    list.push({\n      index: REFERENCE_VALUE + 20,\n      name: 'theme-toggle',\n    });\n  }\n  if (preferences.widget.languageToggle) {\n    list.push({\n      index: REFERENCE_VALUE + 30,\n      name: 'language-toggle',\n    });\n  }\n  if (preferences.widget.fullscreen) {\n    list.push({\n      index: REFERENCE_VALUE + 40,\n      name: 'fullscreen',\n    });\n  }\n  if (preferences.widget.notification) {\n    list.push({\n      index: REFERENCE_VALUE + 50,\n      name: 'notification',\n    });\n  }\n\n  Object.keys(slots).forEach((key) => {\n    const name = key.split('-');\n    if (key.startsWith('header-right')) {\n      list.push({ index: Number(name[2]), name: key });\n    }\n  });\n  return list.sort((a, b) => a.index - b.index);\n});\n\nconst leftSlots = computed(() => {\n  const list: Array<{ index: number; name: string }> = [];\n\n  if (preferences.widget.refresh) {\n    list.push({\n      index: 0,\n      name: 'refresh',\n    });\n  }\n\n  Object.keys(slots).forEach((key) => {\n    const name = key.split('-');\n    if (key.startsWith('header-left')) {\n      list.push({ index: Number(name[2]), name: key });\n    }\n  });\n  return list.sort((a, b) => a.index - b.index);\n});\n\nfunction clearPreferencesAndLogout() {\n  emit('clearPreferencesAndLogout');\n}\n</script>\n\n<template>\n  <template\n    v-for=\"slot in leftSlots.filter((item) => item.index < REFERENCE_VALUE)\"\n    :key=\"slot.name\"\n  >\n    <slot :name=\"slot.name\">\n      <template v-if=\"slot.name === 'refresh'\">\n        <VbenIconButton class=\"my-0 mr-1 rounded-md\" @click=\"refresh\">\n          <RotateCw class=\"size-4\" />\n        </VbenIconButton>\n      </template>\n    </slot>\n  </template>\n  <div class=\"flex-center hidden lg:block\">\n    <slot name=\"breadcrumb\"></slot>\n  </div>\n  <template\n    v-for=\"slot in leftSlots.filter((item) => item.index > REFERENCE_VALUE)\"\n    :key=\"slot.name\"\n  >\n    <slot :name=\"slot.name\"></slot>\n  </template>\n  <div\n    :class=\"`menu-align-${preferences.header.menuAlign}`\"\n    class=\"flex h-full min-w-0 flex-1 items-center\"\n  >\n    <slot name=\"menu\"></slot>\n  </div>\n  <div class=\"flex h-full min-w-0 flex-shrink-0 items-center\">\n    <template v-for=\"slot in rightSlots\" :key=\"slot.name\">\n      <slot :name=\"slot.name\">\n        <template v-if=\"slot.name === 'global-search'\">\n          <GlobalSearch\n            :enable-shortcut-key=\"globalSearchShortcutKey\"\n            :menus=\"accessStore.accessMenus\"\n            class=\"mr-1 sm:mr-4\"\n          />\n        </template>\n\n        <template v-else-if=\"slot.name === 'preferences'\">\n          <PreferencesButton\n            class=\"mr-1\"\n            @clear-preferences-and-logout=\"clearPreferencesAndLogout\"\n          />\n        </template>\n        <template v-else-if=\"slot.name === 'theme-toggle'\">\n          <ThemeToggle class=\"mr-1 mt-[2px]\" />\n        </template>\n        <template v-else-if=\"slot.name === 'language-toggle'\">\n          <LanguageToggle class=\"mr-1\" />\n        </template>\n        <template v-else-if=\"slot.name === 'fullscreen'\">\n          <VbenFullScreen class=\"mr-1\" />\n        </template>\n      </slot>\n    </template>\n  </div>\n</template>\n<style lang=\"scss\" scoped>\n.menu-align-start {\n  --menu-align: start;\n}\n\n.menu-align-center {\n  --menu-align: center;\n}\n\n.menu-align-end {\n  --menu-align: end;\n}\n</style>\n"
  },
  {
    "path": "packages/effects/layouts/src/basic/header/index.ts",
    "content": "export { default as LayoutHeader } from './header.vue';\n"
  },
  {
    "path": "packages/effects/layouts/src/basic/index.ts",
    "content": "export { default as BasicLayout } from './layout.vue';\n"
  },
  {
    "path": "packages/effects/layouts/src/basic/layout.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { SetupContext } from 'vue';\nimport type { RouteLocationNormalizedLoaded } from 'vue-router';\n\nimport type { MenuRecordRaw } from '@vben/types';\n\nimport { computed, onMounted, useSlots, watch } from 'vue';\nimport { useRoute } from 'vue-router';\n\nimport { useRefresh } from '@vben/hooks';\nimport { $t, i18n } from '@vben/locales';\nimport {\n  preferences,\n  updatePreferences,\n  usePreferences,\n} from '@vben/preferences';\nimport { useAccessStore } from '@vben/stores';\nimport { cloneDeep, mapTree } from '@vben/utils';\n\nimport { VbenAdminLayout } from '@vben-core/layout-ui';\nimport { VbenBackTop, VbenLogo } from '@vben-core/shadcn-ui';\n\nimport { Breadcrumb, CheckUpdates, Preferences } from '../widgets';\nimport { LayoutContent, LayoutContentSpinner } from './content';\nimport { Copyright } from './copyright';\nimport { LayoutFooter } from './footer';\nimport { LayoutHeader } from './header';\nimport {\n  LayoutExtraMenu,\n  LayoutMenu,\n  LayoutMixedMenu,\n  useExtraMenu,\n  useMixedMenu,\n} from './menu';\nimport { LayoutTabbar } from './tabbar';\n\ndefineOptions({ name: 'BasicLayout' });\n\nconst emit = defineEmits<{ clearPreferencesAndLogout: []; clickLogo: [] }>();\n\nconst {\n  isDark,\n  isHeaderNav,\n  isMixedNav,\n  isMobile,\n  isSideMixedNav,\n  isHeaderMixedNav,\n  isHeaderSidebarNav,\n  layout,\n  preferencesButtonPosition,\n  sidebarCollapsed,\n  theme,\n} = usePreferences();\nconst accessStore = useAccessStore();\nconst { refresh } = useRefresh();\n\nconst sidebarTheme = computed(() => {\n  const dark = isDark.value || preferences.theme.semiDarkSidebar;\n  return dark ? 'dark' : 'light';\n});\n\nconst headerTheme = computed(() => {\n  const dark = isDark.value || preferences.theme.semiDarkHeader;\n  return dark ? 'dark' : 'light';\n});\n\nconst logoClass = computed(() => {\n  const { collapsedShowTitle } = preferences.sidebar;\n  const classes: string[] = [];\n\n  if (collapsedShowTitle && sidebarCollapsed.value && !isMixedNav.value) {\n    classes.push('mx-auto');\n  }\n\n  if (isSideMixedNav.value) {\n    classes.push('flex-center');\n  }\n\n  return classes.join(' ');\n});\n\nconst isMenuRounded = computed(() => {\n  return preferences.navigation.styleType === 'rounded';\n});\n\nconst logoCollapsed = computed(() => {\n  if (isMobile.value && sidebarCollapsed.value) {\n    return true;\n  }\n  if (isHeaderNav.value || isMixedNav.value || isHeaderSidebarNav.value) {\n    return false;\n  }\n  return (\n    sidebarCollapsed.value || isSideMixedNav.value || isHeaderMixedNav.value\n  );\n});\n\nconst showHeaderNav = computed(() => {\n  return (\n    !isMobile.value &&\n    (isHeaderNav.value || isMixedNav.value || isHeaderMixedNav.value)\n  );\n});\n\nconst {\n  handleMenuSelect,\n  handleMenuOpen,\n  headerActive,\n  headerMenus,\n  sidebarActive,\n  sidebarMenus,\n  mixHeaderMenus,\n  sidebarVisible,\n} = useMixedMenu();\n\n// 侧边多列菜单\nconst {\n  extraActiveMenu,\n  extraMenus,\n  handleDefaultSelect,\n  handleMenuMouseEnter,\n  handleMixedMenuSelect,\n  handleSideMouseLeave,\n  sidebarExtraVisible,\n} = useExtraMenu(mixHeaderMenus);\n\n/**\n * 包装菜单，翻译菜单名称\n * @param menus 原始菜单数据\n * @param deep 是否深度包装。对于双列布局，只需要包装第一层，因为更深层的数据会在扩展菜单中重新包装\n */\nfunction wrapperMenus(menus: MenuRecordRaw[], deep: boolean = true) {\n  return deep\n    ? mapTree(menus, (item) => {\n        return { ...cloneDeep(item), name: $t(item.name) };\n      })\n    : menus.map((item) => {\n        return { ...cloneDeep(item), name: $t(item.name) };\n      });\n}\n\nfunction toggleSidebar() {\n  updatePreferences({\n    sidebar: {\n      hidden: !preferences.sidebar.hidden,\n    },\n  });\n}\n\nfunction clearPreferencesAndLogout() {\n  emit('clearPreferencesAndLogout');\n}\n\nfunction clickLogo() {\n  emit('clickLogo');\n}\n\nfunction autoCollapseMenuByRouteMeta(route: RouteLocationNormalizedLoaded) {\n  // 只在双列模式下生效\n  if (\n    ['header-mixed-nav', 'sidebar-mixed-nav'].includes(\n      preferences.app.layout,\n    ) &&\n    route.meta &&\n    route.meta.hideInMenu\n  ) {\n    sidebarExtraVisible.value = false;\n  }\n}\n\nconst route = useRoute();\n\nonMounted(() => {\n  autoCollapseMenuByRouteMeta(route);\n});\n\nwatch(\n  () => preferences.app.layout,\n  async (val) => {\n    if (val === 'sidebar-mixed-nav' && preferences.sidebar.hidden) {\n      updatePreferences({\n        sidebar: {\n          hidden: false,\n        },\n      });\n    }\n  },\n);\n\n// 语言更新后，刷新页面\n// i18n.global.locale会在preference.app.locale变更之后才会更新，因此watchpreference.app.locale是不合适的，刷新页面时可能语言配置尚未完全加载完成\nwatch(i18n.global.locale, refresh, { flush: 'post' });\n\nconst slots: SetupContext['slots'] = useSlots();\nconst headerSlots = computed(() => {\n  return Object.keys(slots).filter((key) => key.startsWith('header-'));\n});\n</script>\n\n<template>\n  <VbenAdminLayout\n    v-model:sidebar-extra-visible=\"sidebarExtraVisible\"\n    :content-compact=\"preferences.app.contentCompact\"\n    :content-compact-width=\"preferences.app.contentCompactWidth\"\n    :content-padding=\"preferences.app.contentPadding\"\n    :content-padding-bottom=\"preferences.app.contentPaddingBottom\"\n    :content-padding-left=\"preferences.app.contentPaddingLeft\"\n    :content-padding-right=\"preferences.app.contentPaddingRight\"\n    :content-padding-top=\"preferences.app.contentPaddingTop\"\n    :footer-enable=\"preferences.footer.enable\"\n    :footer-fixed=\"preferences.footer.fixed\"\n    :footer-height=\"preferences.footer.height\"\n    :header-height=\"preferences.header.height\"\n    :header-hidden=\"preferences.header.hidden\"\n    :header-mode=\"preferences.header.mode\"\n    :header-theme=\"headerTheme\"\n    :header-toggle-sidebar-button=\"preferences.widget.sidebarToggle\"\n    :header-visible=\"preferences.header.enable\"\n    :is-mobile=\"preferences.app.isMobile\"\n    :layout=\"layout\"\n    :sidebar-collapse=\"preferences.sidebar.collapsed\"\n    :sidebar-collapse-show-title=\"preferences.sidebar.collapsedShowTitle\"\n    :sidebar-enable=\"sidebarVisible\"\n    :sidebar-collapsed-button=\"preferences.sidebar.collapsedButton\"\n    :sidebar-fixed-button=\"preferences.sidebar.fixedButton\"\n    :sidebar-expand-on-hover=\"preferences.sidebar.expandOnHover\"\n    :sidebar-extra-collapse=\"preferences.sidebar.extraCollapse\"\n    :sidebar-extra-collapsed-width=\"preferences.sidebar.extraCollapsedWidth\"\n    :sidebar-hidden=\"preferences.sidebar.hidden\"\n    :sidebar-mixed-width=\"preferences.sidebar.mixedWidth\"\n    :sidebar-theme=\"sidebarTheme\"\n    :sidebar-width=\"preferences.sidebar.width\"\n    :side-collapse-width=\"preferences.sidebar.collapseWidth\"\n    :tabbar-enable=\"preferences.tabbar.enable\"\n    :tabbar-height=\"preferences.tabbar.height\"\n    :z-index=\"preferences.app.zIndex\"\n    @side-mouse-leave=\"handleSideMouseLeave\"\n    @toggle-sidebar=\"toggleSidebar\"\n    @update:sidebar-collapse=\"\n      (value: boolean) => updatePreferences({ sidebar: { collapsed: value } })\n    \"\n    @update:sidebar-enable=\"\n      (value: boolean) => updatePreferences({ sidebar: { enable: value } })\n    \"\n    @update:sidebar-expand-on-hover=\"\n      (value: boolean) =>\n        updatePreferences({ sidebar: { expandOnHover: value } })\n    \"\n    @update:sidebar-extra-collapse=\"\n      (value: boolean) =>\n        updatePreferences({ sidebar: { extraCollapse: value } })\n    \"\n  >\n    <!-- logo -->\n    <template #logo>\n      <VbenLogo\n        v-if=\"preferences.logo.enable\"\n        :fit=\"preferences.logo.fit\"\n        :class=\"logoClass\"\n        :collapsed=\"logoCollapsed\"\n        :src=\"preferences.logo.source\"\n        :text=\"preferences.app.name\"\n        :theme=\"showHeaderNav ? headerTheme : theme\"\n        @click=\"clickLogo\"\n      >\n        <template v-if=\"$slots['logo-text']\" #text>\n          <slot name=\"logo-text\"></slot>\n        </template>\n      </VbenLogo>\n    </template>\n    <!-- 头部区域 -->\n    <template #header>\n      <LayoutHeader\n        :theme=\"theme\"\n        @clear-preferences-and-logout=\"clearPreferencesAndLogout\"\n      >\n        <template\n          v-if=\"!showHeaderNav && preferences.breadcrumb.enable\"\n          #breadcrumb\n        >\n          <Breadcrumb\n            :hide-when-only-one=\"preferences.breadcrumb.hideOnlyOne\"\n            :show-home=\"preferences.breadcrumb.showHome\"\n            :show-icon=\"preferences.breadcrumb.showIcon\"\n            :type=\"preferences.breadcrumb.styleType\"\n          />\n        </template>\n        <template v-if=\"showHeaderNav\" #menu>\n          <LayoutMenu\n            :default-active=\"headerActive\"\n            :menus=\"wrapperMenus(headerMenus)\"\n            :rounded=\"isMenuRounded\"\n            :theme=\"headerTheme\"\n            class=\"w-full\"\n            mode=\"horizontal\"\n            @select=\"handleMenuSelect\"\n          />\n        </template>\n        <template #user-dropdown>\n          <slot name=\"user-dropdown\"></slot>\n        </template>\n        <template #notification>\n          <slot name=\"notification\"></slot>\n        </template>\n        <template v-for=\"item in headerSlots\" #[item]>\n          <slot :name=\"item\"></slot>\n        </template>\n      </LayoutHeader>\n    </template>\n    <!-- 侧边菜单区域 -->\n    <template #menu>\n      <LayoutMenu\n        :accordion=\"preferences.navigation.accordion\"\n        :collapse=\"preferences.sidebar.collapsed\"\n        :collapse-show-title=\"preferences.sidebar.collapsedShowTitle\"\n        :default-active=\"sidebarActive\"\n        :menus=\"wrapperMenus(sidebarMenus)\"\n        :rounded=\"isMenuRounded\"\n        :theme=\"sidebarTheme\"\n        mode=\"vertical\"\n        @open=\"handleMenuOpen\"\n        @select=\"handleMenuSelect\"\n      />\n    </template>\n    <template #mixed-menu>\n      <LayoutMixedMenu\n        :active-path=\"extraActiveMenu\"\n        :menus=\"wrapperMenus(mixHeaderMenus, false)\"\n        :rounded=\"isMenuRounded\"\n        :theme=\"sidebarTheme\"\n        @default-select=\"handleDefaultSelect\"\n        @enter=\"handleMenuMouseEnter\"\n        @select=\"handleMixedMenuSelect\"\n      />\n    </template>\n    <!-- 侧边额外区域 -->\n    <template #side-extra>\n      <LayoutExtraMenu\n        :accordion=\"preferences.navigation.accordion\"\n        :collapse=\"preferences.sidebar.extraCollapse\"\n        :menus=\"wrapperMenus(extraMenus)\"\n        :rounded=\"isMenuRounded\"\n        :theme=\"sidebarTheme\"\n      />\n    </template>\n    <template #side-extra-title>\n      <VbenLogo\n        v-if=\"preferences.logo.enable\"\n        :fit=\"preferences.logo.fit\"\n        :text=\"preferences.app.name\"\n        :theme=\"theme\"\n      >\n        <template v-if=\"$slots['logo-text']\" #text>\n          <slot name=\"logo-text\"></slot>\n        </template>\n      </VbenLogo>\n    </template>\n\n    <template #tabbar>\n      <LayoutTabbar\n        v-if=\"preferences.tabbar.enable\"\n        :show-icon=\"preferences.tabbar.showIcon\"\n        :theme=\"theme\"\n      />\n    </template>\n\n    <!-- 主体内容 -->\n    <template #content>\n      <LayoutContent />\n    </template>\n\n    <template v-if=\"preferences.transition.loading\" #content-overlay>\n      <LayoutContentSpinner />\n    </template>\n\n    <!-- 页脚 -->\n    <template v-if=\"preferences.footer.enable\" #footer>\n      <LayoutFooter>\n        <Copyright\n          v-if=\"preferences.copyright.enable\"\n          v-bind=\"preferences.copyright\"\n        />\n      </LayoutFooter>\n    </template>\n\n    <template #extra>\n      <slot name=\"extra\"></slot>\n      <CheckUpdates\n        v-if=\"preferences.app.enableCheckUpdates\"\n        :check-updates-interval=\"preferences.app.checkUpdatesInterval\"\n      />\n\n      <Transition v-if=\"preferences.widget.lockScreen\" name=\"slide-up\">\n        <slot v-if=\"accessStore.isLockScreen\" name=\"lock-screen\"></slot>\n      </Transition>\n\n      <template v-if=\"preferencesButtonPosition.fixed\">\n        <Preferences\n          class=\"z-100 fixed bottom-20 right-0\"\n          @clear-preferences-and-logout=\"clearPreferencesAndLogout\"\n        />\n      </template>\n      <VbenBackTop />\n    </template>\n  </VbenAdminLayout>\n</template>\n"
  },
  {
    "path": "packages/effects/layouts/src/basic/menu/extra-menu.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { MenuRecordRaw } from '@vben/types';\n\nimport type { MenuProps } from '@vben-core/menu-ui';\n\nimport { useRoute } from 'vue-router';\n\nimport { Menu } from '@vben-core/menu-ui';\n\nimport { useNavigation } from './use-navigation';\n\ninterface Props extends MenuProps {\n  collapse?: boolean;\n  menus?: MenuRecordRaw[];\n}\n\nwithDefaults(defineProps<Props>(), {\n  accordion: true,\n  menus: () => [],\n});\n\nconst route = useRoute();\nconst { navigation } = useNavigation();\n\nasync function handleSelect(key: string) {\n  await navigation(key);\n}\n</script>\n\n<template>\n  <Menu\n    :accordion=\"accordion\"\n    :collapse=\"collapse\"\n    :default-active=\"route.meta?.activePath || route.path\"\n    :menus=\"menus\"\n    :rounded=\"rounded\"\n    :theme=\"theme\"\n    mode=\"vertical\"\n    @select=\"handleSelect\"\n  />\n</template>\n"
  },
  {
    "path": "packages/effects/layouts/src/basic/menu/index.ts",
    "content": "export { default as LayoutExtraMenu } from './extra-menu.vue';\nexport { default as LayoutMenu } from './menu.vue';\nexport { default as LayoutMixedMenu } from './mixed-menu.vue';\nexport * from './use-extra-menu';\nexport * from './use-mixed-menu';\n"
  },
  {
    "path": "packages/effects/layouts/src/basic/menu/menu.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { MenuRecordRaw } from '@vben/types';\n\nimport type { MenuProps } from '@vben-core/menu-ui';\n\nimport { Menu } from '@vben-core/menu-ui';\n\ninterface Props extends MenuProps {\n  menus?: MenuRecordRaw[];\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n  accordion: true,\n  menus: () => [],\n});\n\nconst emit = defineEmits<{\n  open: [string, string[]];\n  select: [string, string?];\n}>();\n\nfunction handleMenuSelect(key: string) {\n  emit('select', key, props.mode);\n}\n\nfunction handleMenuOpen(key: string, path: string[]) {\n  emit('open', key, path);\n}\n</script>\n\n<template>\n  <Menu\n    :accordion=\"accordion\"\n    :collapse=\"collapse\"\n    :collapse-show-title=\"collapseShowTitle\"\n    :default-active=\"defaultActive\"\n    :menus=\"menus\"\n    :mode=\"mode\"\n    :rounded=\"rounded\"\n    scroll-to-active\n    :theme=\"theme\"\n    @open=\"handleMenuOpen\"\n    @select=\"handleMenuSelect\"\n  />\n</template>\n"
  },
  {
    "path": "packages/effects/layouts/src/basic/menu/mixed-menu.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { MenuRecordRaw } from '@vben/types';\nimport type { NormalMenuProps } from '@vben-core/menu-ui';\n\nimport { findMenuByPath } from '@vben/utils';\nimport { NormalMenu } from '@vben-core/menu-ui';\nimport { onBeforeMount } from 'vue';\nimport { useRoute } from 'vue-router';\n\ninterface Props extends NormalMenuProps {}\n\nconst props = defineProps<Props>();\n\nconst emit = defineEmits<{\n  defaultSelect: [MenuRecordRaw, MenuRecordRaw?];\n  enter: [MenuRecordRaw];\n  select: [MenuRecordRaw];\n}>();\n\nconst route = useRoute();\n\nonBeforeMount(() => {\n  const menu = findMenuByPath(props.menus || [], route.path);\n  if (menu) {\n    const rootMenu = (props.menus || []).find(\n      (item) => item.path === menu.parents?.[0],\n    );\n    emit('defaultSelect', menu, rootMenu);\n  }\n});\n</script>\n\n<template>\n  <NormalMenu\n    :active-path=\"activePath\"\n    :collapse=\"collapse\"\n    :menus=\"menus\"\n    :rounded=\"rounded\"\n    :theme=\"theme\"\n    @enter=\"(menu) => emit('enter', menu)\"\n    @select=\"(menu) => emit('select', menu)\"\n  />\n</template>\n"
  },
  {
    "path": "packages/effects/layouts/src/basic/menu/use-extra-menu.ts",
    "content": "import type { ComputedRef } from 'vue';\n\nimport type { MenuRecordRaw } from '@vben/types';\n\nimport { computed, ref, watch } from 'vue';\nimport { useRoute } from 'vue-router';\n\nimport { preferences } from '@vben/preferences';\nimport { useAccessStore } from '@vben/stores';\nimport { findRootMenuByPath } from '@vben/utils';\n\nimport { useNavigation } from './use-navigation';\n\nfunction useExtraMenu(useRootMenus?: ComputedRef<MenuRecordRaw[]>) {\n  const accessStore = useAccessStore();\n  const { navigation, willOpenedByWindow } = useNavigation();\n\n  const menus = computed(() => useRootMenus?.value ?? accessStore.accessMenus);\n\n  /** 记录当前顶级菜单下哪个子菜单最后激活 */\n  const defaultSubMap = new Map<string, string>();\n  const extraRootMenus = ref<MenuRecordRaw[]>([]);\n  const route = useRoute();\n  const extraMenus = ref<MenuRecordRaw[]>([]);\n  const sidebarExtraVisible = ref<boolean>(false);\n  const extraActiveMenu = ref('');\n  const parentLevel = computed(() =>\n    preferences.app.layout === 'header-mixed-nav' ? 1 : 0,\n  );\n\n  /**\n   * 选择混合菜单事件\n   * @param menu\n   */\n  const handleMixedMenuSelect = async (menu: MenuRecordRaw) => {\n    const _extraMenus = menu?.children ?? [];\n    const hasChildren = _extraMenus.length > 0;\n\n    if (!willOpenedByWindow(menu.path)) {\n      extraMenus.value = _extraMenus ?? [];\n      extraActiveMenu.value = menu.parents?.[parentLevel.value] ?? menu.path;\n      sidebarExtraVisible.value = hasChildren;\n    }\n\n    if (!hasChildren) {\n      await navigation(menu.path);\n    } else if (preferences.sidebar.autoActivateChild) {\n      await navigation(\n        defaultSubMap.has(menu.path)\n          ? (defaultSubMap.get(menu.path) as string)\n          : menu.path,\n      );\n    }\n  };\n\n  /**\n   * 选择默认菜单事件\n   * @param menu\n   * @param rootMenu\n   */\n  const handleDefaultSelect = async (\n    menu: MenuRecordRaw,\n    rootMenu?: MenuRecordRaw,\n  ) => {\n    extraMenus.value = rootMenu?.children ?? extraRootMenus.value ?? [];\n    extraActiveMenu.value = menu.parents?.[parentLevel.value] ?? menu.path;\n\n    if (preferences.sidebar.expandOnHover) {\n      sidebarExtraVisible.value = extraMenus.value.length > 0;\n    }\n  };\n\n  /**\n   * 侧边菜单鼠标移出事件\n   */\n  const handleSideMouseLeave = () => {\n    if (preferences.sidebar.expandOnHover) {\n      return;\n    }\n\n    const { findMenu, rootMenu, rootMenuPath } = findRootMenuByPath(\n      menus.value,\n      route.path,\n    );\n    extraActiveMenu.value = rootMenuPath ?? findMenu?.path ?? '';\n    extraMenus.value = rootMenu?.children ?? [];\n  };\n\n  const handleMenuMouseEnter = (menu: MenuRecordRaw) => {\n    if (!preferences.sidebar.expandOnHover) {\n      const { findMenu } = findRootMenuByPath(menus.value, menu.path);\n      extraMenus.value = findMenu?.children ?? [];\n      extraActiveMenu.value = menu.parents?.[parentLevel.value] ?? menu.path;\n      sidebarExtraVisible.value = extraMenus.value.length > 0;\n    }\n  };\n\n  function calcExtraMenus(path: string) {\n    const currentPath = route.meta?.activePath || path;\n    const { findMenu, rootMenu, rootMenuPath } = findRootMenuByPath(\n      menus.value,\n      currentPath,\n      parentLevel.value,\n    );\n    extraRootMenus.value = rootMenu?.children ?? [];\n    if (rootMenuPath) defaultSubMap.set(rootMenuPath, currentPath);\n    extraActiveMenu.value = rootMenuPath ?? findMenu?.path ?? '';\n    extraMenus.value = rootMenu?.children ?? [];\n    if (preferences.sidebar.expandOnHover) {\n      sidebarExtraVisible.value = extraMenus.value.length > 0;\n    }\n  }\n\n  watch(\n    () => [route.path, preferences.app.layout],\n    ([path]) => {\n      calcExtraMenus(path || '');\n    },\n    { immediate: true },\n  );\n\n  return {\n    extraActiveMenu,\n    extraMenus,\n    handleDefaultSelect,\n    handleMenuMouseEnter,\n    handleMixedMenuSelect,\n    handleSideMouseLeave,\n    sidebarExtraVisible,\n  };\n}\n\nexport { useExtraMenu };\n"
  },
  {
    "path": "packages/effects/layouts/src/basic/menu/use-mixed-menu.ts",
    "content": "import type { MenuRecordRaw } from '@vben/types';\n\nimport { computed, onBeforeMount, ref, watch } from 'vue';\nimport { useRoute } from 'vue-router';\n\nimport { preferences, usePreferences } from '@vben/preferences';\nimport { useAccessStore } from '@vben/stores';\nimport { findRootMenuByPath } from '@vben/utils';\n\nimport { useNavigation } from './use-navigation';\n\nfunction useMixedMenu() {\n  const { navigation, willOpenedByWindow } = useNavigation();\n  const accessStore = useAccessStore();\n  const route = useRoute();\n  const splitSideMenus = ref<MenuRecordRaw[]>([]);\n  const rootMenuPath = ref<string>('');\n  const mixedRootMenuPath = ref<string>('');\n  const mixExtraMenus = ref<MenuRecordRaw[]>([]);\n  /** 记录当前顶级菜单下哪个子菜单最后激活 */\n  const defaultSubMap = new Map<string, string>();\n  const { isMixedNav, isHeaderMixedNav } = usePreferences();\n\n  const needSplit = computed(\n    () =>\n      (preferences.navigation.split && isMixedNav.value) ||\n      isHeaderMixedNav.value,\n  );\n\n  const sidebarVisible = computed(() => {\n    const enableSidebar = preferences.sidebar.enable;\n    if (needSplit.value) {\n      return enableSidebar && splitSideMenus.value.length > 0;\n    }\n    return enableSidebar;\n  });\n  const menus = computed(() => accessStore.accessMenus);\n\n  /**\n   * 头部菜单\n   */\n  const headerMenus = computed(() => {\n    if (!needSplit.value) {\n      return menus.value;\n    }\n    return menus.value.map((item) => {\n      return {\n        ...item,\n        children: [],\n      };\n    });\n  });\n\n  /**\n   * 侧边菜单\n   */\n  const sidebarMenus = computed(() => {\n    return needSplit.value ? splitSideMenus.value : menus.value;\n  });\n\n  const mixHeaderMenus = computed(() => {\n    return isHeaderMixedNav.value ? sidebarMenus.value : headerMenus.value;\n  });\n\n  /**\n   * 侧边菜单激活路径\n   */\n  const sidebarActive = computed(() => {\n    return (route?.meta?.activePath as string) ?? route.path;\n  });\n\n  /**\n   * 头部菜单激活路径\n   */\n  const headerActive = computed(() => {\n    if (!needSplit.value) {\n      return route.meta?.activePath ?? route.path;\n    }\n    return rootMenuPath.value;\n  });\n\n  /**\n   * 菜单点击事件处理\n   * @param key 菜单路径\n   * @param mode 菜单模式\n   */\n  const handleMenuSelect = (key: string, mode?: string) => {\n    if (!needSplit.value || mode === 'vertical') {\n      navigation(key);\n      return;\n    }\n    const rootMenu = menus.value.find((item) => item.path === key);\n    const _splitSideMenus = rootMenu?.children ?? [];\n\n    if (!willOpenedByWindow(key)) {\n      rootMenuPath.value = rootMenu?.path ?? '';\n      splitSideMenus.value = _splitSideMenus;\n    }\n\n    if (_splitSideMenus.length === 0) {\n      navigation(key);\n    } else if (rootMenu && preferences.sidebar.autoActivateChild) {\n      navigation(\n        defaultSubMap.has(rootMenu.path)\n          ? (defaultSubMap.get(rootMenu.path) as string)\n          : rootMenu.path,\n      );\n    }\n  };\n\n  /**\n   * 侧边菜单展开事件\n   * @param key 路由路径\n   * @param parentsPath 父级路径\n   */\n  const handleMenuOpen = (key: string, parentsPath: string[]) => {\n    if (parentsPath.length <= 1 && preferences.sidebar.autoActivateChild) {\n      navigation(\n        defaultSubMap.has(key) ? (defaultSubMap.get(key) as string) : key,\n      );\n    }\n  };\n\n  /**\n   * 计算侧边菜单\n   * @param path 路由路径\n   */\n  function calcSideMenus(path: string = route.path) {\n    let { rootMenu } = findRootMenuByPath(menus.value, path);\n    if (!rootMenu) {\n      rootMenu = menus.value.find((item) => item.path === path);\n    }\n    const result = findRootMenuByPath(rootMenu?.children || [], path, 1);\n    mixedRootMenuPath.value = result.rootMenuPath ?? '';\n    mixExtraMenus.value = result.rootMenu?.children ?? [];\n    rootMenuPath.value = rootMenu?.path ?? '';\n    splitSideMenus.value = rootMenu?.children ?? [];\n  }\n\n  watch(\n    () => route.path,\n    (path) => {\n      const currentPath = route?.meta?.activePath ?? route?.meta?.link ?? path;\n      if (willOpenedByWindow(currentPath)) {\n        return;\n      }\n      calcSideMenus(currentPath);\n      if (rootMenuPath.value)\n        defaultSubMap.set(rootMenuPath.value, currentPath);\n    },\n    { immediate: true },\n  );\n\n  // 初始化计算侧边菜单\n  onBeforeMount(() => {\n    calcSideMenus(route.meta?.activePath || route.path);\n  });\n\n  return {\n    handleMenuSelect,\n    handleMenuOpen,\n    headerActive,\n    headerMenus,\n    sidebarActive,\n    sidebarMenus,\n    mixHeaderMenus,\n    mixExtraMenus,\n    sidebarVisible,\n  };\n}\n\nexport { useMixedMenu };\n"
  },
  {
    "path": "packages/effects/layouts/src/basic/menu/use-navigation.ts",
    "content": "import type { RouteRecordNormalized } from 'vue-router';\n\nimport { useRouter } from 'vue-router';\n\nimport { isHttpUrl, openRouteInNewWindow, openWindow } from '@vben/utils';\n\nfunction useNavigation() {\n  const router = useRouter();\n  const routeMetaMap = new Map<string, RouteRecordNormalized>();\n\n  // 初始化路由映射\n  const initRouteMetaMap = () => {\n    const routes = router.getRoutes();\n    routes.forEach((route) => {\n      routeMetaMap.set(route.path, route);\n    });\n  };\n\n  initRouteMetaMap();\n\n  // 监听路由变化\n  router.afterEach(() => {\n    initRouteMetaMap();\n  });\n\n  // 检查是否应该在新窗口打开\n  const shouldOpenInNewWindow = (path: string): boolean => {\n    if (isHttpUrl(path)) {\n      return true;\n    }\n    const route = routeMetaMap.get(path);\n    // 如果有外链或者设置了在新窗口打开，返回 true\n    return !!(route?.meta?.link || route?.meta?.openInNewWindow);\n  };\n\n  const resolveHref = (path: string): string => {\n    return router.resolve(path).href;\n  };\n\n  const navigation = async (path: string) => {\n    try {\n      const route = routeMetaMap.get(path);\n      const { openInNewWindow = false, query = {}, link } = route?.meta ?? {};\n\n      // 检查是否有外链\n      if (link && typeof link === 'string') {\n        openWindow(link, { target: '_blank' });\n        return;\n      }\n\n      if (isHttpUrl(path)) {\n        openWindow(path, { target: '_blank' });\n      } else if (openInNewWindow) {\n        openRouteInNewWindow(resolveHref(path));\n      } else {\n        await router.push({\n          path,\n          query,\n        });\n      }\n    } catch (error) {\n      console.error('Navigation failed:', error);\n      throw error;\n    }\n  };\n\n  const willOpenedByWindow = (path: string) => {\n    return shouldOpenInNewWindow(path);\n  };\n\n  return { navigation, willOpenedByWindow };\n}\n\nexport { useNavigation };\n"
  },
  {
    "path": "packages/effects/layouts/src/basic/tabbar/index.ts",
    "content": "export { default as LayoutTabbar } from './tabbar.vue';\nexport * from './use-tabbar';\n"
  },
  {
    "path": "packages/effects/layouts/src/basic/tabbar/tabbar.vue",
    "content": "<script lang=\"ts\" setup>\nimport { computed } from 'vue';\nimport { useRoute } from 'vue-router';\n\nimport { useContentMaximize, useTabs } from '@vben/hooks';\nimport { preferences } from '@vben/preferences';\nimport { useTabbarStore } from '@vben/stores';\n\nimport { TabsToolMore, TabsToolScreen, TabsView } from '@vben-core/tabs-ui';\n\nimport { useTabbar } from './use-tabbar';\n\ndefineOptions({\n  name: 'LayoutTabbar',\n});\n\ndefineProps<{ showIcon?: boolean; theme?: string }>();\n\nconst route = useRoute();\nconst tabbarStore = useTabbarStore();\nconst { contentIsMaximize, toggleMaximize } = useContentMaximize();\nconst { unpinTab } = useTabs();\n\nconst {\n  createContextMenus,\n  currentActive,\n  currentTabs,\n  handleClick,\n  handleClose,\n} = useTabbar();\n\nconst menus = computed(() => {\n  const tab = tabbarStore.getTabByKey(currentActive.value);\n  const menus = createContextMenus(tab);\n  return menus.map((item) => {\n    return {\n      ...item,\n      label: item.text,\n      value: item.key,\n    };\n  });\n});\n\n// 刷新后如果不保持tab状态，关闭其他tab\nif (!preferences.tabbar.persist) {\n  tabbarStore.closeOtherTabs(route);\n}\n</script>\n\n<template>\n  <TabsView\n    :active=\"currentActive\"\n    :class=\"theme\"\n    :context-menus=\"createContextMenus\"\n    :draggable=\"preferences.tabbar.draggable\"\n    :show-icon=\"showIcon\"\n    :style-type=\"preferences.tabbar.styleType\"\n    :tabs=\"currentTabs\"\n    :wheelable=\"preferences.tabbar.wheelable\"\n    :middle-click-to-close=\"preferences.tabbar.middleClickToClose\"\n    @close=\"handleClose\"\n    @sort-tabs=\"tabbarStore.sortTabs\"\n    @unpin=\"unpinTab\"\n    @update:active=\"handleClick\"\n  />\n  <div class=\"flex-center h-full\">\n    <TabsToolMore v-if=\"preferences.tabbar.showMore\" :menus=\"menus\" />\n    <TabsToolScreen\n      v-if=\"preferences.tabbar.showMaximize\"\n      :screen=\"contentIsMaximize\"\n      @change=\"toggleMaximize\"\n      @update:screen=\"toggleMaximize\"\n    />\n  </div>\n</template>\n"
  },
  {
    "path": "packages/effects/layouts/src/basic/tabbar/use-tabbar.ts",
    "content": "import type { RouteLocationNormalizedGeneric } from 'vue-router';\n\nimport type { TabDefinition } from '@vben/types';\n\nimport type { IContextMenuItem } from '@vben-core/tabs-ui';\n\nimport { computed, ref, watch } from 'vue';\nimport { useRoute, useRouter } from 'vue-router';\n\nimport { useContentMaximize, useTabs } from '@vben/hooks';\nimport {\n  ArrowLeftToLine,\n  ArrowRightLeft,\n  ArrowRightToLine,\n  ExternalLink,\n  FoldHorizontal,\n  Fullscreen,\n  Minimize2,\n  Pin,\n  PinOff,\n  RotateCw,\n  X,\n} from '@vben/icons';\nimport { $t, useI18n } from '@vben/locales';\nimport { getTabKey, useAccessStore, useTabbarStore } from '@vben/stores';\nimport { filterTree } from '@vben/utils';\n\nexport function useTabbar() {\n  const router = useRouter();\n  const route = useRoute();\n  const accessStore = useAccessStore();\n  const tabbarStore = useTabbarStore();\n  const { contentIsMaximize, toggleMaximize } = useContentMaximize();\n  const {\n    closeAllTabs,\n    closeCurrentTab,\n    closeLeftTabs,\n    closeOtherTabs,\n    closeRightTabs,\n    closeTabByKey,\n    getTabDisableState,\n    openTabInNewWindow,\n    refreshTab,\n    toggleTabPin,\n  } = useTabs();\n\n  /**\n   * 当前路径对应的tab的key\n   */\n  const currentActive = computed(() => {\n    return getTabKey(route);\n  });\n\n  const { locale } = useI18n();\n  const currentTabs = ref<RouteLocationNormalizedGeneric[]>();\n  watch(\n    [\n      () => tabbarStore.getTabs,\n      () => tabbarStore.updateTime,\n      () => locale.value,\n    ],\n    ([tabs]) => {\n      currentTabs.value = tabs.map((item) => wrapperTabLocale(item));\n    },\n  );\n\n  /**\n   * 初始化固定标签页\n   */\n  const initAffixTabs = () => {\n    const affixTabs = filterTree(router.getRoutes(), (route) => {\n      return !!route.meta?.affixTab;\n    });\n    tabbarStore.setAffixTabs(affixTabs);\n  };\n\n  // 点击tab,跳转路由\n  const handleClick = (key: string) => {\n    const { fullPath, path } = tabbarStore.getTabByKey(key);\n    router.push(fullPath || path);\n  };\n\n  // 关闭tab\n  const handleClose = async (key: string) => {\n    await closeTabByKey(key);\n  };\n\n  function wrapperTabLocale(tab: RouteLocationNormalizedGeneric) {\n    return {\n      ...tab,\n      meta: {\n        ...tab?.meta,\n        title: $t(tab?.meta?.title as string),\n      },\n    };\n  }\n\n  watch(\n    () => accessStore.accessMenus,\n    () => {\n      initAffixTabs();\n    },\n    { immediate: true },\n  );\n\n  watch(\n    () => route.fullPath,\n    () => {\n      const meta = route.matched?.[route.matched.length - 1]?.meta;\n      tabbarStore.addTab({\n        ...route,\n        meta: meta || route.meta,\n      });\n    },\n    { immediate: true },\n  );\n\n  const createContextMenus = (tab: TabDefinition) => {\n    const {\n      disabledCloseAll,\n      disabledCloseCurrent,\n      disabledCloseLeft,\n      disabledCloseOther,\n      disabledCloseRight,\n      disabledRefresh,\n    } = getTabDisableState(tab);\n\n    const affixTab = tab?.meta?.affixTab ?? false;\n\n    const menus: IContextMenuItem[] = [\n      {\n        disabled: disabledCloseCurrent,\n        handler: async () => {\n          await closeCurrentTab(tab);\n        },\n        icon: X,\n        key: 'close',\n        text: $t('preferences.tabbar.contextMenu.close'),\n      },\n      {\n        handler: async () => {\n          await toggleTabPin(tab);\n        },\n        icon: affixTab ? PinOff : Pin,\n        key: 'affix',\n        text: affixTab\n          ? $t('preferences.tabbar.contextMenu.unpin')\n          : $t('preferences.tabbar.contextMenu.pin'),\n      },\n      {\n        handler: async () => {\n          if (!contentIsMaximize.value) {\n            await router.push(tab.fullPath);\n          }\n          toggleMaximize();\n        },\n        icon: contentIsMaximize.value ? Minimize2 : Fullscreen,\n        key: contentIsMaximize.value ? 'restore-maximize' : 'maximize',\n        text: contentIsMaximize.value\n          ? $t('preferences.tabbar.contextMenu.restoreMaximize')\n          : $t('preferences.tabbar.contextMenu.maximize'),\n      },\n      {\n        disabled: disabledRefresh,\n        handler: () => refreshTab(),\n        icon: RotateCw,\n        key: 'reload',\n        text: $t('preferences.tabbar.contextMenu.reload'),\n      },\n      {\n        handler: async () => {\n          await openTabInNewWindow(tab);\n        },\n        icon: ExternalLink,\n        key: 'open-in-new-window',\n        separator: true,\n        text: $t('preferences.tabbar.contextMenu.openInNewWindow'),\n      },\n\n      {\n        disabled: disabledCloseLeft,\n        handler: async () => {\n          await closeLeftTabs(tab);\n        },\n        icon: ArrowLeftToLine,\n        key: 'close-left',\n        text: $t('preferences.tabbar.contextMenu.closeLeft'),\n      },\n      {\n        disabled: disabledCloseRight,\n        handler: async () => {\n          await closeRightTabs(tab);\n        },\n        icon: ArrowRightToLine,\n        key: 'close-right',\n        separator: true,\n        text: $t('preferences.tabbar.contextMenu.closeRight'),\n      },\n      {\n        disabled: disabledCloseOther,\n        handler: async () => {\n          await closeOtherTabs(tab);\n        },\n        icon: FoldHorizontal,\n        key: 'close-other',\n        text: $t('preferences.tabbar.contextMenu.closeOther'),\n      },\n      {\n        disabled: disabledCloseAll,\n        handler: closeAllTabs,\n        icon: ArrowRightLeft,\n        key: 'close-all',\n        text: $t('preferences.tabbar.contextMenu.closeAll'),\n      },\n    ];\n\n    return menus.filter((item) => tabbarStore.getMenuList.includes(item.key));\n  };\n\n  return {\n    createContextMenus,\n    currentActive,\n    currentTabs,\n    handleClick,\n    handleClose,\n  };\n}\n"
  },
  {
    "path": "packages/effects/layouts/src/iframe/iframe-router-view.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { RouteLocationNormalized } from 'vue-router';\n\nimport { preferences } from '@vben/preferences';\nimport { useTabbarStore } from '@vben/stores';\nimport { VbenSpinner } from '@vben-core/shadcn-ui';\nimport { computed, ref } from 'vue';\nimport { useRoute } from 'vue-router';\n\ndefineOptions({ name: 'IFrameRouterView' });\n\nconst spinningList = ref<boolean[]>([]);\nconst tabbarStore = useTabbarStore();\nconst route = useRoute();\n\nconst enableTabbar = computed(() => preferences.tabbar.enable);\n\nconst iframeRoutes = computed(() => {\n  if (!enableTabbar.value) {\n    return route.meta.iframeSrc ? [route] : [];\n  }\n  return tabbarStore.getTabs.filter((tab) => !!tab.meta?.iframeSrc);\n});\n\nconst tabNames = computed(\n  () => new Set(iframeRoutes.value.map((item) => item.name as string)),\n);\n\nconst showIframe = computed(() => iframeRoutes.value.length > 0);\n\nfunction routeShow(tabItem: RouteLocationNormalized) {\n  return tabItem.name === route.name;\n}\n\nfunction canRender(tabItem: RouteLocationNormalized) {\n  const { meta, name } = tabItem;\n\n  if (!name || !tabbarStore.renderRouteView) {\n    return false;\n  }\n\n  if (!enableTabbar.value) {\n    return routeShow(tabItem);\n  }\n\n  // 跟随 keepAlive 状态,与其他tab页保持一致\n  if (\n    !meta?.keepAlive &&\n    tabNames.value.has(name as string) &&\n    name !== route.name\n  ) {\n    return false;\n  }\n  return tabbarStore.getTabs.some((tab) => tab.name === name);\n}\n\nfunction hideLoading(index: number) {\n  spinningList.value[index] = false;\n}\n\nfunction showSpinning(index: number) {\n  const curSpinning = spinningList.value[index];\n  // 首次加载时显示loading\n  return curSpinning === undefined ? true : curSpinning;\n}\n</script>\n<template>\n  <template v-if=\"showIframe\">\n    <template v-for=\"(item, index) in iframeRoutes\" :key=\"item.fullPath\">\n      <div\n        v-if=\"canRender(item)\"\n        v-show=\"routeShow(item)\"\n        class=\"relative size-full\"\n      >\n        <VbenSpinner :spinning=\"showSpinning(index)\" />\n        <iframe\n          :src=\"item.meta.iframeSrc as string\"\n          class=\"size-full\"\n          @load=\"hideLoading(index)\"\n        ></iframe>\n      </div>\n    </template>\n  </template>\n</template>\n"
  },
  {
    "path": "packages/effects/layouts/src/iframe/iframe-view.vue",
    "content": "<template>\n  <div></div>\n</template>\n"
  },
  {
    "path": "packages/effects/layouts/src/iframe/index.ts",
    "content": "export { default as IFrameRouterView } from './iframe-router-view.vue';\nexport { default as IFrameView } from './iframe-view.vue';\n"
  },
  {
    "path": "packages/effects/layouts/src/index.ts",
    "content": "export * from './authentication';\nexport * from './basic';\nexport * from './iframe';\nexport * from './widgets';\n"
  },
  {
    "path": "packages/effects/layouts/src/widgets/breadcrumb.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { BreadcrumbStyleType } from '@vben/types';\nimport type { IBreadcrumb } from '@vben-core/shadcn-ui';\n\nimport { $t } from '@vben/locales';\nimport { VbenBreadcrumbView } from '@vben-core/shadcn-ui';\nimport { computed } from 'vue';\nimport { useRoute, useRouter } from 'vue-router';\n\ninterface Props {\n  hideWhenOnlyOne?: boolean;\n  showHome?: boolean;\n  showIcon?: boolean;\n  type?: BreadcrumbStyleType;\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n  showHome: false,\n  showIcon: false,\n  type: 'normal',\n});\n\nconst route = useRoute();\nconst router = useRouter();\n\nconst breadcrumbs = computed((): IBreadcrumb[] => {\n  const matched = route.matched;\n\n  const resultBreadcrumb: IBreadcrumb[] = [];\n\n  for (const match of matched) {\n    const { meta, path } = match;\n    const { hideChildrenInMenu, hideInBreadcrumb, icon, name, title } =\n      meta || {};\n    if (hideInBreadcrumb || hideChildrenInMenu || !path) {\n      continue;\n    }\n\n    resultBreadcrumb.push({\n      icon,\n      path: path || route.path,\n      title: title ? $t((title || name) as string) : '',\n    });\n  }\n  if (props.showHome) {\n    resultBreadcrumb.unshift({\n      icon: 'mdi:home-outline',\n      isHome: true,\n      path: '/',\n    });\n  }\n  if (props.hideWhenOnlyOne && resultBreadcrumb.length === 1) {\n    return [];\n  }\n\n  return resultBreadcrumb;\n});\n\nfunction handleSelect(path: string) {\n  router.push(path);\n}\n</script>\n<template>\n  <VbenBreadcrumbView\n    :breadcrumbs=\"breadcrumbs\"\n    :show-icon=\"showIcon\"\n    :style-type=\"type\"\n    class=\"ml-2\"\n    @select=\"handleSelect\"\n  />\n</template>\n"
  },
  {
    "path": "packages/effects/layouts/src/widgets/check-updates/check-updates.vue",
    "content": "<script setup lang=\"ts\">\nimport { $t } from '@vben/locales';\nimport { useVbenModal } from '@vben-core/popup-ui';\nimport { onMounted, onUnmounted, ref } from 'vue';\n\ninterface Props {\n  // 轮询时间，分钟\n  checkUpdatesInterval?: number;\n  // 检查更新的地址\n  checkUpdateUrl?: string;\n}\n\ndefineOptions({ name: 'CheckUpdates' });\n\nconst props = withDefaults(defineProps<Props>(), {\n  checkUpdatesInterval: 1,\n  checkUpdateUrl: import.meta.env.BASE_URL || '/',\n});\n\nlet isCheckingUpdates = false;\nconst currentVersionTag = ref('');\nconst lastVersionTag = ref('');\nconst timer = ref<ReturnType<typeof setInterval>>();\n\nconst [UpdateNoticeModal, modalApi] = useVbenModal({\n  closable: false,\n  closeOnPressEscape: false,\n  closeOnClickModal: false,\n  onConfirm() {\n    lastVersionTag.value = currentVersionTag.value;\n    window.location.reload();\n    // handleSubmitLogout();\n  },\n});\n\nasync function getVersionTag() {\n  try {\n    if (\n      location.hostname === 'localhost' ||\n      location.hostname === '127.0.0.1'\n    ) {\n      return null;\n    }\n    const response = await fetch(props.checkUpdateUrl, {\n      cache: 'no-cache',\n      method: 'HEAD',\n      redirect: 'manual',\n    });\n\n    return (\n      response.headers.get('etag') || response.headers.get('last-modified')\n    );\n  } catch {\n    console.error('Failed to fetch version tag');\n    return null;\n  }\n}\n\nasync function checkForUpdates() {\n  const versionTag = await getVersionTag();\n  if (!versionTag) {\n    return;\n  }\n\n  // 首次运行时不提示更新\n  if (!lastVersionTag.value) {\n    lastVersionTag.value = versionTag;\n    return;\n  }\n\n  if (lastVersionTag.value !== versionTag && versionTag) {\n    clearInterval(timer.value);\n    handleNotice(versionTag);\n  }\n}\nfunction handleNotice(versionTag: string) {\n  currentVersionTag.value = versionTag;\n  modalApi.open();\n}\n\nfunction start() {\n  if (props.checkUpdatesInterval <= 0) {\n    return;\n  }\n\n  // 每 checkUpdatesInterval(默认值为1) 分钟检查一次\n  timer.value = setInterval(\n    checkForUpdates,\n    props.checkUpdatesInterval * 60 * 1000,\n  );\n}\n\nfunction handleVisibilitychange() {\n  if (document.hidden) {\n    stop();\n  } else {\n    if (!isCheckingUpdates) {\n      isCheckingUpdates = true;\n      checkForUpdates().finally(() => {\n        isCheckingUpdates = false;\n        start();\n      });\n    }\n  }\n}\n\nfunction stop() {\n  clearInterval(timer.value);\n}\n\nonMounted(() => {\n  start();\n  document.addEventListener('visibilitychange', handleVisibilitychange);\n});\n\nonUnmounted(() => {\n  stop();\n  document.removeEventListener('visibilitychange', handleVisibilitychange);\n});\n</script>\n<template>\n  <UpdateNoticeModal\n    :cancel-text=\"$t('common.cancel')\"\n    :confirm-text=\"$t('common.refresh')\"\n    :fullscreen-button=\"false\"\n    :title=\"$t('ui.widgets.checkUpdatesTitle')\"\n    centered\n    content-class=\"px-8 min-h-10\"\n    footer-class=\"border-none mb-3 mr-3\"\n    header-class=\"border-none\"\n  >\n    {{ $t('ui.widgets.checkUpdatesDescription') }}\n  </UpdateNoticeModal>\n</template>\n"
  },
  {
    "path": "packages/effects/layouts/src/widgets/check-updates/index.ts",
    "content": "export { default as CheckUpdates } from './check-updates.vue';\n"
  },
  {
    "path": "packages/effects/layouts/src/widgets/color-toggle.vue",
    "content": "<script setup lang=\"ts\">\nimport type { BuiltinThemeType } from '@vben/types';\n\nimport { Palette } from '@vben/icons';\nimport {\n  COLOR_PRESETS,\n  preferences,\n  updatePreferences,\n} from '@vben/preferences';\nimport { VbenIconButton } from '@vben-core/shadcn-ui';\n\ndefineOptions({\n  name: 'AuthenticationColorToggle',\n});\n\nfunction handleUpdate(colorPrimary: string, type: BuiltinThemeType) {\n  updatePreferences({\n    theme: {\n      colorPrimary,\n      builtinType: type,\n    },\n  });\n}\n</script>\n\n<template>\n  <div class=\"group relative flex items-center overflow-hidden\">\n    <div\n      class=\"flex w-0 overflow-hidden transition-all duration-500 ease-out group-hover:w-60\"\n    >\n      <template v-for=\"preset in COLOR_PRESETS\" :key=\"preset.color\">\n        <VbenIconButton\n          class=\"flex-center flex-shrink-0\"\n          @click=\"handleUpdate(preset.color, preset.type)\"\n        >\n          <div\n            :style=\"{ backgroundColor: preset.color }\"\n            class=\"flex-center relative size-5 rounded-full hover:scale-110\"\n          >\n            <svg\n              v-if=\"preferences.theme.builtinType === preset.type\"\n              class=\"h-3.5 w-3.5 text-white\"\n              height=\"1em\"\n              viewBox=\"0 0 15 15\"\n              width=\"1em\"\n            >\n              <path\n                clip-rule=\"evenodd\"\n                d=\"M11.467 3.727c.289.189.37.576.181.865l-4.25 6.5a.625.625 0 0 1-.944.12l-2.75-2.5a.625.625 0 0 1 .841-.925l2.208 2.007l3.849-5.886a.625.625 0 0 1 .865-.181\"\n                fill=\"currentColor\"\n                fill-rule=\"evenodd\"\n              />\n            </svg>\n          </div>\n        </VbenIconButton>\n      </template>\n    </div>\n\n    <VbenIconButton>\n      <Palette class=\"text-primary size-4\" />\n    </VbenIconButton>\n  </div>\n</template>\n"
  },
  {
    "path": "packages/effects/layouts/src/widgets/global-search/global-search.vue",
    "content": "<script setup lang=\"ts\">\nimport type { MenuRecordRaw } from '@vben/types';\n\nimport { nextTick, onMounted, onUnmounted, ref, watch } from 'vue';\n\nimport {\n  ArrowDown,\n  ArrowUp,\n  CornerDownLeft,\n  MdiKeyboardEsc,\n  Search,\n} from '@vben/icons';\nimport { $t } from '@vben/locales';\nimport { isWindowsOs } from '@vben/utils';\n\nimport { useVbenModal } from '@vben-core/popup-ui';\n\nimport { useMagicKeys, whenever } from '@vueuse/core';\n\nimport SearchPanel from './search-panel.vue';\n\ndefineOptions({\n  name: 'GlobalSearch',\n});\n\nconst props = withDefaults(\n  defineProps<{ enableShortcutKey?: boolean; menus?: MenuRecordRaw[] }>(),\n  {\n    enableShortcutKey: true,\n    menus: () => [],\n  },\n);\n\nconst keyword = ref('');\nconst searchInputRef = ref<HTMLInputElement>();\n\nconst [Modal, modalApi] = useVbenModal({\n  onCancel() {\n    modalApi.close();\n  },\n  onOpenChange(isOpen: boolean) {\n    if (!isOpen) {\n      keyword.value = '';\n    }\n  },\n});\nconst open = modalApi.useStore((state) => state.isOpen);\n\nfunction handleClose() {\n  modalApi.close();\n  keyword.value = '';\n}\n\nconst keys = useMagicKeys();\nconst cmd = isWindowsOs() ? keys['ctrl+k'] : keys['cmd+k'];\nwhenever(cmd!, () => {\n  if (props.enableShortcutKey) {\n    modalApi.open();\n  }\n});\n\nwhenever(open, () => {\n  nextTick(() => {\n    searchInputRef.value?.focus();\n  });\n});\n\nconst preventDefaultBrowserSearchHotKey = (event: KeyboardEvent) => {\n  if (event.key?.toLowerCase() === 'k' && (event.metaKey || event.ctrlKey)) {\n    event.preventDefault();\n  }\n};\n\nconst toggleKeydownListener = () => {\n  if (props.enableShortcutKey) {\n    window.addEventListener('keydown', preventDefaultBrowserSearchHotKey);\n  } else {\n    window.removeEventListener('keydown', preventDefaultBrowserSearchHotKey);\n  }\n};\n\nconst toggleOpen = () => {\n  open.value ? modalApi.close() : modalApi.open();\n};\n\nwatch(() => props.enableShortcutKey, toggleKeydownListener);\n\nonMounted(() => {\n  toggleKeydownListener();\n\n  onUnmounted(() => {\n    window.removeEventListener('keydown', preventDefaultBrowserSearchHotKey);\n  });\n});\n</script>\n\n<template>\n  <div>\n    <Modal\n      :fullscreen-button=\"false\"\n      class=\"w-[600px]\"\n      header-class=\"py-2 border-b\"\n    >\n      <template #title>\n        <div class=\"flex items-center\">\n          <Search class=\"text-muted-foreground mr-2 size-4\" />\n          <input\n            ref=\"searchInputRef\"\n            v-model=\"keyword\"\n            :placeholder=\"$t('ui.widgets.search.searchNavigate')\"\n            class=\"ring-none placeholder:text-muted-foreground w-[80%] rounded-md border border-none bg-transparent p-2 pl-0 text-sm font-normal outline-none ring-0 ring-offset-transparent focus-visible:ring-transparent\"\n          />\n        </div>\n      </template>\n\n      <SearchPanel :keyword=\"keyword\" :menus=\"menus\" @close=\"handleClose\" />\n      <template #footer>\n        <div class=\"flex w-full justify-start text-xs\">\n          <div class=\"mr-2 flex items-center\">\n            <CornerDownLeft class=\"mr-1 size-3\" />\n            {{ $t('ui.widgets.search.select') }}\n          </div>\n          <div class=\"mr-2 flex items-center\">\n            <ArrowUp class=\"mr-1 size-3\" />\n            <ArrowDown class=\"mr-1 size-3\" />\n            {{ $t('ui.widgets.search.navigate') }}\n          </div>\n          <div class=\"flex items-center\">\n            <MdiKeyboardEsc class=\"mr-1 size-3\" />\n            {{ $t('ui.widgets.search.close') }}\n          </div>\n        </div>\n      </template>\n    </Modal>\n    <div\n      class=\"md:bg-accent group flex h-8 cursor-pointer items-center gap-3 rounded-2xl border-none bg-none px-2 py-0.5 outline-none\"\n      @click=\"toggleOpen()\"\n    >\n      <Search\n        class=\"text-muted-foreground group-hover:text-foreground size-4 group-hover:opacity-100\"\n      />\n      <span\n        class=\"text-muted-foreground group-hover:text-foreground hidden text-xs duration-300 md:block\"\n      >\n        {{ $t('ui.widgets.search.title') }}\n      </span>\n      <span\n        v-if=\"enableShortcutKey\"\n        class=\"bg-background border-foreground/60 text-muted-foreground group-hover:text-foreground relative hidden rounded-sm rounded-r-xl px-1.5 py-1 text-xs leading-none group-hover:opacity-100 md:block\"\n      >\n        {{ isWindowsOs() ? 'Ctrl' : '⌘' }}\n        <kbd>K</kbd>\n      </span>\n      <span v-else></span>\n    </div>\n  </div>\n</template>\n"
  },
  {
    "path": "packages/effects/layouts/src/widgets/global-search/index.ts",
    "content": "export { default as GlobalSearch } from './global-search.vue';\n"
  },
  {
    "path": "packages/effects/layouts/src/widgets/global-search/search-panel.vue",
    "content": "<script setup lang=\"ts\">\nimport type { MenuRecordRaw } from '@vben/types';\n\nimport { nextTick, onMounted, ref, shallowRef, watch } from 'vue';\nimport { useRouter } from 'vue-router';\n\nimport { SearchX, X } from '@vben/icons';\nimport { $t } from '@vben/locales';\nimport { mapTree, traverseTreeValues, uniqueByField } from '@vben/utils';\n\nimport { VbenIcon, VbenScrollbar } from '@vben-core/shadcn-ui';\nimport { isHttpUrl } from '@vben-core/shared/utils';\n\nimport { onKeyStroke, useLocalStorage, useThrottleFn } from '@vueuse/core';\n\ndefineOptions({\n  name: 'SearchPanel',\n});\n\nconst props = withDefaults(\n  defineProps<{ keyword?: string; menus?: MenuRecordRaw[] }>(),\n  {\n    keyword: '',\n    menus: () => [],\n  },\n);\nconst emit = defineEmits<{ close: [] }>();\n\nconst router = useRouter();\nconst searchHistory = useLocalStorage<MenuRecordRaw[]>(\n  `__search-history-${location.hostname}__`,\n  [],\n);\nconst activeIndex = ref(-1);\nconst searchItems = shallowRef<MenuRecordRaw[]>([]);\nconst searchResults = ref<MenuRecordRaw[]>([]);\n\nconst handleSearch = useThrottleFn(search, 200);\n\n// 搜索函数，用于根据搜索关键词查找匹配的菜单项\nfunction search(searchKey: string) {\n  // 去除搜索关键词的前后空格\n  searchKey = searchKey.trim();\n\n  // 如果搜索关键词为空，清空搜索结果并返回\n  if (!searchKey) {\n    searchResults.value = [];\n    return;\n  }\n\n  // 使用搜索关键词创建正则表达式\n  const reg = createSearchReg(searchKey);\n\n  // 初始化结果数组\n  const results: MenuRecordRaw[] = [];\n\n  // 遍历搜索项\n  traverseTreeValues(searchItems.value, (item) => {\n    // 如果菜单项的名称匹配正则表达式，将其添加到结果数组中\n    if (reg.test(item.name?.toLowerCase())) {\n      results.push(item);\n    }\n  });\n\n  // 更新搜索结果\n  searchResults.value = results;\n\n  // 如果有搜索结果，设置索引为 0\n  if (results.length > 0) {\n    activeIndex.value = 0;\n  }\n\n  // 赋值索引为 0\n  activeIndex.value = 0;\n}\n\n// When the keyboard up and down keys move to an invisible place\n// the scroll bar needs to scroll automatically\nfunction scrollIntoView() {\n  const element = document.querySelector(\n    `[data-search-item=\"${activeIndex.value}\"]`,\n  );\n\n  if (element) {\n    element.scrollIntoView({ block: 'nearest' });\n  }\n}\n\n// enter keyboard event\nasync function handleEnter() {\n  if (searchResults.value.length === 0) {\n    return;\n  }\n  const result = searchResults.value;\n  const index = activeIndex.value;\n  if (result.length === 0 || index < 0) {\n    return;\n  }\n  const to = result[index];\n  if (to) {\n    searchHistory.value = uniqueByField([...searchHistory.value, to], 'path');\n    handleClose();\n    await nextTick();\n    if (isHttpUrl(to.path)) {\n      window.open(to.path, '_blank');\n    } else {\n      router.push({ path: to.path, replace: true });\n    }\n  }\n}\n\n// Arrow key up\nfunction handleUp() {\n  if (searchResults.value.length === 0) {\n    return;\n  }\n  activeIndex.value--;\n  if (activeIndex.value < 0) {\n    activeIndex.value = searchResults.value.length - 1;\n  }\n  scrollIntoView();\n}\n\n// Arrow key down\nfunction handleDown() {\n  if (searchResults.value.length === 0) {\n    return;\n  }\n  activeIndex.value++;\n  if (activeIndex.value > searchResults.value.length - 1) {\n    activeIndex.value = 0;\n  }\n  scrollIntoView();\n}\n\n// close search modal\nfunction handleClose() {\n  searchResults.value = [];\n  emit('close');\n}\n\n// Activate when the mouse moves to a certain line\nfunction handleMouseenter(e: MouseEvent) {\n  const index = (e.target as HTMLElement)?.dataset.index;\n  activeIndex.value = Number(index);\n}\n\nfunction removeItem(index: number) {\n  if (props.keyword) {\n    searchResults.value.splice(index, 1);\n  } else {\n    searchHistory.value.splice(index, 1);\n  }\n  activeIndex.value = Math.max(activeIndex.value - 1, 0);\n  scrollIntoView();\n}\n\n// 存储所有需要转义的特殊字符\nconst code = new Set([\n  '$',\n  '(',\n  ')',\n  '*',\n  '+',\n  '.',\n  '?',\n  '[',\n  '\\\\',\n  ']',\n  '^',\n  '{',\n  '|',\n  '}',\n]);\n\n// 转换函数，用于转义特殊字符\nfunction transform(c: string) {\n  // 如果字符在特殊字符列表中，返回转义后的字符\n  // 如果不在，返回字符本身\n  return code.has(c) ? `\\\\${c}` : c;\n}\n\n// 创建搜索正则表达式\nfunction createSearchReg(key: string) {\n  // 将输入的字符串拆分为单个字符\n  // 对每个字符进行转义\n  // 然后用'.*'连接所有字符，创建正则表达式\n  const keys = [...key].map((item) => transform(item)).join('.*');\n  // 返回创建的正则表达式\n  return new RegExp(`.*${keys}.*`);\n}\n\nwatch(\n  () => props.keyword,\n  (val) => {\n    if (val) {\n      handleSearch(val);\n    } else {\n      searchResults.value = [...searchHistory.value];\n    }\n  },\n);\n\nonMounted(() => {\n  searchItems.value = mapTree(props.menus, (item) => {\n    return {\n      ...item,\n      name: $t(item?.name),\n    };\n  });\n  if (searchHistory.value.length > 0) {\n    searchResults.value = searchHistory.value;\n  }\n  // enter search\n  onKeyStroke('Enter', handleEnter);\n  // Monitor keyboard arrow keys\n  onKeyStroke('ArrowUp', handleUp);\n  onKeyStroke('ArrowDown', handleDown);\n  // esc close\n  onKeyStroke('Escape', handleClose);\n});\n</script>\n\n<template>\n  <VbenScrollbar>\n    <div class=\"!flex h-full justify-center px-2 sm:max-h-[450px]\">\n      <!-- 无搜索结果 -->\n      <div\n        v-if=\"keyword && searchResults.length === 0\"\n        class=\"text-muted-foreground text-center\"\n      >\n        <SearchX class=\"mx-auto mt-4 size-12\" />\n        <p class=\"mb-10 mt-6 text-xs\">\n          {{ $t('ui.widgets.search.noResults') }}\n          <span class=\"text-foreground text-sm font-medium\">\n            \"{{ keyword }}\"\n          </span>\n        </p>\n      </div>\n      <!-- 历史搜索记录 & 没有搜索结果 -->\n      <div\n        v-if=\"!keyword && searchResults.length === 0\"\n        class=\"text-muted-foreground text-center\"\n      >\n        <p class=\"my-10 text-xs\">\n          {{ $t('ui.widgets.search.noRecent') }}\n        </p>\n      </div>\n\n      <ul v-show=\"searchResults.length > 0\" class=\"w-full\">\n        <li\n          v-if=\"searchHistory.length > 0 && !keyword\"\n          class=\"text-muted-foreground mb-2 text-xs\"\n        >\n          {{ $t('ui.widgets.search.recent') }}\n        </li>\n        <li\n          v-for=\"(item, index) in uniqueByField(searchResults, 'path')\"\n          :key=\"item.path\"\n          :class=\"\n            activeIndex === index\n              ? 'active bg-primary text-primary-foreground'\n              : ''\n          \"\n          :data-index=\"index\"\n          :data-search-item=\"index\"\n          class=\"bg-accent flex-center group mb-3 w-full cursor-pointer rounded-lg px-4 py-4\"\n          @click=\"handleEnter\"\n          @mouseenter=\"handleMouseenter\"\n        >\n          <VbenIcon\n            :icon=\"item.icon\"\n            class=\"mr-2 size-5 flex-shrink-0\"\n            fallback\n          />\n\n          <span class=\"flex-1\">{{ item.name }}</span>\n          <div\n            class=\"flex-center dark:hover:bg-accent hover:text-primary-foreground rounded-full p-1 hover:scale-110\"\n            @click.stop=\"removeItem(index)\"\n          >\n            <X class=\"size-4\" />\n          </div>\n        </li>\n      </ul>\n    </div>\n  </VbenScrollbar>\n</template>\n"
  },
  {
    "path": "packages/effects/layouts/src/widgets/index.ts",
    "content": "export { default as Breadcrumb } from './breadcrumb.vue';\nexport * from './check-updates';\nexport { default as AuthenticationColorToggle } from './color-toggle.vue';\nexport * from './global-search';\nexport { default as LanguageToggle } from './language-toggle.vue';\nexport { default as AuthenticationLayoutToggle } from './layout-toggle.vue';\nexport * from './lock-screen';\nexport * from './notification';\nexport * from './preferences';\nexport * from './theme-toggle';\nexport * from './user-dropdown';\n"
  },
  {
    "path": "packages/effects/layouts/src/widgets/language-toggle.vue",
    "content": "<script setup lang=\"ts\">\nimport type { SupportedLanguagesType } from '@vben/locales';\n\nimport { SUPPORT_LANGUAGES } from '@vben/constants';\nimport { Languages } from '@vben/icons';\nimport { loadLocaleMessages } from '@vben/locales';\nimport { preferences, updatePreferences } from '@vben/preferences';\nimport { VbenDropdownRadioMenu, VbenIconButton } from '@vben-core/shadcn-ui';\n\ndefineOptions({\n  name: 'LanguageToggle',\n});\n\nasync function handleUpdate(value: string | undefined) {\n  if (!value) return;\n  const locale = value as SupportedLanguagesType;\n  updatePreferences({\n    app: {\n      locale,\n    },\n  });\n  await loadLocaleMessages(locale);\n}\n</script>\n\n<template>\n  <div>\n    <VbenDropdownRadioMenu\n      :menus=\"SUPPORT_LANGUAGES\"\n      :model-value=\"preferences.app.locale\"\n      @update:model-value=\"handleUpdate\"\n    >\n      <VbenIconButton class=\"hover:animate-[shrink_0.3s_ease-in-out]\">\n        <Languages class=\"text-foreground size-4\" />\n      </VbenIconButton>\n    </VbenDropdownRadioMenu>\n  </div>\n</template>\n"
  },
  {
    "path": "packages/effects/layouts/src/widgets/layout-toggle.vue",
    "content": "<script setup lang=\"ts\">\nimport type { AuthPageLayoutType } from '@vben/types';\nimport type { VbenDropdownMenuItem } from '@vben-core/shadcn-ui';\n\nimport { InspectionPanel, PanelLeft, PanelRight } from '@vben/icons';\nimport { $t } from '@vben/locales';\nimport {\n  preferences,\n  updatePreferences,\n  usePreferences,\n} from '@vben/preferences';\nimport { VbenDropdownRadioMenu, VbenIconButton } from '@vben-core/shadcn-ui';\nimport { computed } from 'vue';\n\ndefineOptions({\n  name: 'AuthenticationLayoutToggle',\n});\n\nconst menus = computed((): VbenDropdownMenuItem[] => [\n  {\n    icon: PanelLeft,\n    label: $t('authentication.layout.alignLeft'),\n    value: 'panel-left',\n  },\n  {\n    icon: InspectionPanel,\n    label: $t('authentication.layout.center'),\n    value: 'panel-center',\n  },\n  {\n    icon: PanelRight,\n    label: $t('authentication.layout.alignRight'),\n    value: 'panel-right',\n  },\n]);\n\nconst { authPanelCenter, authPanelLeft, authPanelRight } = usePreferences();\n\nfunction handleUpdate(value: string | undefined) {\n  if (!value) return;\n  updatePreferences({\n    app: {\n      authPageLayout: value as AuthPageLayoutType,\n    },\n  });\n}\n</script>\n\n<template>\n  <VbenDropdownRadioMenu\n    :menus=\"menus\"\n    :model-value=\"preferences.app.authPageLayout\"\n    @update:model-value=\"handleUpdate\"\n  >\n    <VbenIconButton>\n      <PanelRight v-if=\"authPanelRight\" class=\"size-4\" />\n      <PanelLeft v-if=\"authPanelLeft\" class=\"size-4\" />\n      <InspectionPanel v-if=\"authPanelCenter\" class=\"size-4\" />\n    </VbenIconButton>\n  </VbenDropdownRadioMenu>\n</template>\n"
  },
  {
    "path": "packages/effects/layouts/src/widgets/lock-screen/index.ts",
    "content": "export { default as LockScreen } from './lock-screen.vue';\nexport { default as LockScreenModal } from './lock-screen-modal.vue';\n"
  },
  {
    "path": "packages/effects/layouts/src/widgets/lock-screen/lock-screen-modal.vue",
    "content": "<script setup lang=\"ts\">\nimport type { Recordable } from '@vben/types';\n\nimport { $t } from '@vben/locales';\nimport { useVbenForm, z } from '@vben-core/form-ui';\nimport { useVbenModal } from '@vben-core/popup-ui';\nimport { VbenAvatar, VbenButton } from '@vben-core/shadcn-ui';\nimport { computed, reactive } from 'vue';\n\ninterface Props {\n  avatar?: string;\n  text?: string;\n}\n\ndefineOptions({\n  name: 'LockScreenModal',\n});\n\nwithDefaults(defineProps<Props>(), {\n  avatar: '',\n  text: '',\n});\n\nconst emit = defineEmits<{\n  submit: [Recordable<any>];\n}>();\n\nconst [Form, { resetForm, validate, getValues }] = useVbenForm(\n  reactive({\n    commonConfig: {\n      hideLabel: true,\n      hideRequiredMark: true,\n    },\n    schema: computed(() => [\n      {\n        component: 'VbenInputPassword' as const,\n        componentProps: {\n          placeholder: $t('ui.widgets.lockScreen.placeholder'),\n        },\n        fieldName: 'lockScreenPassword',\n        formFieldProps: { validateOnBlur: false },\n        label: $t('authentication.password'),\n        rules: z\n          .string()\n          .min(1, { message: $t('ui.widgets.lockScreen.placeholder') }),\n      },\n    ]),\n    showDefaultActions: false,\n  }),\n);\n\nconst [Modal] = useVbenModal({\n  onConfirm() {\n    handleSubmit();\n  },\n  onOpenChange(isOpen) {\n    if (isOpen) {\n      resetForm();\n    }\n  },\n});\n\nasync function handleSubmit() {\n  const { valid } = await validate();\n  const values = await getValues();\n  if (valid) {\n    emit('submit', values?.lockScreenPassword);\n  }\n}\n</script>\n\n<template>\n  <Modal\n    :footer=\"false\"\n    :fullscreen-button=\"false\"\n    :title=\"$t('ui.widgets.lockScreen.title')\"\n  >\n    <div\n      class=\"mb-10 flex w-full flex-col items-center px-10\"\n      @keydown.enter.prevent=\"handleSubmit\"\n    >\n      <div class=\"w-full\">\n        <div class=\"ml-2 flex w-full flex-col items-center\">\n          <VbenAvatar\n            :src=\"avatar\"\n            class=\"size-20\"\n            dot-class=\"bottom-0 right-1 border-2 size-4 bg-green-500\"\n          />\n          <div class=\"text-foreground my-6 flex items-center font-medium\">\n            {{ text }}\n          </div>\n        </div>\n        <Form />\n        <VbenButton class=\"mt-1 w-full\" @click=\"handleSubmit\">\n          {{ $t('ui.widgets.lockScreen.screenButton') }}\n        </VbenButton>\n      </div>\n    </div>\n  </Modal>\n</template>\n"
  },
  {
    "path": "packages/effects/layouts/src/widgets/lock-screen/lock-screen.vue",
    "content": "<script setup lang=\"ts\">\nimport { computed, reactive, ref } from 'vue';\n\nimport { LockKeyhole } from '@vben/icons';\nimport { $t, useI18n } from '@vben/locales';\nimport { storeToRefs, useAccessStore } from '@vben/stores';\n\nimport { useScrollLock } from '@vben-core/composables';\nimport { useVbenForm, z } from '@vben-core/form-ui';\nimport { VbenAvatar, VbenButton } from '@vben-core/shadcn-ui';\n\nimport { useDateFormat, useNow } from '@vueuse/core';\n\ninterface Props {\n  avatar?: string;\n}\n\ndefineOptions({\n  name: 'LockScreen',\n});\n\nwithDefaults(defineProps<Props>(), {\n  avatar: '',\n});\n\ndefineEmits<{ toLogin: [] }>();\n\nconst { locale } = useI18n();\nconst accessStore = useAccessStore();\n\nconst now = useNow();\nconst meridiem = useDateFormat(now, 'A');\nconst hour = useDateFormat(now, 'HH');\nconst minute = useDateFormat(now, 'mm');\nconst date = useDateFormat(now, 'YYYY-MM-DD dddd', { locales: locale.value });\n\nconst showUnlockForm = ref(false);\nconst { lockScreenPassword } = storeToRefs(accessStore);\n\nconst [Form, { form, validate }] = useVbenForm(\n  reactive({\n    commonConfig: {\n      hideLabel: true,\n      hideRequiredMark: true,\n    },\n    schema: computed(() => [\n      {\n        component: 'VbenInputPassword' as const,\n        componentProps: {\n          placeholder: $t('ui.widgets.lockScreen.placeholder'),\n        },\n        fieldName: 'password',\n        label: $t('authentication.password'),\n        rules: z.string().min(1, { message: $t('authentication.passwordTip') }),\n      },\n    ]),\n    showDefaultActions: false,\n  }),\n);\n\nconst validPass = computed(\n  () => lockScreenPassword?.value === form?.values?.password,\n);\n\nasync function handleSubmit() {\n  const { valid } = await validate();\n  if (valid) {\n    if (validPass.value) {\n      accessStore.unlockScreen();\n    } else {\n      form.setFieldError('password', $t('authentication.passwordErrorTip'));\n    }\n  }\n}\n\nfunction toggleUnlockForm() {\n  showUnlockForm.value = !showUnlockForm.value;\n}\n\nuseScrollLock();\n</script>\n\n<template>\n  <div class=\"bg-background fixed z-[2000] size-full\">\n    <transition name=\"slide-left\">\n      <div v-show=\"!showUnlockForm\" class=\"size-full\">\n        <div\n          class=\"flex-col-center text-foreground/80 hover:text-foreground group fixed left-1/2 top-6 z-[2001] -translate-x-1/2 cursor-pointer text-xl font-semibold\"\n          @click=\"toggleUnlockForm\"\n        >\n          <LockKeyhole\n            class=\"size-5 transition-all duration-300 group-hover:scale-125\"\n          />\n          <span>{{ $t('ui.widgets.lockScreen.unlock') }}</span>\n        </div>\n        <div class=\"flex h-full w-full items-center justify-center\">\n          <div class=\"flex w-full justify-center gap-4 px-4 sm:gap-6 md:gap-8\">\n            <div\n              class=\"bg-accent relative flex h-[140px] w-[140px] items-center justify-center rounded-xl text-[36px] sm:h-[160px] sm:w-[160px] sm:text-[42px] md:h-[200px] md:w-[200px] md:text-[72px]\"\n            >\n              <span\n                class=\"absolute left-3 top-3 text-xs font-semibold sm:text-sm md:text-xl\"\n              >\n                {{ meridiem }}\n              </span>\n              {{ hour }}\n            </div>\n            <div\n              class=\"bg-accent flex h-[140px] w-[140px] items-center justify-center rounded-xl text-[36px] sm:h-[160px] sm:w-[160px] sm:text-[42px] md:h-[200px] md:w-[200px] md:text-[72px]\"\n            >\n              {{ minute }}\n            </div>\n          </div>\n        </div>\n      </div>\n    </transition>\n\n    <transition name=\"slide-right\">\n      <div\n        v-if=\"showUnlockForm\"\n        class=\"flex-center size-full\"\n        @keydown.enter.prevent=\"handleSubmit\"\n      >\n        <div class=\"flex-col-center mb-10 w-[90%] max-w-[300px] px-4\">\n          <VbenAvatar :src=\"avatar\" class=\"enter-x mb-6 size-20\" />\n          <div class=\"enter-x mb-2 w-full items-center\">\n            <Form />\n          </div>\n          <VbenButton class=\"enter-x w-full\" @click=\"handleSubmit\">\n            {{ $t('ui.widgets.lockScreen.entry') }}\n          </VbenButton>\n          <VbenButton\n            class=\"enter-x my-2 w-full\"\n            variant=\"ghost\"\n            @click=\"$emit('toLogin')\"\n          >\n            {{ $t('ui.widgets.lockScreen.backToLogin') }}\n          </VbenButton>\n          <VbenButton\n            class=\"enter-x mr-2 w-full\"\n            variant=\"ghost\"\n            @click=\"toggleUnlockForm\"\n          >\n            {{ $t('common.back') }}\n          </VbenButton>\n        </div>\n      </div>\n    </transition>\n\n    <div\n      class=\"enter-y absolute bottom-5 w-full text-center text-xl md:text-2xl xl:text-xl 2xl:text-3xl\"\n    >\n      <div v-if=\"showUnlockForm\" class=\"enter-x mb-2 text-2xl md:text-3xl\">\n        {{ hour }}:{{ minute }}\n        <span class=\"text-base md:text-lg\">{{ meridiem }}</span>\n      </div>\n      <div class=\"text-xl md:text-3xl\">{{ date }}</div>\n    </div>\n  </div>\n</template>\n"
  },
  {
    "path": "packages/effects/layouts/src/widgets/notification/index.ts",
    "content": "export { default as Notification } from './notification.vue';\n\nexport type * from './types';\n"
  },
  {
    "path": "packages/effects/layouts/src/widgets/notification/notification.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { NotificationItem } from './types';\n\nimport { Bell, MailCheck } from '@vben/icons';\nimport { $t } from '@vben/locales';\nimport {\n  VbenButton,\n  VbenIconButton,\n  VbenPopover,\n  VbenScrollbar,\n} from '@vben-core/shadcn-ui';\nimport { useToggle } from '@vueuse/core';\n\ninterface Props {\n  /**\n   * 显示圆点\n   */\n  dot?: boolean;\n  /**\n   * 消息列表\n   */\n  notifications?: NotificationItem[];\n}\n\ndefineOptions({ name: 'NotificationPopup' });\n\nwithDefaults(defineProps<Props>(), {\n  dot: false,\n  notifications: () => [],\n});\n\nconst emit = defineEmits<{\n  clear: [];\n  makeAll: [];\n  read: [NotificationItem];\n  viewAll: [];\n}>();\n\nconst [open, toggle] = useToggle();\n\nfunction close() {\n  open.value = false;\n}\n\nfunction handleViewAll() {\n  emit('viewAll');\n  close();\n}\n\nfunction handleMakeAll() {\n  emit('makeAll');\n}\n\nfunction handleClear() {\n  emit('clear');\n}\n\nfunction handleClick(item: NotificationItem) {\n  emit('read', item);\n}\n</script>\n<template>\n  <VbenPopover\n    v-model:open=\"open\"\n    content-class=\"relative right-2 w-[360px] p-0\"\n  >\n    <template #trigger>\n      <div class=\"flex-center mr-2 h-full\" @click.stop=\"toggle()\">\n        <VbenIconButton class=\"bell-button text-foreground relative\">\n          <span\n            v-if=\"dot\"\n            class=\"bg-primary absolute right-0.5 top-0.5 h-2 w-2 rounded\"\n          ></span>\n          <Bell class=\"size-4\" />\n        </VbenIconButton>\n      </div>\n    </template>\n\n    <div class=\"relative\">\n      <div class=\"flex items-center justify-between p-4 py-3\">\n        <div class=\"text-foreground\">{{ $t('ui.widgets.notifications') }}</div>\n        <VbenIconButton\n          :disabled=\"notifications.length <= 0\"\n          :tooltip=\"$t('ui.widgets.markAllAsRead')\"\n          @click=\"handleMakeAll\"\n        >\n          <MailCheck class=\"size-4\" />\n        </VbenIconButton>\n      </div>\n      <VbenScrollbar v-if=\"notifications.length > 0\">\n        <ul class=\"!flex max-h-[360px] w-full flex-col\">\n          <template v-for=\"item in notifications\" :key=\"item.title\">\n            <li\n              class=\"hover:bg-accent border-border relative flex w-full cursor-pointer items-start gap-5 border-t px-3 py-3\"\n              @click=\"handleClick(item)\"\n            >\n              <span\n                v-if=\"!item.isRead\"\n                class=\"bg-primary absolute right-2 top-2 h-2 w-2 rounded\"\n              ></span>\n\n              <span\n                class=\"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full\"\n              >\n                <img\n                  :src=\"item.avatar\"\n                  class=\"aspect-square h-full w-full object-cover\"\n                  role=\"img\"\n                />\n              </span>\n              <div class=\"flex flex-col gap-1 leading-none\">\n                <p class=\"font-semibold\">{{ item.title }}</p>\n                <p class=\"text-muted-foreground my-1 line-clamp-2 text-xs\">\n                  {{ item.message }}\n                </p>\n                <p class=\"text-muted-foreground line-clamp-2 text-xs\">\n                  {{ item.date }}\n                </p>\n              </div>\n            </li>\n          </template>\n        </ul>\n      </VbenScrollbar>\n\n      <template v-else>\n        <div class=\"flex-center text-muted-foreground min-h-[150px] w-full\">\n          {{ $t('common.noData') }}\n        </div>\n      </template>\n\n      <div\n        class=\"border-border flex items-center justify-between border-t px-4 py-3\"\n      >\n        <VbenButton\n          :disabled=\"notifications.length <= 0\"\n          size=\"sm\"\n          variant=\"ghost\"\n          @click=\"handleClear\"\n        >\n          {{ $t('ui.widgets.clearNotifications') }}\n        </VbenButton>\n        <VbenButton size=\"sm\" @click=\"handleViewAll\">\n          {{ $t('ui.widgets.viewAll') }}\n        </VbenButton>\n      </div>\n    </div>\n  </VbenPopover>\n</template>\n\n<style scoped>\n:deep(.bell-button) {\n  &:hover {\n    svg {\n      animation: bell-ring 1s both;\n    }\n  }\n}\n\n@keyframes bell-ring {\n  0%,\n  100% {\n    transform-origin: top;\n  }\n\n  15% {\n    transform: rotateZ(10deg);\n  }\n\n  30% {\n    transform: rotateZ(-10deg);\n  }\n\n  45% {\n    transform: rotateZ(5deg);\n  }\n\n  60% {\n    transform: rotateZ(-5deg);\n  }\n\n  75% {\n    transform: rotateZ(2deg);\n  }\n}\n</style>\n"
  },
  {
    "path": "packages/effects/layouts/src/widgets/notification/types.ts",
    "content": "interface NotificationItem {\n  avatar: string;\n  date: string;\n  isRead?: boolean;\n  message: string;\n  title: string;\n  userId: number | string;\n}\n\nexport type { NotificationItem };\n"
  },
  {
    "path": "packages/effects/layouts/src/widgets/preferences/blocks/block.vue",
    "content": "<script setup lang=\"ts\">\ninterface Props {\n  title?: string;\n}\n\ndefineOptions({\n  name: 'PreferenceBlock',\n});\n\nwithDefaults(defineProps<Props>(), {\n  title: '',\n});\n</script>\n\n<template>\n  <div class=\"flex flex-col py-4\">\n    <h3 class=\"mb-3 font-semibold leading-none tracking-tight\">\n      {{ title }}\n    </h3>\n    <slot></slot>\n  </div>\n</template>\n"
  },
  {
    "path": "packages/effects/layouts/src/widgets/preferences/blocks/checkbox-item.vue",
    "content": "<script setup lang=\"ts\">\nimport type { SelectOption } from '@vben/types';\n\nimport { useSlots } from 'vue';\n\nimport { CircleHelp } from '@vben/icons';\n\nimport { VbenCheckButtonGroup, VbenTooltip } from '@vben-core/shadcn-ui';\n\ndefineOptions({\n  name: 'PreferenceCheckboxItem',\n});\n\nwithDefaults(\n  defineProps<{\n    disabled?: boolean;\n    items?: SelectOption[];\n    multiple?: boolean;\n    onBtnClick?: (value: string) => void;\n    placeholder?: string;\n  }>(),\n  {\n    disabled: false,\n    placeholder: '',\n    items: () => [],\n    onBtnClick: () => {},\n    multiple: false,\n  },\n);\n\nconst inputValue = defineModel<string[]>();\n\nconst slots = useSlots();\n</script>\n\n<template>\n  <div\n    :class=\"{\n      'hover:bg-accent': !slots.tip,\n      'pointer-events-none opacity-50': disabled,\n    }\"\n    class=\"my-1 flex w-full items-center justify-between rounded-md px-2 py-1\"\n  >\n    <span class=\"flex items-center text-sm\">\n      <slot></slot>\n\n      <VbenTooltip v-if=\"slots.tip\" side=\"bottom\">\n        <template #trigger>\n          <CircleHelp class=\"ml-1 size-3 cursor-help\" />\n        </template>\n        <slot name=\"tip\"></slot>\n      </VbenTooltip>\n    </span>\n    <VbenCheckButtonGroup\n      v-model=\"inputValue\"\n      class=\"h-8 w-[165px]\"\n      :options=\"items\"\n      :disabled=\"disabled\"\n      :multiple=\"multiple\"\n      @btn-click=\"onBtnClick\"\n    />\n  </div>\n</template>\n"
  },
  {
    "path": "packages/effects/layouts/src/widgets/preferences/blocks/general/animation.vue",
    "content": "<script setup lang=\"ts\">\nimport { $t } from '@vben/locales';\n\nimport SwitchItem from '../switch-item.vue';\n\ndefineOptions({\n  name: 'PreferenceAnimation',\n});\n\nconst transitionProgress = defineModel<boolean>('transitionProgress', {\n  // 默认值\n  default: false,\n});\nconst transitionName = defineModel<string>('transitionName');\nconst transitionEnable = defineModel<boolean>('transitionEnable');\nconst transitionLoading = defineModel<boolean>('transitionLoading');\n\nconst transitionPreset = ['fade', 'fade-slide', 'fade-up', 'fade-down'];\n\nfunction handleClick(value: string) {\n  transitionName.value = value;\n}\n</script>\n\n<template>\n  <SwitchItem v-model=\"transitionProgress\">\n    {{ $t('preferences.animation.progress') }}\n  </SwitchItem>\n  <SwitchItem v-model=\"transitionLoading\">\n    {{ $t('preferences.animation.loading') }}\n  </SwitchItem>\n  <SwitchItem v-model=\"transitionEnable\">\n    {{ $t('preferences.animation.transition') }}\n  </SwitchItem>\n  <div\n    v-if=\"transitionEnable\"\n    class=\"mb-2 mt-3 flex justify-between gap-3 px-2\"\n  >\n    <div\n      v-for=\"item in transitionPreset\"\n      :key=\"item\"\n      :class=\"{\n        'outline-box-active': transitionName === item,\n      }\"\n      class=\"outline-box p-2\"\n      @click=\"handleClick(item)\"\n    >\n      <div :class=\"`${item}-slow`\" class=\"bg-accent h-10 w-12 rounded-md\"></div>\n    </div>\n  </div>\n</template>\n"
  },
  {
    "path": "packages/effects/layouts/src/widgets/preferences/blocks/general/general.vue",
    "content": "<script setup lang=\"ts\">\nimport { SUPPORT_LANGUAGES } from '@vben/constants';\nimport { $t } from '@vben/locales';\n\nimport InputItem from '../input-item.vue';\nimport SelectItem from '../select-item.vue';\nimport SwitchItem from '../switch-item.vue';\n\ndefineOptions({\n  name: 'PreferenceGeneralConfig',\n});\n\nconst appLocale = defineModel<string>('appLocale');\nconst appDynamicTitle = defineModel<boolean>('appDynamicTitle');\nconst appWatermark = defineModel<boolean>('appWatermark');\nconst appWatermarkContent = defineModel<string>('appWatermarkContent');\nconst appEnableCheckUpdates = defineModel<boolean>('appEnableCheckUpdates');\n</script>\n\n<template>\n  <SelectItem v-model=\"appLocale\" :items=\"SUPPORT_LANGUAGES\">\n    {{ $t('preferences.language') }}\n  </SelectItem>\n  <SwitchItem v-model=\"appDynamicTitle\">\n    {{ $t('preferences.dynamicTitle') }}\n  </SwitchItem>\n  <SwitchItem\n    v-model=\"appWatermark\"\n    @update:model-value=\"\n      (val) => {\n        if (!val) appWatermarkContent = '';\n      }\n    \"\n  >\n    {{ $t('preferences.watermark') }}\n  </SwitchItem>\n  <InputItem\n    v-if=\"appWatermark\"\n    v-model=\"appWatermarkContent\"\n    :placeholder=\"$t('preferences.watermarkContent')\"\n  >\n    {{ $t('preferences.watermarkContent') }}\n  </InputItem>\n  <SwitchItem v-model=\"appEnableCheckUpdates\">\n    {{ $t('preferences.checkUpdates') }}\n  </SwitchItem>\n</template>\n"
  },
  {
    "path": "packages/effects/layouts/src/widgets/preferences/blocks/index.ts",
    "content": "export { default as Block } from './block.vue';\nexport { default as Animation } from './general/animation.vue';\nexport { default as General } from './general/general.vue';\nexport { default as Breadcrumb } from './layout/breadcrumb.vue';\nexport { default as Content } from './layout/content.vue';\nexport { default as Copyright } from './layout/copyright.vue';\nexport { default as Footer } from './layout/footer.vue';\nexport { default as Header } from './layout/header.vue';\nexport { default as Layout } from './layout/layout.vue';\nexport { default as Navigation } from './layout/navigation.vue';\nexport { default as Sidebar } from './layout/sidebar.vue';\nexport { default as Tabbar } from './layout/tabbar.vue';\nexport { default as Widget } from './layout/widget.vue';\nexport { default as GlobalShortcutKeys } from './shortcut-keys/global.vue';\nexport { default as SwitchItem } from './switch-item.vue';\nexport { default as BuiltinTheme } from './theme/builtin.vue';\nexport { default as ColorMode } from './theme/color-mode.vue';\nexport { default as Radius } from './theme/radius.vue';\nexport { default as Theme } from './theme/theme.vue';\n"
  },
  {
    "path": "packages/effects/layouts/src/widgets/preferences/blocks/input-item.vue",
    "content": "<script setup lang=\"ts\">\nimport type { SelectOption } from '@vben/types';\n\nimport { useSlots } from 'vue';\n\nimport { CircleHelp, CircleX } from '@vben/icons';\n\nimport { Input, VbenTooltip } from '@vben-core/shadcn-ui';\n\ndefineOptions({\n  name: 'PreferenceSelectItem',\n});\n\nwithDefaults(\n  defineProps<{\n    disabled?: boolean;\n    items?: SelectOption[];\n    placeholder?: string;\n  }>(),\n  {\n    disabled: false,\n    placeholder: '',\n    items: () => [],\n  },\n);\n\nconst inputValue = defineModel<string>();\n\nconst slots = useSlots();\n</script>\n\n<template>\n  <div\n    :class=\"{\n      'hover:bg-accent': !slots.tip,\n      'pointer-events-none opacity-50': disabled,\n    }\"\n    class=\"my-1 flex w-full items-center justify-between rounded-md px-2 py-1\"\n  >\n    <span class=\"flex items-center text-sm\">\n      <slot></slot>\n\n      <VbenTooltip v-if=\"slots.tip\" side=\"bottom\">\n        <template #trigger>\n          <CircleHelp class=\"ml-1 size-3 cursor-help\" />\n        </template>\n        <slot name=\"tip\"></slot>\n      </VbenTooltip>\n    </span>\n    <div class=\"relative\">\n      <Input\n        v-model=\"inputValue\"\n        class=\"h-8 w-[165px]\"\n        :placeholder=\"placeholder\"\n      />\n      <CircleX\n        v-if=\"inputValue\"\n        class=\"hover:text-foreground text-foreground/60 absolute right-2 top-1/2 size-3 -translate-y-1/2 transform cursor-pointer\"\n        @click=\"() => (inputValue = '')\"\n      />\n    </div>\n  </div>\n</template>\n"
  },
  {
    "path": "packages/effects/layouts/src/widgets/preferences/blocks/layout/breadcrumb.vue",
    "content": "<script setup lang=\"ts\">\nimport type { SelectOption } from '@vben/types';\n\nimport { computed } from 'vue';\n\nimport { $t } from '@vben/locales';\n\nimport SwitchItem from '../switch-item.vue';\nimport ToggleItem from '../toggle-item.vue';\n\ndefineOptions({\n  name: 'PreferenceBreadcrumbConfig',\n});\n\nconst props = defineProps<{ disabled?: boolean }>();\n\nconst breadcrumbEnable = defineModel<boolean>('breadcrumbEnable');\nconst breadcrumbShowIcon = defineModel<boolean>('breadcrumbShowIcon');\nconst breadcrumbStyleType = defineModel<string>('breadcrumbStyleType');\nconst breadcrumbShowHome = defineModel<boolean>('breadcrumbShowHome');\nconst breadcrumbHideOnlyOne = defineModel<boolean>('breadcrumbHideOnlyOne');\n\nconst typeItems: SelectOption[] = [\n  { label: $t('preferences.normal'), value: 'normal' },\n  { label: $t('preferences.breadcrumb.background'), value: 'background' },\n];\n\nconst disableItem = computed(() => {\n  return !breadcrumbEnable.value || props.disabled;\n});\n</script>\n\n<template>\n  <SwitchItem v-model=\"breadcrumbEnable\" :disabled=\"disabled\">\n    {{ $t('preferences.breadcrumb.enable') }}\n  </SwitchItem>\n  <SwitchItem v-model=\"breadcrumbHideOnlyOne\" :disabled=\"disableItem\">\n    {{ $t('preferences.breadcrumb.hideOnlyOne') }}\n  </SwitchItem>\n  <SwitchItem v-model=\"breadcrumbShowIcon\" :disabled=\"disableItem\">\n    {{ $t('preferences.breadcrumb.icon') }}\n  </SwitchItem>\n  <SwitchItem\n    v-model=\"breadcrumbShowHome\"\n    :disabled=\"disableItem || !breadcrumbShowIcon\"\n  >\n    {{ $t('preferences.breadcrumb.home') }}\n  </SwitchItem>\n  <ToggleItem\n    v-model=\"breadcrumbStyleType\"\n    :disabled=\"disableItem\"\n    :items=\"typeItems\"\n  >\n    {{ $t('preferences.breadcrumb.style') }}\n  </ToggleItem>\n</template>\n"
  },
  {
    "path": "packages/effects/layouts/src/widgets/preferences/blocks/layout/content.vue",
    "content": "<script setup lang=\"ts\">\nimport type { Component } from 'vue';\n\nimport { $t } from '@vben/locales';\nimport { computed } from 'vue';\n\nimport { ContentCompact, ContentWide } from '../../icons';\n\ndefineOptions({\n  name: 'PreferenceLayoutContent',\n});\n\nconst modelValue = defineModel<string>({ default: 'wide' });\n\nconst components: Record<string, Component> = {\n  compact: ContentCompact,\n  wide: ContentWide,\n};\n\nconst PRESET = computed(() => [\n  {\n    name: $t('preferences.wide'),\n    type: 'wide',\n  },\n  {\n    name: $t('preferences.compact'),\n    type: 'compact',\n  },\n]);\n\nfunction activeClass(theme: string): string[] {\n  return theme === modelValue.value ? ['outline-box-active'] : [];\n}\n</script>\n\n<template>\n  <div class=\"flex w-full gap-5\">\n    <template v-for=\"theme in PRESET\" :key=\"theme.name\">\n      <div\n        class=\"flex w-[100px] cursor-pointer flex-col\"\n        @click=\"modelValue = theme.type\"\n      >\n        <div :class=\"activeClass(theme.type)\" class=\"outline-box flex-center\">\n          <component :is=\"components[theme.type]\" />\n        </div>\n        <div class=\"text-muted-foreground mt-2 text-center text-xs\">\n          {{ theme.name }}\n        </div>\n      </div>\n    </template>\n  </div>\n</template>\n"
  },
  {
    "path": "packages/effects/layouts/src/widgets/preferences/blocks/layout/copyright.vue",
    "content": "<script setup lang=\"ts\">\nimport { computed } from 'vue';\n\nimport { $t } from '@vben/locales';\n\nimport InputItem from '../input-item.vue';\nimport SwitchItem from '../switch-item.vue';\n\nconst props = defineProps<{ disabled: boolean }>();\n\nconst copyrightEnable = defineModel<boolean>('copyrightEnable');\nconst copyrightDate = defineModel<string>('copyrightDate');\nconst copyrightIcp = defineModel<string>('copyrightIcp');\nconst copyrightIcpLink = defineModel<string>('copyrightIcpLink');\nconst copyrightCompanyName = defineModel<string>('copyrightCompanyName');\nconst copyrightCompanySiteLink = defineModel<string>(\n  'copyrightCompanySiteLink',\n);\n\nconst itemDisabled = computed(() => props.disabled || !copyrightEnable.value);\n</script>\n\n<template>\n  <SwitchItem v-model=\"copyrightEnable\" :disabled=\"disabled\">\n    {{ $t('preferences.copyright.enable') }}\n  </SwitchItem>\n\n  <InputItem v-model=\"copyrightCompanyName\" :disabled=\"itemDisabled\">\n    {{ $t('preferences.copyright.companyName') }}\n  </InputItem>\n  <InputItem v-model=\"copyrightCompanySiteLink\" :disabled=\"itemDisabled\">\n    {{ $t('preferences.copyright.companySiteLink') }}\n  </InputItem>\n  <InputItem v-model=\"copyrightDate\" :disabled=\"itemDisabled\">\n    {{ $t('preferences.copyright.date') }}\n  </InputItem>\n\n  <InputItem v-model=\"copyrightIcp\" :disabled=\"itemDisabled\">\n    {{ $t('preferences.copyright.icp') }}\n  </InputItem>\n  <InputItem v-model=\"copyrightIcpLink\" :disabled=\"itemDisabled\">\n    {{ $t('preferences.copyright.icpLink') }}\n  </InputItem>\n</template>\n"
  },
  {
    "path": "packages/effects/layouts/src/widgets/preferences/blocks/layout/footer.vue",
    "content": "<script setup lang=\"ts\">\nimport { $t } from '@vben/locales';\n\nimport SwitchItem from '../switch-item.vue';\n\nconst footerEnable = defineModel<boolean>('footerEnable');\nconst footerFixed = defineModel<boolean>('footerFixed');\n</script>\n\n<template>\n  <SwitchItem v-model=\"footerEnable\">\n    {{ $t('preferences.footer.visible') }}\n  </SwitchItem>\n  <SwitchItem v-model=\"footerFixed\" :disabled=\"!footerEnable\">\n    {{ $t('preferences.footer.fixed') }}\n  </SwitchItem>\n</template>\n"
  },
  {
    "path": "packages/effects/layouts/src/widgets/preferences/blocks/layout/header.vue",
    "content": "<script setup lang=\"ts\">\nimport type {\n  LayoutHeaderMenuAlignType,\n  LayoutHeaderModeType,\n  SelectOption,\n} from '@vben/types';\n\nimport { $t } from '@vben/locales';\n\nimport SelectItem from '../select-item.vue';\nimport SwitchItem from '../switch-item.vue';\nimport ToggleItem from '../toggle-item.vue';\n\ndefineProps<{ disabled: boolean }>();\n\nconst headerEnable = defineModel<boolean>('headerEnable');\nconst headerMode = defineModel<LayoutHeaderModeType>('headerMode');\nconst headerMenuAlign =\n  defineModel<LayoutHeaderMenuAlignType>('headerMenuAlign');\n\nconst localeItems: SelectOption[] = [\n  {\n    label: $t('preferences.header.modeStatic'),\n    value: 'static',\n  },\n  {\n    label: $t('preferences.header.modeFixed'),\n    value: 'fixed',\n  },\n  {\n    label: $t('preferences.header.modeAuto'),\n    value: 'auto',\n  },\n  {\n    label: $t('preferences.header.modeAutoScroll'),\n    value: 'auto-scroll',\n  },\n];\n\nconst headerMenuAlignItems: SelectOption[] = [\n  {\n    label: $t('preferences.header.menuAlignStart'),\n    value: 'start',\n  },\n  {\n    label: $t('preferences.header.menuAlignCenter'),\n    value: 'center',\n  },\n  {\n    label: $t('preferences.header.menuAlignEnd'),\n    value: 'end',\n  },\n];\n</script>\n\n<template>\n  <SwitchItem v-model=\"headerEnable\" :disabled=\"disabled\">\n    {{ $t('preferences.header.visible') }}\n  </SwitchItem>\n  <SelectItem\n    v-model=\"headerMode\"\n    :disabled=\"!headerEnable\"\n    :items=\"localeItems\"\n  >\n    {{ $t('preferences.mode') }}\n  </SelectItem>\n  <ToggleItem\n    v-model=\"headerMenuAlign\"\n    :disabled=\"!headerEnable\"\n    :items=\"headerMenuAlignItems\"\n  >\n    {{ $t('preferences.header.menuAlign') }}\n  </ToggleItem>\n</template>\n"
  },
  {
    "path": "packages/effects/layouts/src/widgets/preferences/blocks/layout/layout.vue",
    "content": "<script setup lang=\"ts\">\nimport type { LayoutType } from '@vben/types';\nimport type { Component } from 'vue';\n\nimport { CircleHelp } from '@vben/icons';\nimport { $t } from '@vben/locales';\nimport { VbenTooltip } from '@vben-core/shadcn-ui';\nimport { computed } from 'vue';\n\nimport {\n  FullContent,\n  HeaderMixedNav,\n  HeaderNav,\n  HeaderSidebarNav,\n  MixedNav,\n  SidebarMixedNav,\n  SidebarNav,\n} from '../../icons';\n\ninterface PresetItem {\n  name: string;\n  tip: string;\n  type: LayoutType;\n}\n\ndefineOptions({\n  name: 'PreferenceLayout',\n});\n\nconst modelValue = defineModel<LayoutType>({ default: 'sidebar-nav' });\n\nconst components: Record<LayoutType, Component> = {\n  'full-content': FullContent,\n  'header-nav': HeaderNav,\n  'mixed-nav': MixedNav,\n  'sidebar-mixed-nav': SidebarMixedNav,\n  'sidebar-nav': SidebarNav,\n  'header-mixed-nav': HeaderMixedNav,\n  'header-sidebar-nav': HeaderSidebarNav,\n};\n\nconst PRESET = computed((): PresetItem[] => [\n  {\n    name: $t('preferences.vertical'),\n    tip: $t('preferences.verticalTip'),\n    type: 'sidebar-nav',\n  },\n  {\n    name: $t('preferences.twoColumn'),\n    tip: $t('preferences.twoColumnTip'),\n    type: 'sidebar-mixed-nav',\n  },\n  {\n    name: $t('preferences.horizontal'),\n    tip: $t('preferences.horizontalTip'),\n    type: 'header-nav',\n  },\n  {\n    name: $t('preferences.headerSidebarNav'),\n    tip: $t('preferences.headerSidebarNavTip'),\n    type: 'header-sidebar-nav',\n  },\n  {\n    name: $t('preferences.mixedMenu'),\n    tip: $t('preferences.mixedMenuTip'),\n    type: 'mixed-nav',\n  },\n  {\n    name: $t('preferences.headerTwoColumn'),\n    tip: $t('preferences.headerTwoColumnTip'),\n    type: 'header-mixed-nav',\n  },\n  {\n    name: $t('preferences.fullContent'),\n    tip: $t('preferences.fullContentTip'),\n    type: 'full-content',\n  },\n]);\n\nfunction activeClass(theme: string): string[] {\n  return theme === modelValue.value ? ['outline-box-active'] : [];\n}\n</script>\n\n<template>\n  <div class=\"flex w-full flex-wrap gap-5\">\n    <template v-for=\"theme in PRESET\" :key=\"theme.name\">\n      <div\n        class=\"flex w-[100px] cursor-pointer flex-col\"\n        @click=\"modelValue = theme.type\"\n      >\n        <div :class=\"activeClass(theme.type)\" class=\"outline-box flex-center\">\n          <component :is=\"components[theme.type]\" />\n        </div>\n        <div\n          class=\"text-muted-foreground flex-center hover:text-foreground mt-2 text-center text-xs\"\n        >\n          {{ theme.name }}\n          <VbenTooltip v-if=\"theme.tip\" side=\"bottom\">\n            <template #trigger>\n              <CircleHelp class=\"ml-1 size-3 cursor-help\" />\n            </template>\n            {{ theme.tip }}\n          </VbenTooltip>\n        </div>\n      </div>\n    </template>\n  </div>\n</template>\n"
  },
  {
    "path": "packages/effects/layouts/src/widgets/preferences/blocks/layout/navigation.vue",
    "content": "<script setup lang=\"ts\">\nimport type { SelectOption } from '@vben/types';\n\nimport { $t } from '@vben/locales';\n\nimport SwitchItem from '../switch-item.vue';\nimport ToggleItem from '../toggle-item.vue';\n\ndefineOptions({\n  name: 'PreferenceNavigationConfig',\n});\n\ndefineProps<{ disabled?: boolean; disabledNavigationSplit?: boolean }>();\n\nconst navigationStyleType = defineModel<string>('navigationStyleType');\nconst navigationSplit = defineModel<boolean>('navigationSplit');\nconst navigationAccordion = defineModel<boolean>('navigationAccordion');\n\nconst stylesItems: SelectOption[] = [\n  { label: $t('preferences.rounded'), value: 'rounded' },\n  { label: $t('preferences.plain'), value: 'plain' },\n];\n</script>\n\n<template>\n  <ToggleItem\n    v-model=\"navigationStyleType\"\n    :disabled=\"disabled\"\n    :items=\"stylesItems\"\n  >\n    {{ $t('preferences.navigationMenu.style') }}\n  </ToggleItem>\n  <SwitchItem\n    v-model=\"navigationSplit\"\n    :disabled=\"disabledNavigationSplit || disabled\"\n  >\n    {{ $t('preferences.navigationMenu.split') }}\n    <template #tip>\n      {{ $t('preferences.navigationMenu.splitTip') }}\n    </template>\n  </SwitchItem>\n  <SwitchItem v-model=\"navigationAccordion\" :disabled=\"disabled\">\n    {{ $t('preferences.navigationMenu.accordion') }}\n  </SwitchItem>\n</template>\n"
  },
  {
    "path": "packages/effects/layouts/src/widgets/preferences/blocks/layout/sidebar.vue",
    "content": "<script setup lang=\"ts\">\nimport type { LayoutType } from '@vben/types';\n\nimport { onMounted } from 'vue';\n\nimport { $t } from '@vben/locales';\n\nimport CheckboxItem from '../checkbox-item.vue';\nimport NumberFieldItem from '../number-field-item.vue';\nimport SwitchItem from '../switch-item.vue';\n\ndefineProps<{ currentLayout?: LayoutType; disabled: boolean }>();\n\nconst sidebarEnable = defineModel<boolean>('sidebarEnable');\nconst sidebarWidth = defineModel<number>('sidebarWidth');\nconst sidebarCollapsedShowTitle = defineModel<boolean>(\n  'sidebarCollapsedShowTitle',\n);\nconst sidebarAutoActivateChild = defineModel<boolean>(\n  'sidebarAutoActivateChild',\n);\nconst sidebarCollapsed = defineModel<boolean>('sidebarCollapsed');\nconst sidebarExpandOnHover = defineModel<boolean>('sidebarExpandOnHover');\n\nconst sidebarButtons = defineModel<string[]>('sidebarButtons', { default: [] });\nconst sidebarCollapsedButton = defineModel<boolean>('sidebarCollapsedButton');\nconst sidebarFixedButton = defineModel<boolean>('sidebarFixedButton');\n\nonMounted(() => {\n  if (\n    sidebarCollapsedButton.value &&\n    !sidebarButtons.value.includes('collapsed')\n  ) {\n    sidebarButtons.value.push('collapsed');\n  }\n  if (sidebarFixedButton.value && !sidebarButtons.value.includes('fixed')) {\n    sidebarButtons.value.push('fixed');\n  }\n});\n\nconst handleCheckboxChange = () => {\n  sidebarCollapsedButton.value = !!sidebarButtons.value.includes('collapsed');\n  sidebarFixedButton.value = !!sidebarButtons.value.includes('fixed');\n};\n</script>\n\n<template>\n  <SwitchItem v-model=\"sidebarEnable\" :disabled=\"disabled\">\n    {{ $t('preferences.sidebar.visible') }}\n  </SwitchItem>\n  <SwitchItem v-model=\"sidebarCollapsed\" :disabled=\"!sidebarEnable || disabled\">\n    {{ $t('preferences.sidebar.collapsed') }}\n  </SwitchItem>\n  <SwitchItem\n    v-model=\"sidebarExpandOnHover\"\n    :disabled=\"!sidebarEnable || disabled || !sidebarCollapsed\"\n    :tip=\"$t('preferences.sidebar.expandOnHoverTip')\"\n  >\n    {{ $t('preferences.sidebar.expandOnHover') }}\n  </SwitchItem>\n  <SwitchItem\n    v-model=\"sidebarCollapsedShowTitle\"\n    :disabled=\"!sidebarEnable || disabled || !sidebarCollapsed\"\n  >\n    {{ $t('preferences.sidebar.collapsedShowTitle') }}\n  </SwitchItem>\n  <SwitchItem\n    v-model=\"sidebarAutoActivateChild\"\n    :disabled=\"\n      !sidebarEnable ||\n      !['sidebar-mixed-nav', 'mixed-nav', 'header-mixed-nav'].includes(\n        currentLayout as string,\n      ) ||\n      disabled\n    \"\n    :tip=\"$t('preferences.sidebar.autoActivateChildTip')\"\n  >\n    {{ $t('preferences.sidebar.autoActivateChild') }}\n  </SwitchItem>\n  <CheckboxItem\n    :items=\"[\n      { label: $t('preferences.sidebar.buttonCollapsed'), value: 'collapsed' },\n      { label: $t('preferences.sidebar.buttonFixed'), value: 'fixed' },\n    ]\"\n    multiple\n    v-model=\"sidebarButtons\"\n    :on-btn-click=\"handleCheckboxChange\"\n  >\n    {{ $t('preferences.sidebar.buttons') }}\n  </CheckboxItem>\n  <NumberFieldItem\n    v-model=\"sidebarWidth\"\n    :disabled=\"!sidebarEnable || disabled\"\n    :max=\"320\"\n    :min=\"160\"\n    :step=\"10\"\n  >\n    {{ $t('preferences.sidebar.width') }}\n  </NumberFieldItem>\n</template>\n"
  },
  {
    "path": "packages/effects/layouts/src/widgets/preferences/blocks/layout/tabbar.vue",
    "content": "<script setup lang=\"ts\">\nimport type { SelectOption } from '@vben/types';\n\nimport { computed } from 'vue';\n\nimport { $t } from '@vben/locales';\n\nimport NumberFieldItem from '../number-field-item.vue';\nimport SelectItem from '../select-item.vue';\nimport SwitchItem from '../switch-item.vue';\n\ndefineOptions({\n  name: 'PreferenceTabsConfig',\n});\n\ndefineProps<{ disabled?: boolean }>();\n\nconst tabbarEnable = defineModel<boolean>('tabbarEnable');\nconst tabbarShowIcon = defineModel<boolean>('tabbarShowIcon');\nconst tabbarPersist = defineModel<boolean>('tabbarPersist');\nconst tabbarDraggable = defineModel<boolean>('tabbarDraggable');\nconst tabbarWheelable = defineModel<boolean>('tabbarWheelable');\nconst tabbarStyleType = defineModel<string>('tabbarStyleType');\nconst tabbarShowMore = defineModel<boolean>('tabbarShowMore');\nconst tabbarShowMaximize = defineModel<boolean>('tabbarShowMaximize');\nconst tabbarMaxCount = defineModel<number>('tabbarMaxCount');\nconst tabbarMiddleClickToClose = defineModel<boolean>(\n  'tabbarMiddleClickToClose',\n);\n\nconst styleItems = computed((): SelectOption[] => [\n  {\n    label: $t('preferences.tabbar.styleType.chrome'),\n    value: 'chrome',\n  },\n  {\n    label: $t('preferences.tabbar.styleType.plain'),\n    value: 'plain',\n  },\n  {\n    label: $t('preferences.tabbar.styleType.card'),\n    value: 'card',\n  },\n\n  {\n    label: $t('preferences.tabbar.styleType.brisk'),\n    value: 'brisk',\n  },\n]);\n</script>\n\n<template>\n  <SwitchItem v-model=\"tabbarEnable\" :disabled=\"disabled\">\n    {{ $t('preferences.tabbar.enable') }}\n  </SwitchItem>\n  <SwitchItem v-model=\"tabbarPersist\" :disabled=\"!tabbarEnable\">\n    {{ $t('preferences.tabbar.persist') }}\n  </SwitchItem>\n  <NumberFieldItem\n    v-model=\"tabbarMaxCount\"\n    :disabled=\"!tabbarEnable\"\n    :max=\"30\"\n    :min=\"0\"\n    :step=\"5\"\n    :tip=\"$t('preferences.tabbar.maxCountTip')\"\n  >\n    {{ $t('preferences.tabbar.maxCount') }}\n  </NumberFieldItem>\n  <SwitchItem v-model=\"tabbarDraggable\" :disabled=\"!tabbarEnable\">\n    {{ $t('preferences.tabbar.draggable') }}\n  </SwitchItem>\n  <SwitchItem\n    v-model=\"tabbarWheelable\"\n    :disabled=\"!tabbarEnable\"\n    :tip=\"$t('preferences.tabbar.wheelableTip')\"\n  >\n    {{ $t('preferences.tabbar.wheelable') }}\n  </SwitchItem>\n  <SwitchItem v-model=\"tabbarMiddleClickToClose\" :disabled=\"!tabbarEnable\">\n    {{ $t('preferences.tabbar.middleClickClose') }}\n  </SwitchItem>\n  <SwitchItem v-model=\"tabbarShowIcon\" :disabled=\"!tabbarEnable\">\n    {{ $t('preferences.tabbar.icon') }}\n  </SwitchItem>\n  <SwitchItem v-model=\"tabbarShowMore\" :disabled=\"!tabbarEnable\">\n    {{ $t('preferences.tabbar.showMore') }}\n  </SwitchItem>\n  <SwitchItem v-model=\"tabbarShowMaximize\" :disabled=\"!tabbarEnable\">\n    {{ $t('preferences.tabbar.showMaximize') }}\n  </SwitchItem>\n  <SelectItem v-model=\"tabbarStyleType\" :items=\"styleItems\">\n    {{ $t('preferences.tabbar.styleType.title') }}\n  </SelectItem>\n</template>\n"
  },
  {
    "path": "packages/effects/layouts/src/widgets/preferences/blocks/layout/widget.vue",
    "content": "<script setup lang=\"ts\">\nimport type { SelectOption } from '@vben/types';\n\nimport { computed } from 'vue';\n\nimport { $t } from '@vben/locales';\n\nimport SelectItem from '../select-item.vue';\nimport SwitchItem from '../switch-item.vue';\n\ndefineOptions({\n  name: 'PreferenceInterfaceControl',\n});\n\nconst widgetGlobalSearch = defineModel<boolean>('widgetGlobalSearch');\nconst widgetFullscreen = defineModel<boolean>('widgetFullscreen');\nconst widgetLanguageToggle = defineModel<boolean>('widgetLanguageToggle');\nconst widgetNotification = defineModel<boolean>('widgetNotification');\nconst widgetThemeToggle = defineModel<boolean>('widgetThemeToggle');\nconst widgetSidebarToggle = defineModel<boolean>('widgetSidebarToggle');\nconst widgetLockScreen = defineModel<boolean>('widgetLockScreen');\nconst appPreferencesButtonPosition = defineModel<string>(\n  'appPreferencesButtonPosition',\n);\nconst widgetRefresh = defineModel<boolean>('widgetRefresh');\n\nconst positionItems = computed((): SelectOption[] => [\n  {\n    label: $t('preferences.position.auto'),\n    value: 'auto',\n  },\n  {\n    label: $t('preferences.position.header'),\n    value: 'header',\n  },\n  {\n    label: $t('preferences.position.fixed'),\n    value: 'fixed',\n  },\n]);\n</script>\n\n<template>\n  <SwitchItem v-model=\"widgetGlobalSearch\">\n    {{ $t('preferences.widget.globalSearch') }}\n  </SwitchItem>\n  <SwitchItem v-model=\"widgetThemeToggle\">\n    {{ $t('preferences.widget.themeToggle') }}\n  </SwitchItem>\n  <SwitchItem v-model=\"widgetLanguageToggle\">\n    {{ $t('preferences.widget.languageToggle') }}\n  </SwitchItem>\n  <SwitchItem v-model=\"widgetFullscreen\">\n    {{ $t('preferences.widget.fullscreen') }}\n  </SwitchItem>\n  <SwitchItem v-model=\"widgetNotification\">\n    {{ $t('preferences.widget.notification') }}\n  </SwitchItem>\n  <SwitchItem v-model=\"widgetLockScreen\">\n    {{ $t('preferences.widget.lockScreen') }}\n  </SwitchItem>\n  <SwitchItem v-model=\"widgetSidebarToggle\">\n    {{ $t('preferences.widget.sidebarToggle') }}\n  </SwitchItem>\n  <SwitchItem v-model=\"widgetRefresh\">\n    {{ $t('preferences.widget.refresh') }}\n  </SwitchItem>\n  <SelectItem v-model=\"appPreferencesButtonPosition\" :items=\"positionItems\">\n    {{ $t('preferences.position.title') }}\n  </SelectItem>\n</template>\n"
  },
  {
    "path": "packages/effects/layouts/src/widgets/preferences/blocks/number-field-item.vue",
    "content": "<script setup lang=\"ts\">\nimport type { SelectOption } from '@vben/types';\n\nimport { useSlots } from 'vue';\n\nimport { CircleHelp } from '@vben/icons';\n\nimport {\n  NumberField,\n  NumberFieldContent,\n  NumberFieldDecrement,\n  NumberFieldIncrement,\n  NumberFieldInput,\n  VbenTooltip,\n} from '@vben-core/shadcn-ui';\n\ndefineOptions({\n  name: 'PreferenceSelectItem',\n});\n\nwithDefaults(\n  defineProps<{\n    disabled?: boolean;\n    items?: SelectOption[];\n    placeholder?: string;\n    tip?: string;\n  }>(),\n  {\n    disabled: false,\n    placeholder: '',\n    tip: '',\n    items: () => [],\n  },\n);\n\nconst inputValue = defineModel<number>();\n\nconst slots = useSlots();\n</script>\n\n<template>\n  <div\n    :class=\"{\n      'hover:bg-accent': !slots.tip,\n      'pointer-events-none opacity-50': disabled,\n    }\"\n    class=\"my-1 flex w-full items-center justify-between rounded-md px-2 py-1\"\n  >\n    <span class=\"flex items-center text-sm\">\n      <slot></slot>\n\n      <VbenTooltip v-if=\"slots.tip || tip\" side=\"bottom\">\n        <template #trigger>\n          <CircleHelp class=\"ml-1 size-3 cursor-help\" />\n        </template>\n        <slot name=\"tip\">\n          <template v-if=\"tip\">\n            <p v-for=\"(line, index) in tip.split('\\n')\" :key=\"index\">\n              {{ line }}\n            </p>\n          </template>\n        </slot>\n      </VbenTooltip>\n    </span>\n\n    <NumberField v-model=\"inputValue\" v-bind=\"$attrs\" class=\"w-[165px]\">\n      <NumberFieldContent>\n        <NumberFieldDecrement />\n        <NumberFieldInput />\n        <NumberFieldIncrement />\n      </NumberFieldContent>\n    </NumberField>\n  </div>\n</template>\n"
  },
  {
    "path": "packages/effects/layouts/src/widgets/preferences/blocks/select-item.vue",
    "content": "<script setup lang=\"ts\">\nimport type { SelectOption } from '@vben/types';\n\nimport { CircleHelp } from '@vben/icons';\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n  VbenTooltip,\n} from '@vben-core/shadcn-ui';\nimport { useSlots } from 'vue';\n\ndefineOptions({\n  name: 'PreferenceSelectItem',\n});\n\nwithDefaults(\n  defineProps<{\n    disabled?: boolean;\n    items?: SelectOption[];\n    placeholder?: string;\n  }>(),\n  {\n    disabled: false,\n    placeholder: '',\n    items: () => [],\n  },\n);\n\nconst selectValue = defineModel<string>();\n\nconst slots = useSlots();\n</script>\n\n<template>\n  <div\n    :class=\"{\n      'hover:bg-accent': !slots.tip,\n      'pointer-events-none opacity-50': disabled,\n    }\"\n    class=\"my-1 flex w-full items-center justify-between rounded-md px-2 py-1\"\n  >\n    <span class=\"flex items-center text-sm\">\n      <slot></slot>\n\n      <VbenTooltip v-if=\"slots.tip\" side=\"bottom\">\n        <template #trigger>\n          <CircleHelp class=\"ml-1 size-3 cursor-help\" />\n        </template>\n        <slot name=\"tip\"></slot>\n      </VbenTooltip>\n    </span>\n    <Select v-model=\"selectValue\">\n      <SelectTrigger class=\"h-8 w-[165px]\">\n        <SelectValue :placeholder=\"placeholder\" />\n      </SelectTrigger>\n      <SelectContent>\n        <template v-for=\"item in items\" :key=\"item.value\">\n          <SelectItem :value=\"item.value\"> {{ item.label }} </SelectItem>\n        </template>\n      </SelectContent>\n    </Select>\n  </div>\n</template>\n"
  },
  {
    "path": "packages/effects/layouts/src/widgets/preferences/blocks/shortcut-keys/global.vue",
    "content": "<script setup lang=\"ts\">\nimport { computed } from 'vue';\n\nimport { $t } from '@vben/locales';\nimport { isWindowsOs } from '@vben/utils';\n\nimport SwitchItem from '../switch-item.vue';\n\ndefineOptions({\n  name: 'PreferenceGeneralConfig',\n});\n\nconst shortcutKeysEnable = defineModel<boolean>('shortcutKeysEnable');\nconst shortcutKeysGlobalSearch = defineModel<boolean>(\n  'shortcutKeysGlobalSearch',\n);\nconst shortcutKeysLogout = defineModel<boolean>('shortcutKeysLogout');\n// const shortcutKeysPreferences = defineModel<boolean>('shortcutKeysPreferences');\nconst shortcutKeysLockScreen = defineModel<boolean>('shortcutKeysLockScreen');\n\nconst altView = computed(() => (isWindowsOs() ? 'Alt' : '⌥'));\n</script>\n\n<template>\n  <SwitchItem v-model=\"shortcutKeysEnable\">\n    {{ $t('preferences.shortcutKeys.title') }}\n  </SwitchItem>\n  <SwitchItem\n    v-model=\"shortcutKeysGlobalSearch\"\n    :disabled=\"!shortcutKeysEnable\"\n  >\n    {{ $t('preferences.shortcutKeys.search') }}\n    <template #shortcut>\n      {{ isWindowsOs() ? 'Ctrl' : '⌘' }}\n      <kbd> K </kbd>\n    </template>\n  </SwitchItem>\n  <SwitchItem v-model=\"shortcutKeysLogout\" :disabled=\"!shortcutKeysEnable\">\n    {{ $t('preferences.shortcutKeys.logout') }}\n    <template #shortcut> {{ altView }} Q </template>\n  </SwitchItem>\n  <!-- <SwitchItem v-model=\"shortcutKeysPreferences\" :disabled=\"!shortcutKeysEnable\">\n    {{ $t('preferences.shortcutKeys.preferences') }}\n    <template #shortcut> {{ altView }} , </template>\n  </SwitchItem> -->\n  <SwitchItem v-model=\"shortcutKeysLockScreen\" :disabled=\"!shortcutKeysEnable\">\n    {{ $t('ui.widgets.lockScreen.title') }}\n    <template #shortcut> {{ altView }} L </template>\n  </SwitchItem>\n</template>\n"
  },
  {
    "path": "packages/effects/layouts/src/widgets/preferences/blocks/switch-item.vue",
    "content": "<script setup lang=\"ts\">\nimport { CircleHelp } from '@vben/icons';\nimport { Switch, VbenTooltip } from '@vben-core/shadcn-ui';\nimport { useSlots } from 'vue';\n\ndefineOptions({\n  name: 'PreferenceSwitchItem',\n});\n\nwithDefaults(defineProps<{ disabled?: boolean; tip?: string }>(), {\n  disabled: false,\n  tip: '',\n});\n\nconst checked = defineModel<boolean>();\n\nconst slots = useSlots();\n\nfunction handleClick() {\n  checked.value = !checked.value;\n}\n</script>\n\n<template>\n  <div\n    :class=\"{\n      'pointer-events-none opacity-50': disabled,\n    }\"\n    class=\"hover:bg-accent my-1 flex w-full items-center justify-between rounded-md px-2 py-2.5\"\n    @click=\"handleClick\"\n  >\n    <span class=\"flex items-center text-sm\">\n      <slot></slot>\n\n      <VbenTooltip v-if=\"slots.tip || tip\" side=\"bottom\">\n        <template #trigger>\n          <CircleHelp class=\"ml-1 size-3 cursor-help\" />\n        </template>\n        <slot name=\"tip\">\n          <template v-if=\"tip\">\n            <p v-for=\"(line, index) in tip.split('\\n')\" :key=\"index\">\n              {{ line }}\n            </p>\n          </template>\n        </slot>\n      </VbenTooltip>\n    </span>\n    <span v-if=\"$slots.shortcut\" class=\"ml-auto mr-2 text-xs opacity-60\">\n      <slot name=\"shortcut\"></slot>\n    </span>\n    <Switch v-model:checked=\"checked\" @click.stop />\n  </div>\n</template>\n"
  },
  {
    "path": "packages/effects/layouts/src/widgets/preferences/blocks/theme/builtin.vue",
    "content": "<script setup lang=\"ts\">\nimport type { BuiltinThemePreset } from '@vben/preferences';\nimport type { BuiltinThemeType } from '@vben/types';\n\nimport { computed, ref, watch } from 'vue';\n\nimport { UserRoundPen } from '@vben/icons';\nimport { $t } from '@vben/locales';\nimport { BUILT_IN_THEME_PRESETS } from '@vben/preferences';\nimport { convertToHsl, TinyColor } from '@vben/utils';\n\nimport { useThrottleFn } from '@vueuse/core';\n\ndefineOptions({\n  name: 'PreferenceBuiltinTheme',\n});\n\nconst props = defineProps<{ isDark: boolean }>();\n\nconst colorInput = ref();\nconst modelValue = defineModel<BuiltinThemeType>({ default: 'default' });\nconst themeColorPrimary = defineModel<string>('themeColorPrimary');\n\nconst updateThemeColorPrimary = useThrottleFn(\n  (value: string) => {\n    themeColorPrimary.value = value;\n  },\n  300,\n  true,\n  true,\n);\n\nconst inputValue = computed(() => {\n  return new TinyColor(themeColorPrimary.value || '').toHexString();\n});\n\nconst builtinThemePresets = computed(() => {\n  return [...BUILT_IN_THEME_PRESETS];\n});\n\nfunction typeView(name: BuiltinThemeType) {\n  switch (name) {\n    case 'custom': {\n      return $t('preferences.theme.builtin.custom');\n    }\n    case 'deep-blue': {\n      return $t('preferences.theme.builtin.deepBlue');\n    }\n    case 'deep-green': {\n      return $t('preferences.theme.builtin.deepGreen');\n    }\n    case 'default': {\n      return $t('preferences.theme.builtin.default');\n    }\n    case 'gray': {\n      return $t('preferences.theme.builtin.gray');\n    }\n    case 'green': {\n      return $t('preferences.theme.builtin.green');\n    }\n\n    case 'neutral': {\n      return $t('preferences.theme.builtin.neutral');\n    }\n    case 'orange': {\n      return $t('preferences.theme.builtin.orange');\n    }\n    case 'pink': {\n      return $t('preferences.theme.builtin.pink');\n    }\n    case 'rose': {\n      return $t('preferences.theme.builtin.rose');\n    }\n    case 'sky-blue': {\n      return $t('preferences.theme.builtin.skyBlue');\n    }\n    case 'slate': {\n      return $t('preferences.theme.builtin.slate');\n    }\n    case 'violet': {\n      return $t('preferences.theme.builtin.violet');\n    }\n    case 'yellow': {\n      return $t('preferences.theme.builtin.yellow');\n    }\n    case 'zinc': {\n      return $t('preferences.theme.builtin.zinc');\n    }\n  }\n}\n\nfunction handleSelect(theme: BuiltinThemePreset) {\n  modelValue.value = theme.type;\n}\n\nfunction handleInputChange(e: Event) {\n  const target = e.target as HTMLInputElement;\n  updateThemeColorPrimary(convertToHsl(target.value));\n}\n\nfunction selectColor() {\n  colorInput.value?.[0]?.click?.();\n}\n\nwatch(\n  () => [modelValue.value, props.isDark] as [BuiltinThemeType, boolean],\n  ([themeType, isDark], [_, isDarkPrev]) => {\n    const theme = builtinThemePresets.value.find(\n      (item) => item.type === themeType,\n    );\n    if (theme) {\n      const primaryColor = isDark\n        ? theme.darkPrimaryColor || theme.primaryColor\n        : theme.primaryColor;\n\n      if (!(theme.type === 'custom' && isDark !== isDarkPrev)) {\n        themeColorPrimary.value = primaryColor || theme.color;\n      }\n    }\n  },\n);\n</script>\n\n<template>\n  <div class=\"flex w-full flex-wrap justify-between\">\n    <template v-for=\"theme in builtinThemePresets\" :key=\"theme.type\">\n      <div class=\"flex cursor-pointer flex-col\" @click=\"handleSelect(theme)\">\n        <div\n          :class=\"{\n            'outline-box-active': theme.type === modelValue,\n          }\"\n          class=\"outline-box flex-center group cursor-pointer\"\n        >\n          <template v-if=\"theme.type !== 'custom'\">\n            <div\n              :style=\"{ backgroundColor: theme.color }\"\n              class=\"mx-9 my-2 size-5 rounded-md\"\n            ></div>\n          </template>\n          <template v-else>\n            <div class=\"size-full px-9 py-2\" @click.stop=\"selectColor\">\n              <div class=\"flex-center relative size-5 rounded-sm\">\n                <UserRoundPen\n                  class=\"z-1 absolute size-5 opacity-60 group-hover:opacity-100\"\n                />\n                <input\n                  ref=\"colorInput\"\n                  :value=\"inputValue\"\n                  class=\"absolute inset-0 opacity-0\"\n                  type=\"color\"\n                  @input=\"handleInputChange\"\n                />\n              </div>\n            </div>\n          </template>\n        </div>\n        <div class=\"text-muted-foreground my-2 text-center text-xs\">\n          {{ typeView(theme.type) }}\n        </div>\n      </div>\n    </template>\n  </div>\n</template>\n"
  },
  {
    "path": "packages/effects/layouts/src/widgets/preferences/blocks/theme/color-mode.vue",
    "content": "<script setup lang=\"ts\">\nimport { $t } from '@vben/locales';\n\nimport SwitchItem from '../switch-item.vue';\n\ndefineOptions({\n  name: 'PreferenceColorMode',\n});\n\nconst appColorWeakMode = defineModel<boolean>('appColorWeakMode', {\n  default: false,\n});\n\nconst appColorGrayMode = defineModel<boolean>('appColorGrayMode', {\n  default: false,\n});\n</script>\n\n<template>\n  <SwitchItem v-model=\"appColorWeakMode\">\n    {{ $t('preferences.theme.weakMode') }}\n  </SwitchItem>\n  <SwitchItem v-model=\"appColorGrayMode\">\n    {{ $t('preferences.theme.grayMode') }}\n  </SwitchItem>\n</template>\n"
  },
  {
    "path": "packages/effects/layouts/src/widgets/preferences/blocks/theme/radius.vue",
    "content": "<script setup lang=\"ts\">\nimport { ToggleGroup, ToggleGroupItem } from '@vben-core/shadcn-ui';\n\ndefineOptions({\n  name: 'PreferenceColorMode',\n});\n\nconst modelValue = defineModel<string | undefined>('themeRadius', {\n  default: '0.5',\n});\n\nconst items = [\n  { label: '0', value: '0' },\n  { label: '0.25', value: '0.25' },\n  { label: '0.5', value: '0.5' },\n  { label: '0.75', value: '0.75' },\n  { label: '1', value: '1' },\n];\n</script>\n\n<template>\n  <ToggleGroup\n    v-model=\"modelValue\"\n    class=\"gap-2\"\n    size=\"sm\"\n    type=\"single\"\n    variant=\"outline\"\n  >\n    <template v-for=\"item in items\" :key=\"item.value\">\n      <ToggleGroupItem\n        :value=\"item.value\"\n        class=\"data-[state=on]:bg-primary data-[state=on]:text-primary-foreground h-7 w-16 rounded-sm\"\n      >\n        {{ item.label }}\n      </ToggleGroupItem>\n    </template>\n  </ToggleGroup>\n</template>\n"
  },
  {
    "path": "packages/effects/layouts/src/widgets/preferences/blocks/theme/theme.vue",
    "content": "<script setup lang=\"ts\">\nimport type { ThemeModeType } from '@vben/types';\nimport type { Component } from 'vue';\n\nimport { MoonStar, Sun, SunMoon } from '@vben/icons';\nimport { $t } from '@vben/locales';\n\nimport SwitchItem from '../switch-item.vue';\n\ndefineOptions({\n  name: 'PreferenceTheme',\n});\n\nconst modelValue = defineModel<string>({ default: 'auto' });\nconst themeSemiDarkSidebar = defineModel<boolean>('themeSemiDarkSidebar');\nconst themeSemiDarkHeader = defineModel<boolean>('themeSemiDarkHeader');\n\nconst THEME_PRESET: Array<{ icon: Component; name: ThemeModeType }> = [\n  {\n    icon: Sun,\n    name: 'light',\n  },\n  {\n    icon: MoonStar,\n    name: 'dark',\n  },\n  {\n    icon: SunMoon,\n    name: 'auto',\n  },\n];\n\nfunction activeClass(theme: string): string[] {\n  return theme === modelValue.value ? ['outline-box-active'] : [];\n}\n\nfunction nameView(name: string) {\n  switch (name) {\n    case 'auto': {\n      return $t('preferences.followSystem');\n    }\n    case 'dark': {\n      return $t('preferences.theme.dark');\n    }\n    case 'light': {\n      return $t('preferences.theme.light');\n    }\n  }\n}\n</script>\n\n<template>\n  <div class=\"flex w-full flex-wrap justify-between\">\n    <template v-for=\"theme in THEME_PRESET\" :key=\"theme.name\">\n      <div\n        class=\"flex cursor-pointer flex-col\"\n        @click=\"modelValue = theme.name\"\n      >\n        <div\n          :class=\"activeClass(theme.name)\"\n          class=\"outline-box flex-center py-4\"\n        >\n          <component :is=\"theme.icon\" class=\"mx-9 size-5\" />\n        </div>\n        <div class=\"text-muted-foreground mt-2 text-center text-xs\">\n          {{ nameView(theme.name) }}\n        </div>\n      </div>\n    </template>\n\n    <SwitchItem\n      v-model=\"themeSemiDarkSidebar\"\n      :disabled=\"modelValue === 'dark'\"\n      class=\"mt-6\"\n    >\n      {{ $t('preferences.theme.darkSidebar') }}\n    </SwitchItem>\n    <SwitchItem v-model=\"themeSemiDarkHeader\" :disabled=\"modelValue === 'dark'\">\n      {{ $t('preferences.theme.darkHeader') }}\n    </SwitchItem>\n  </div>\n</template>\n"
  },
  {
    "path": "packages/effects/layouts/src/widgets/preferences/blocks/toggle-item.vue",
    "content": "<script setup lang=\"ts\">\nimport type { SelectOption } from '@vben/types';\n\nimport { ToggleGroup, ToggleGroupItem } from '@vben-core/shadcn-ui';\n\ndefineOptions({\n  name: 'PreferenceToggleItem',\n});\n\nwithDefaults(defineProps<{ disabled?: boolean; items?: SelectOption[] }>(), {\n  disabled: false,\n  items: () => [],\n});\n\nconst modelValue = defineModel<string>();\n</script>\n\n<template>\n  <div\n    :class=\"{\n      'pointer-events-none opacity-50': disabled,\n    }\"\n    class=\"hover:bg-accent flex w-full items-center justify-between rounded-md px-2 py-2\"\n    disabled\n  >\n    <span class=\"text-sm\">\n      <slot></slot>\n    </span>\n    <ToggleGroup\n      v-model=\"modelValue\"\n      class=\"gap-2\"\n      size=\"sm\"\n      type=\"single\"\n      variant=\"outline\"\n    >\n      <template v-for=\"item in items\" :key=\"item.value\">\n        <ToggleGroupItem\n          :value=\"item.value\"\n          class=\"data-[state=on]:bg-primary data-[state=on]:text-primary-foreground h-7 rounded-sm\"\n        >\n          {{ item.label }}\n        </ToggleGroupItem>\n      </template>\n    </ToggleGroup>\n  </div>\n</template>\n"
  },
  {
    "path": "packages/effects/layouts/src/widgets/preferences/icons/content-compact.vue",
    "content": "<template>\n  <svg\n    class=\"custom-radio-image\"\n    fill=\"none\"\n    height=\"66\"\n    width=\"104\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n  >\n    <g>\n      <rect\n        id=\"svg_1\"\n        fill=\"currentColor\"\n        fill-opacity=\"0.02\"\n        height=\"66\"\n        rx=\"4\"\n        stroke=\"null\"\n        width=\"104\"\n        x=\"0.13514\"\n        y=\"0.13514\"\n      />\n      <rect\n        id=\"svg_8\"\n        fill=\"hsl(var(--primary))\"\n        height=\"9.07027\"\n        stroke=\"null\"\n        width=\"104.07934\"\n        x=\"-0.07419\"\n        y=\"-0.05773\"\n      />\n      <rect\n        id=\"svg_3\"\n        fill=\"#e5e5e5\"\n        height=\"2.789\"\n        rx=\"1.395\"\n        stroke=\"null\"\n        width=\"7.52486\"\n        x=\"15.58168\"\n        y=\"3.20832\"\n      />\n      <path\n        id=\"svg_12\"\n        d=\"m98.19822,2.872c0,-0.54338 0.45662,-1 1,-1l1.925,0c0.54338,0 1,0.45662 1,1l0,2.4c0,0.54338 -0.45662,1 -1,1l-1.925,0c-0.54338,0 -1,-0.45662 -1,-1l0,-2.4z\"\n        fill=\"#ffffff\"\n        opacity=\"undefined\"\n        stroke=\"null\"\n      />\n      <rect\n        id=\"svg_13\"\n        fill=\"currentColor\"\n        fill-opacity=\"0.08\"\n        height=\"21.51892\"\n        rx=\"2\"\n        stroke=\"null\"\n        width=\"41.98275\"\n        x=\"45.37589\"\n        y=\"13.53192\"\n      />\n      <path\n        id=\"svg_14\"\n        d=\"m16.4123,15.53192c0,-1.08676 0.74096,-2 1.62271,-2l21.74653,0c0.88175,0 1.62271,0.91324 1.62271,2l0,17.24865c0,1.08676 -0.74096,2 -1.62271,2l-21.74653,0c-0.88175,0 -1.62271,-0.91324 -1.62271,-2l0,-17.24865z\"\n        fill=\"currentColor\"\n        fill-opacity=\"0.08\"\n        opacity=\"undefined\"\n        stroke=\"null\"\n      />\n      <rect\n        id=\"svg_15\"\n        fill=\"currentColor\"\n        fill-opacity=\"0.08\"\n        height=\"21.65405\"\n        rx=\"2\"\n        stroke=\"null\"\n        width=\"71.10636\"\n        x=\"16.54743\"\n        y=\"39.34689\"\n      />\n      <rect\n        id=\"svg_21\"\n        fill=\"#e5e5e5\"\n        height=\"2.789\"\n        rx=\"1.395\"\n        stroke=\"null\"\n        width=\"7.52486\"\n        x=\"28.14924\"\n        y=\"3.07319\"\n      />\n      <rect\n        id=\"svg_22\"\n        fill=\"#e5e5e5\"\n        height=\"2.789\"\n        rx=\"1.395\"\n        stroke=\"null\"\n        width=\"7.52486\"\n        x=\"41.25735\"\n        y=\"3.20832\"\n      />\n      <rect\n        id=\"svg_23\"\n        fill=\"#e5e5e5\"\n        height=\"2.789\"\n        rx=\"1.395\"\n        stroke=\"null\"\n        width=\"7.52486\"\n        x=\"54.23033\"\n        y=\"3.07319\"\n      />\n      <rect\n        id=\"svg_4\"\n        fill=\"#ffffff\"\n        height=\"7.13843\"\n        rx=\"2\"\n        stroke=\"null\"\n        width=\"7.78397\"\n        x=\"1.5327\"\n        y=\"0.881\"\n      />\n    </g>\n  </svg>\n</template>\n"
  },
  {
    "path": "packages/effects/layouts/src/widgets/preferences/icons/full-content.vue",
    "content": "<template>\n  <svg\n    class=\"custom-radio-image\"\n    fill=\"none\"\n    height=\"66\"\n    width=\"104\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n  >\n    <g>\n      <path\n        id=\"svg_1\"\n        d=\"m0.13514,4.13514c0,-2.17352 1.82648,-4 4,-4l96,0c2.17352,0 4,1.82648 4,4l0,58c0,2.17352 -1.82648,4 -4,4l-96,0c-2.17352,0 -4,-1.82648 -4,-4l0,-58z\"\n        fill=\"currentColor\"\n        fill-opacity=\"0.02\"\n        opacity=\"undefined\"\n        stroke=\"null\"\n      />\n      <rect\n        id=\"svg_13\"\n        fill=\"currentColor\"\n        fill-opacity=\"0.08\"\n        height=\"26.57155\"\n        rx=\"2\"\n        stroke=\"null\"\n        width=\"53.18333\"\n        x=\"45.79979\"\n        y=\"3.77232\"\n      />\n      <path\n        id=\"svg_14\"\n        d=\"m4.28142,5.96169c0,-1.37748 1.06465,-2.53502 2.33158,-2.53502l31.2463,0c1.26693,0 2.33158,1.15754 2.33158,2.53502l0,21.86282c0,1.37748 -1.06465,2.53502 -2.33158,2.53502l-31.2463,0c-1.26693,0 -2.33158,-1.15754 -2.33158,-2.53502l0,-21.86282z\"\n        fill=\"currentColor\"\n        fill-opacity=\"0.08\"\n        opacity=\"undefined\"\n        stroke=\"null\"\n      />\n      <rect\n        id=\"svg_15\"\n        fill=\"currentColor\"\n        fill-opacity=\"0.08\"\n        height=\"25.02247\"\n        rx=\"2\"\n        stroke=\"null\"\n        width=\"94.39371\"\n        x=\"4.56735\"\n        y=\"34.92584\"\n      />\n    </g>\n  </svg>\n</template>\n"
  },
  {
    "path": "packages/effects/layouts/src/widgets/preferences/icons/header-mixed-nav.vue",
    "content": "<template>\n  <svg\n    class=\"custom-radio-image\"\n    fill=\"none\"\n    height=\"66\"\n    width=\"104\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n  >\n    <g>\n      <rect\n        id=\"svg_1\"\n        fill=\"currentColor\"\n        fill-opacity=\"0.02\"\n        height=\"66\"\n        rx=\"4\"\n        stroke=\"null\"\n        width=\"104\"\n        x=\"0.13514\"\n        y=\"0.13514\"\n      />\n      <path\n        id=\"svg_2\"\n        d=\"m-3.37838,3.7543a1.93401,4.02457 0 0 1 1.93401,-4.02457l11.3488,0l0,66.40541l-11.3488,0a1.93401,4.02457 0 0 1 -1.93401,-4.02457l0,-58.35627z\"\n        fill=\"hsl(var(--primary))\"\n        stroke=\"null\"\n      />\n      <rect\n        id=\"svg_3\"\n        fill=\"#e5e5e5\"\n        height=\"2.789\"\n        rx=\"1.395\"\n        stroke=\"null\"\n        width=\"5.47439\"\n        x=\"1.64059\"\n        y=\"15.46086\"\n      />\n      <rect\n        id=\"svg_4\"\n        fill=\"#ffffff\"\n        height=\"7.67897\"\n        rx=\"2\"\n        stroke=\"null\"\n        width=\"8.18938\"\n        x=\"0.58676\"\n        y=\"1.42154\"\n      />\n      <rect\n        id=\"svg_8\"\n        fill=\"hsl(var(--primary))\"\n        height=\"9.07027\"\n        rx=\"2\"\n        stroke=\"null\"\n        width=\"75.91967\"\n        x=\"25.38277\"\n        y=\"1.42876\"\n      />\n      <rect\n        id=\"svg_9\"\n        fill=\"#b2b2b2\"\n        height=\"4.4\"\n        rx=\"1\"\n        stroke=\"null\"\n        width=\"3.925\"\n        x=\"27.91529\"\n        y=\"3.69284\"\n      />\n      <rect\n        id=\"svg_10\"\n        fill=\"#b2b2b2\"\n        height=\"4.4\"\n        rx=\"1\"\n        stroke=\"null\"\n        width=\"3.925\"\n        x=\"80.75054\"\n        y=\"3.62876\"\n      />\n      <rect\n        id=\"svg_11\"\n        fill=\"#b2b2b2\"\n        height=\"4.4\"\n        rx=\"1\"\n        stroke=\"null\"\n        width=\"3.925\"\n        x=\"87.78868\"\n        y=\"3.69981\"\n      />\n      <rect\n        id=\"svg_12\"\n        fill=\"#b2b2b2\"\n        height=\"4.4\"\n        rx=\"1\"\n        stroke=\"null\"\n        width=\"3.925\"\n        x=\"94.6847\"\n        y=\"3.62876\"\n      />\n      <rect\n        id=\"svg_13\"\n        fill=\"currentColor\"\n        fill-opacity=\"0.08\"\n        height=\"21.51892\"\n        rx=\"2\"\n        stroke=\"null\"\n        width=\"42.9287\"\n        x=\"58.75427\"\n        y=\"14.613\"\n      />\n      <rect\n        id=\"svg_14\"\n        fill=\"currentColor\"\n        fill-opacity=\"0.08\"\n        height=\"20.97838\"\n        rx=\"2\"\n        stroke=\"null\"\n        width=\"28.36894\"\n        x=\"26.14342\"\n        y=\"14.613\"\n      />\n      <rect\n        id=\"svg_15\"\n        fill=\"currentColor\"\n        fill-opacity=\"0.08\"\n        height=\"21.65405\"\n        rx=\"2\"\n        stroke=\"null\"\n        width=\"75.09493\"\n        x=\"26.34264\"\n        y=\"39.68822\"\n      />\n      <rect\n        id=\"svg_5\"\n        fill=\"#e5e5e5\"\n        height=\"2.789\"\n        rx=\"1.395\"\n        stroke=\"null\"\n        width=\"5.47439\"\n        x=\"1.79832\"\n        y=\"28.39462\"\n      />\n      <rect\n        id=\"svg_6\"\n        fill=\"#e5e5e5\"\n        height=\"2.789\"\n        rx=\"1.395\"\n        stroke=\"null\"\n        width=\"5.47439\"\n        x=\"1.64059\"\n        y=\"41.80156\"\n      />\n      <rect\n        id=\"svg_7\"\n        fill=\"#e5e5e5\"\n        height=\"2.789\"\n        rx=\"1.395\"\n        stroke=\"null\"\n        width=\"5.47439\"\n        x=\"1.64059\"\n        y=\"55.36623\"\n      />\n      <rect\n        id=\"svg_16\"\n        fill=\"currentColor\"\n        fill-opacity=\"0.08\"\n        height=\"65.72065\"\n        stroke=\"null\"\n        width=\"12.49265\"\n        x=\"9.85477\"\n        y=\"-0.02618\"\n      />\n      <rect\n        id=\"svg_21\"\n        fill=\"#e5e5e5\"\n        height=\"2.789\"\n        rx=\"1.395\"\n        stroke=\"null\"\n        width=\"7.52486\"\n        x=\"35.14924\"\n        y=\"4.07319\"\n      />\n      <rect\n        id=\"svg_22\"\n        fill=\"#e5e5e5\"\n        height=\"2.789\"\n        rx=\"1.395\"\n        stroke=\"null\"\n        width=\"7.52486\"\n        x=\"47.25735\"\n        y=\"4.20832\"\n      />\n      <rect\n        id=\"svg_23\"\n        fill=\"#e5e5e5\"\n        height=\"2.789\"\n        rx=\"1.395\"\n        stroke=\"null\"\n        width=\"7.52486\"\n        x=\"59.23033\"\n        y=\"4.07319\"\n      />\n    </g>\n  </svg>\n</template>\n"
  },
  {
    "path": "packages/effects/layouts/src/widgets/preferences/icons/header-nav.vue",
    "content": "<template>\n  <svg\n    class=\"custom-radio-image\"\n    fill=\"none\"\n    height=\"66\"\n    width=\"104\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n  >\n    <g>\n      <rect\n        id=\"svg_1\"\n        fill=\"currentColor\"\n        fill-opacity=\"0.02\"\n        height=\"66\"\n        rx=\"4\"\n        stroke=\"null\"\n        width=\"104\"\n        x=\"0.13514\"\n        y=\"0.13514\"\n      />\n      <rect\n        id=\"svg_8\"\n        fill=\"hsl(var(--primary))\"\n        height=\"9.07027\"\n        stroke=\"null\"\n        width=\"104.07934\"\n        x=\"-0.07419\"\n        y=\"-0.05773\"\n      />\n      <rect\n        id=\"svg_3\"\n        fill=\"#e5e5e5\"\n        height=\"2.789\"\n        rx=\"1.395\"\n        stroke=\"null\"\n        width=\"7.52486\"\n        x=\"15.58168\"\n        y=\"3.20832\"\n      />\n      <path\n        id=\"svg_12\"\n        d=\"m98.19822,2.872c0,-0.54338 0.45662,-1 1,-1l1.925,0c0.54338,0 1,0.45662 1,1l0,2.4c0,0.54338 -0.45662,1 -1,1l-1.925,0c-0.54338,0 -1,-0.45662 -1,-1l0,-2.4z\"\n        fill=\"#ffffff\"\n        opacity=\"undefined\"\n        stroke=\"null\"\n      />\n      <rect\n        id=\"svg_13\"\n        fill=\"currentColor\"\n        fill-opacity=\"0.08\"\n        height=\"21.51892\"\n        rx=\"2\"\n        stroke=\"null\"\n        width=\"53.60438\"\n        x=\"43.484\"\n        y=\"13.66705\"\n      />\n      <path\n        id=\"svg_14\"\n        d=\"m3.43932,15.53192c0,-1.08676 1.03344,-2 2.26323,-2l30.33036,0c1.22979,0 2.26323,0.91324 2.26323,2l0,17.24865c0,1.08676 -1.03344,2 -2.26323,2l-30.33036,0c-1.22979,0 -2.26323,-0.91324 -2.26323,-2l0,-17.24865z\"\n        fill=\"currentColor\"\n        fill-opacity=\"0.08\"\n        opacity=\"undefined\"\n        stroke=\"null\"\n      />\n      <rect\n        id=\"svg_15\"\n        fill=\"currentColor\"\n        fill-opacity=\"0.08\"\n        height=\"21.65405\"\n        rx=\"2\"\n        stroke=\"null\"\n        width=\"95.02528\"\n        x=\"3.30419\"\n        y=\"39.34689\"\n      />\n      <rect\n        id=\"svg_21\"\n        fill=\"#e5e5e5\"\n        height=\"2.789\"\n        rx=\"1.395\"\n        stroke=\"null\"\n        width=\"7.52486\"\n        x=\"28.14924\"\n        y=\"3.07319\"\n      />\n      <rect\n        id=\"svg_22\"\n        fill=\"#e5e5e5\"\n        height=\"2.789\"\n        rx=\"1.395\"\n        stroke=\"null\"\n        width=\"7.52486\"\n        x=\"41.25735\"\n        y=\"3.20832\"\n      />\n      <rect\n        id=\"svg_23\"\n        fill=\"#e5e5e5\"\n        height=\"2.789\"\n        rx=\"1.395\"\n        stroke=\"null\"\n        width=\"7.52486\"\n        x=\"54.23033\"\n        y=\"3.07319\"\n      />\n      <rect\n        id=\"svg_4\"\n        fill=\"#ffffff\"\n        height=\"7.13843\"\n        rx=\"2\"\n        stroke=\"null\"\n        width=\"7.78397\"\n        x=\"1.5327\"\n        y=\"0.881\"\n      />\n    </g>\n  </svg>\n</template>\n"
  },
  {
    "path": "packages/effects/layouts/src/widgets/preferences/icons/header-sidebar-nav.vue",
    "content": "<template>\n  <svg\n    class=\"custom-radio-image\"\n    fill=\"none\"\n    height=\"66\"\n    width=\"104\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n  >\n    <g>\n      <rect\n        id=\"svg_1\"\n        fill=\"currentColor\"\n        fill-opacity=\"0.02\"\n        height=\"66\"\n        rx=\"4\"\n        stroke=\"null\"\n        width=\"104\"\n        x=\"0.13514\"\n        y=\"0.13514\"\n      />\n      <rect\n        id=\"svg_8\"\n        fill=\"currentColor\"\n        fill-opacity=\"0.08\"\n        height=\"9.07027\"\n        stroke=\"null\"\n        width=\"104.07934\"\n        x=\"-0.07419\"\n        y=\"-0.05773\"\n      />\n      <rect\n        id=\"svg_3\"\n        fill=\"#b2b2b2\"\n        height=\"1.689\"\n        rx=\"1.395\"\n        stroke=\"null\"\n        width=\"6.52486\"\n        x=\"10.08168\"\n        y=\"3.50832\"\n      />\n      <rect\n        id=\"svg_10\"\n        fill=\"#b2b2b2\"\n        height=\"4.4\"\n        rx=\"1\"\n        stroke=\"null\"\n        width=\"3.925\"\n        x=\"80.75054\"\n        y=\"2.89362\"\n      />\n      <rect\n        id=\"svg_11\"\n        fill=\"#b2b2b2\"\n        height=\"4.4\"\n        rx=\"1\"\n        stroke=\"null\"\n        width=\"3.925\"\n        x=\"87.58249\"\n        y=\"2.89362\"\n      />\n      <path\n        id=\"svg_12\"\n        d=\"m98.19822,2.872c0,-0.54338 0.45662,-1 1,-1l1.925,0c0.54338,0 1,0.45662 1,1l0,2.4c0,0.54338 -0.45662,1 -1,1l-1.925,0c-0.54338,0 -1,-0.45662 -1,-1l0,-2.4z\"\n        fill=\"#ffffff\"\n        opacity=\"undefined\"\n        stroke=\"null\"\n      />\n      <rect\n        id=\"svg_13\"\n        fill=\"currentColor\"\n        fill-opacity=\"0.08\"\n        height=\"21.51892\"\n        rx=\"2\"\n        stroke=\"null\"\n        width=\"44.13071\"\n        x=\"53.37873\"\n        y=\"13.45652\"\n      />\n      <path\n        id=\"svg_14\"\n        d=\"m19.4393,15.74245c0,-1.08676 0.79001,-2 1.73013,-2l23.18605,0c0.94011,0 1.73013,0.91324 1.73013,2l0,17.24865c0,1.08676 -0.79001,2 -1.73013,2l-23.18605,0c-0.94011,0 -1.73013,-0.91324 -1.73013,-2l0,-17.24865z\"\n        fill=\"currentColor\"\n        fill-opacity=\"0.08\"\n        opacity=\"undefined\"\n        stroke=\"null\"\n      />\n      <rect\n        id=\"svg_15\"\n        fill=\"currentColor\"\n        fill-opacity=\"0.08\"\n        height=\"21.65405\"\n        rx=\"2\"\n        stroke=\"null\"\n        width=\"78.39372\"\n        x=\"19.93575\"\n        y=\"39.34689\"\n      />\n      <rect\n        id=\"svg_21\"\n        fill=\"#e5e5e5\"\n        height=\"2.789\"\n        rx=\"1.395\"\n        stroke=\"null\"\n        width=\"7.52486\"\n        x=\"28.14924\"\n        y=\"3.07319\"\n      />\n      <rect\n        id=\"svg_22\"\n        fill=\"#e5e5e5\"\n        height=\"2.789\"\n        rx=\"1.395\"\n        stroke=\"null\"\n        width=\"7.52486\"\n        x=\"41.25735\"\n        y=\"3.20832\"\n      />\n      <rect\n        id=\"svg_23\"\n        fill=\"#e5e5e5\"\n        height=\"2.789\"\n        rx=\"1.395\"\n        stroke=\"null\"\n        width=\"7.52486\"\n        x=\"54.23033\"\n        y=\"3.07319\"\n      />\n      <rect\n        id=\"svg_4\"\n        fill=\"#ffffff\"\n        height=\"5.13843\"\n        rx=\"2\"\n        stroke=\"null\"\n        width=\"5.78397\"\n        x=\"1.5327\"\n        y=\"1.081\"\n      />\n      <rect\n        id=\"svg_5\"\n        fill=\"hsl(var(--primary))\"\n        height=\"56.81191\"\n        stroke=\"null\"\n        width=\"15.44642\"\n        x=\"-0.06423\"\n        y=\"9.03113\"\n      />\n      <path\n        id=\"svg_2\"\n        d=\"m2.38669,15.38074c0,-0.20384 0.27195,-0.37513 0.59557,-0.37513l7.98149,0c0.32362,0 0.59557,0.17129 0.59557,0.37513l0,3.23525c0,0.20384 -0.27195,0.37513 -0.59557,0.37513l-7.98149,0c-0.32362,0 -0.59557,-0.17129 -0.59557,-0.37513l0,-3.23525z\"\n        fill=\"#fff\"\n        opacity=\"undefined\"\n        stroke=\"null\"\n      />\n      <path\n        id=\"svg_6\"\n        d=\"m2.38669,28.43336c0,-0.20384 0.27195,-0.37513 0.59557,-0.37513l7.98149,0c0.32362,0 0.59557,0.17129 0.59557,0.37513l0,3.23525c0,0.20384 -0.27195,0.37513 -0.59557,0.37513l-7.98149,0c-0.32362,0 -0.59557,-0.17129 -0.59557,-0.37513l0,-3.23525z\"\n        fill=\"#fff\"\n        opacity=\"undefined\"\n        stroke=\"null\"\n      />\n      <path\n        id=\"svg_7\"\n        d=\"m2.17616,41.27545c0,-0.20384 0.27195,-0.37513 0.59557,-0.37513l7.98149,0c0.32362,0 0.59557,0.17129 0.59557,0.37513l0,3.23525c0,0.20384 -0.27195,0.37513 -0.59557,0.37513l-7.98149,0c-0.32362,0 -0.59557,-0.17129 -0.59557,-0.37513l0,-3.23525z\"\n        fill=\"#fff\"\n        opacity=\"undefined\"\n        stroke=\"null\"\n      />\n      <path\n        id=\"svg_9\"\n        d=\"m2.17616,54.32806c0,-0.20384 0.27195,-0.37513 0.59557,-0.37513l7.98149,0c0.32362,0 0.59557,0.17129 0.59557,0.37513l0,3.23525c0,0.20384 -0.27195,0.37513 -0.59557,0.37513l-7.98149,0c-0.32362,0 -0.59557,-0.17129 -0.59557,-0.37513l0,-3.23525z\"\n        fill=\"#fff\"\n        opacity=\"undefined\"\n        stroke=\"null\"\n      />\n    </g>\n  </svg>\n</template>\n"
  },
  {
    "path": "packages/effects/layouts/src/widgets/preferences/icons/index.ts",
    "content": "import HeaderNav from './header-nav.vue';\n\nexport { default as ContentCompact } from './content-compact.vue';\nexport { default as FullContent } from './full-content.vue';\nexport { default as HeaderMixedNav } from './header-mixed-nav.vue';\nexport { default as HeaderSidebarNav } from './header-sidebar-nav.vue';\nexport { default as MixedNav } from './mixed-nav.vue';\nexport { default as SidebarMixedNav } from './sidebar-mixed-nav.vue';\nexport { default as SidebarNav } from './sidebar-nav.vue';\n\nconst ContentWide = HeaderNav;\nexport { ContentWide, HeaderNav };\n"
  },
  {
    "path": "packages/effects/layouts/src/widgets/preferences/icons/mixed-nav.vue",
    "content": "<template>\n  <svg\n    class=\"custom-radio-image\"\n    fill=\"none\"\n    height=\"66\"\n    width=\"104\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n  >\n    <g>\n      <rect\n        id=\"svg_1\"\n        fill=\"currentColor\"\n        fill-opacity=\"0.02\"\n        height=\"66\"\n        rx=\"4\"\n        stroke=\"null\"\n        width=\"104\"\n        x=\"0.13514\"\n        y=\"0.13514\"\n      />\n      <rect\n        id=\"svg_8\"\n        fill=\"hsl(var(--primary))\"\n        height=\"9.07027\"\n        stroke=\"null\"\n        width=\"104.07934\"\n        x=\"-0.07419\"\n        y=\"-0.05773\"\n      />\n      <rect\n        id=\"svg_3\"\n        fill=\"#e5e5e5\"\n        height=\"2.789\"\n        rx=\"1.395\"\n        stroke=\"null\"\n        width=\"7.52486\"\n        x=\"15.58168\"\n        y=\"3.20832\"\n      />\n      <path\n        id=\"svg_12\"\n        d=\"m98.19822,2.872c0,-0.54338 0.45662,-1 1,-1l1.925,0c0.54338,0 1,0.45662 1,1l0,2.4c0,0.54338 -0.45662,1 -1,1l-1.925,0c-0.54338,0 -1,-0.45662 -1,-1l0,-2.4z\"\n        fill=\"#ffffff\"\n        opacity=\"undefined\"\n        stroke=\"null\"\n      />\n      <rect\n        id=\"svg_13\"\n        fill=\"currentColor\"\n        fill-opacity=\"0.08\"\n        height=\"21.51892\"\n        rx=\"2\"\n        stroke=\"null\"\n        width=\"44.13071\"\n        x=\"53.37873\"\n        y=\"13.45652\"\n      />\n      <path\n        id=\"svg_14\"\n        d=\"m19.4393,15.74245c0,-1.08676 0.79001,-2 1.73013,-2l23.18605,0c0.94011,0 1.73013,0.91324 1.73013,2l0,17.24865c0,1.08676 -0.79001,2 -1.73013,2l-23.18605,0c-0.94011,0 -1.73013,-0.91324 -1.73013,-2l0,-17.24865z\"\n        fill=\"currentColor\"\n        fill-opacity=\"0.08\"\n        opacity=\"undefined\"\n        stroke=\"null\"\n      />\n      <rect\n        id=\"svg_15\"\n        fill=\"currentColor\"\n        fill-opacity=\"0.08\"\n        height=\"21.65405\"\n        rx=\"2\"\n        stroke=\"null\"\n        width=\"78.39372\"\n        x=\"19.93575\"\n        y=\"39.34689\"\n      />\n      <rect\n        id=\"svg_21\"\n        fill=\"#e5e5e5\"\n        height=\"2.789\"\n        rx=\"1.395\"\n        stroke=\"null\"\n        width=\"7.52486\"\n        x=\"28.14924\"\n        y=\"3.07319\"\n      />\n      <rect\n        id=\"svg_22\"\n        fill=\"#e5e5e5\"\n        height=\"2.789\"\n        rx=\"1.395\"\n        stroke=\"null\"\n        width=\"7.52486\"\n        x=\"41.25735\"\n        y=\"3.20832\"\n      />\n      <rect\n        id=\"svg_23\"\n        fill=\"#e5e5e5\"\n        height=\"2.789\"\n        rx=\"1.395\"\n        stroke=\"null\"\n        width=\"7.52486\"\n        x=\"54.23033\"\n        y=\"3.07319\"\n      />\n      <rect\n        id=\"svg_4\"\n        fill=\"#ffffff\"\n        height=\"7.13843\"\n        rx=\"2\"\n        stroke=\"null\"\n        width=\"7.78397\"\n        x=\"1.5327\"\n        y=\"0.881\"\n      />\n      <rect\n        id=\"svg_5\"\n        fill=\"currentColor\"\n        fill-opacity=\"0.08\"\n        height=\"56.81191\"\n        stroke=\"null\"\n        width=\"15.44642\"\n        x=\"-0.06423\"\n        y=\"9.03113\"\n      />\n      <path\n        id=\"svg_2\"\n        d=\"m2.38669,15.38074c0,-0.20384 0.27195,-0.37513 0.59557,-0.37513l7.98149,0c0.32362,0 0.59557,0.17129 0.59557,0.37513l0,3.23525c0,0.20384 -0.27195,0.37513 -0.59557,0.37513l-7.98149,0c-0.32362,0 -0.59557,-0.17129 -0.59557,-0.37513l0,-3.23525z\"\n        fill=\"currentColor\"\n        fill-opacity=\"0.08\"\n        opacity=\"undefined\"\n        stroke=\"null\"\n      />\n      <path\n        id=\"svg_6\"\n        d=\"m2.38669,28.43336c0,-0.20384 0.27195,-0.37513 0.59557,-0.37513l7.98149,0c0.32362,0 0.59557,0.17129 0.59557,0.37513l0,3.23525c0,0.20384 -0.27195,0.37513 -0.59557,0.37513l-7.98149,0c-0.32362,0 -0.59557,-0.17129 -0.59557,-0.37513l0,-3.23525z\"\n        fill=\"currentColor\"\n        fill-opacity=\"0.08\"\n        opacity=\"undefined\"\n        stroke=\"null\"\n      />\n      <path\n        id=\"svg_7\"\n        d=\"m2.17616,41.27545c0,-0.20384 0.27195,-0.37513 0.59557,-0.37513l7.98149,0c0.32362,0 0.59557,0.17129 0.59557,0.37513l0,3.23525c0,0.20384 -0.27195,0.37513 -0.59557,0.37513l-7.98149,0c-0.32362,0 -0.59557,-0.17129 -0.59557,-0.37513l0,-3.23525z\"\n        fill=\"currentColor\"\n        fill-opacity=\"0.08\"\n        opacity=\"undefined\"\n        stroke=\"null\"\n      />\n      <path\n        id=\"svg_9\"\n        d=\"m2.17616,54.32806c0,-0.20384 0.27195,-0.37513 0.59557,-0.37513l7.98149,0c0.32362,0 0.59557,0.17129 0.59557,0.37513l0,3.23525c0,0.20384 -0.27195,0.37513 -0.59557,0.37513l-7.98149,0c-0.32362,0 -0.59557,-0.17129 -0.59557,-0.37513l0,-3.23525z\"\n        fill=\"currentColor\"\n        fill-opacity=\"0.08\"\n        opacity=\"undefined\"\n        stroke=\"null\"\n      />\n    </g>\n  </svg>\n</template>\n"
  },
  {
    "path": "packages/effects/layouts/src/widgets/preferences/icons/setting.vue",
    "content": "<template>\n  <svg\n    height=\"1em\"\n    viewBox=\"0 0 24 24\"\n    width=\"1em\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n  >\n    <path\n      d=\"M19.9 12.66a1 1 0 0 1 0-1.32l1.28-1.44a1 1 0 0 0 .12-1.17l-2-3.46a1 1 0 0 0-1.07-.48l-1.88.38a1 1 0 0 1-1.15-.66l-.61-1.83a1 1 0 0 0-.95-.68h-4a1 1 0 0 0-1 .68l-.56 1.83a1 1 0 0 1-1.15.66L5 4.79a1 1 0 0 0-1 .48L2 8.73a1 1 0 0 0 .1 1.17l1.27 1.44a1 1 0 0 1 0 1.32L2.1 14.1a1 1 0 0 0-.1 1.17l2 3.46a1 1 0 0 0 1.07.48l1.88-.38a1 1 0 0 1 1.15.66l.61 1.83a1 1 0 0 0 1 .68h4a1 1 0 0 0 .95-.68l.61-1.83a1 1 0 0 1 1.15-.66l1.88.38a1 1 0 0 0 1.07-.48l2-3.46a1 1 0 0 0-.12-1.17ZM18.41 14l.8.9l-1.28 2.22l-1.18-.24a3 3 0 0 0-3.45 2L12.92 20h-2.56L10 18.86a3 3 0 0 0-3.45-2l-1.18.24l-1.3-2.21l.8-.9a3 3 0 0 0 0-4l-.8-.9l1.28-2.2l1.18.24a3 3 0 0 0 3.45-2L10.36 4h2.56l.38 1.14a3 3 0 0 0 3.45 2l1.18-.24l1.28 2.22l-.8.9a3 3 0 0 0 0 3.98m-6.77-6a4 4 0 1 0 4 4a4 4 0 0 0-4-4m0 6a2 2 0 1 1 2-2a2 2 0 0 1-2 2\"\n    />\n  </svg>\n</template>\n"
  },
  {
    "path": "packages/effects/layouts/src/widgets/preferences/icons/sidebar-mixed-nav.vue",
    "content": "<template>\n  <svg\n    class=\"custom-radio-image\"\n    fill=\"none\"\n    height=\"66\"\n    width=\"104\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n  >\n    <g>\n      <rect\n        id=\"svg_1\"\n        fill=\"currentColor\"\n        fill-opacity=\"0.02\"\n        height=\"66\"\n        rx=\"4\"\n        stroke=\"null\"\n        width=\"104\"\n        x=\"0.13514\"\n        y=\"0.13514\"\n      />\n      <path\n        id=\"svg_2\"\n        d=\"m-3.37838,3.7543a1.93401,4.02457 0 0 1 1.93401,-4.02457l11.3488,0l0,66.40541l-11.3488,0a1.93401,4.02457 0 0 1 -1.93401,-4.02457l0,-58.35627z\"\n        fill=\"hsl(var(--primary))\"\n        stroke=\"null\"\n      />\n      <rect\n        id=\"svg_3\"\n        fill=\"#e5e5e5\"\n        height=\"2.789\"\n        rx=\"1.395\"\n        stroke=\"null\"\n        width=\"5.47439\"\n        x=\"1.64059\"\n        y=\"15.46086\"\n      />\n      <rect\n        id=\"svg_4\"\n        fill=\"#ffffff\"\n        height=\"7.67897\"\n        rx=\"2\"\n        stroke=\"null\"\n        width=\"8.18938\"\n        x=\"0.58676\"\n        y=\"1.42154\"\n      />\n      <rect\n        id=\"svg_8\"\n        fill=\"currentColor\"\n        fill-opacity=\"0.08\"\n        height=\"9.07027\"\n        rx=\"2\"\n        stroke=\"null\"\n        width=\"75.91967\"\n        x=\"25.38277\"\n        y=\"1.42876\"\n      />\n      <rect\n        id=\"svg_9\"\n        fill=\"#b2b2b2\"\n        height=\"4.4\"\n        rx=\"1\"\n        stroke=\"null\"\n        width=\"3.925\"\n        x=\"27.91529\"\n        y=\"3.69284\"\n      />\n      <rect\n        id=\"svg_10\"\n        fill=\"#b2b2b2\"\n        height=\"4.4\"\n        rx=\"1\"\n        stroke=\"null\"\n        width=\"3.925\"\n        x=\"80.75054\"\n        y=\"3.62876\"\n      />\n      <rect\n        id=\"svg_11\"\n        fill=\"#b2b2b2\"\n        height=\"4.4\"\n        rx=\"1\"\n        stroke=\"null\"\n        width=\"3.925\"\n        x=\"87.78868\"\n        y=\"3.69981\"\n      />\n      <rect\n        id=\"svg_12\"\n        fill=\"#b2b2b2\"\n        height=\"4.4\"\n        rx=\"1\"\n        stroke=\"null\"\n        width=\"3.925\"\n        x=\"94.6847\"\n        y=\"3.62876\"\n      />\n      <rect\n        id=\"svg_13\"\n        fill=\"currentColor\"\n        fill-opacity=\"0.08\"\n        height=\"21.51892\"\n        rx=\"2\"\n        stroke=\"null\"\n        width=\"42.9287\"\n        x=\"58.75427\"\n        y=\"14.613\"\n      />\n      <rect\n        id=\"svg_14\"\n        fill=\"currentColor\"\n        fill-opacity=\"0.08\"\n        height=\"20.97838\"\n        rx=\"2\"\n        stroke=\"null\"\n        width=\"28.36894\"\n        x=\"26.14342\"\n        y=\"14.613\"\n      />\n      <rect\n        id=\"svg_15\"\n        fill=\"currentColor\"\n        fill-opacity=\"0.08\"\n        height=\"21.65405\"\n        rx=\"2\"\n        stroke=\"null\"\n        width=\"75.09493\"\n        x=\"26.34264\"\n        y=\"39.68822\"\n      />\n      <rect\n        id=\"svg_5\"\n        fill=\"#e5e5e5\"\n        height=\"2.789\"\n        rx=\"1.395\"\n        stroke=\"null\"\n        width=\"5.47439\"\n        x=\"1.79832\"\n        y=\"28.39462\"\n      />\n      <rect\n        id=\"svg_6\"\n        fill=\"#e5e5e5\"\n        height=\"2.789\"\n        rx=\"1.395\"\n        stroke=\"null\"\n        width=\"5.47439\"\n        x=\"1.64059\"\n        y=\"41.80156\"\n      />\n      <rect\n        id=\"svg_7\"\n        fill=\"#e5e5e5\"\n        height=\"2.789\"\n        rx=\"1.395\"\n        stroke=\"null\"\n        width=\"5.47439\"\n        x=\"1.64059\"\n        y=\"55.36623\"\n      />\n      <rect\n        id=\"svg_16\"\n        fill=\"currentColor\"\n        fill-opacity=\"0.08\"\n        height=\"65.72065\"\n        stroke=\"null\"\n        width=\"12.49265\"\n        x=\"9.85477\"\n        y=\"-0.02618\"\n      />\n    </g>\n  </svg>\n</template>\n"
  },
  {
    "path": "packages/effects/layouts/src/widgets/preferences/icons/sidebar-nav.vue",
    "content": "<template>\n  <svg\n    class=\"custom-radio-image\"\n    fill=\"none\"\n    height=\"66\"\n    width=\"104\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n  >\n    <g>\n      <rect\n        id=\"svg_1\"\n        fill=\"currentColor\"\n        fill-opacity=\"0.02\"\n        height=\"66\"\n        rx=\"4\"\n        stroke=\"null\"\n        width=\"104\"\n      />\n      <path\n        id=\"svg_2\"\n        d=\"m-3.37838,3.61916a4.4919,4.02457 0 0 1 4.4919,-4.02457l26.35848,0l0,66.40541l-26.35848,0a4.4919,4.02457 0 0 1 -4.4919,-4.02457l0,-58.35627z\"\n        fill=\"hsl(var(--primary))\"\n        stroke=\"null\"\n      />\n      <rect\n        id=\"svg_3\"\n        fill=\"#e5e5e5\"\n        height=\"2.789\"\n        rx=\"1.395\"\n        width=\"17.66\"\n        x=\"4.906\"\n        y=\"23.884\"\n      />\n      <rect\n        id=\"svg_4\"\n        fill=\"#ffffff\"\n        height=\"9.706\"\n        rx=\"2\"\n        width=\"9.811\"\n        x=\"8.83\"\n        y=\"5.881\"\n      />\n      <path\n        id=\"svg_5\"\n        d=\"m4.906,35.833c0,-0.75801 0.63699,-1.395 1.395,-1.395l14.87,0c0.75801,0 1.395,0.63699 1.395,1.395l0,-0.001c0,0.75801 -0.63699,1.395 -1.395,1.395l-14.87,0c-0.75801,0 -1.395,-0.63699 -1.395,-1.395l0,0.001z\"\n        fill=\"#ffffff\"\n        opacity=\"undefined\"\n      />\n      <rect\n        id=\"svg_6\"\n        fill=\"#ffffff\"\n        height=\"2.789\"\n        rx=\"1.395\"\n        width=\"17.66\"\n        x=\"4.906\"\n        y=\"44.992\"\n      />\n      <rect\n        id=\"svg_7\"\n        fill=\"#ffffff\"\n        height=\"2.789\"\n        rx=\"1.395\"\n        width=\"17.66\"\n        x=\"4.906\"\n        y=\"55.546\"\n      />\n      <rect\n        id=\"svg_8\"\n        fill=\"currentColor\"\n        fill-opacity=\"0.08\"\n        height=\"9.07027\"\n        rx=\"2\"\n        stroke=\"null\"\n        width=\"73.53879\"\n        x=\"28.97986\"\n        y=\"1.42876\"\n      />\n      <rect\n        id=\"svg_9\"\n        fill=\"#b2b2b2\"\n        height=\"4.4\"\n        rx=\"1\"\n        stroke=\"null\"\n        width=\"3.925\"\n        x=\"32.039\"\n        y=\"3.89903\"\n      />\n      <rect\n        id=\"svg_10\"\n        fill=\"#b2b2b2\"\n        height=\"4.4\"\n        rx=\"1\"\n        stroke=\"null\"\n        width=\"3.925\"\n        x=\"80.75054\"\n        y=\"3.62876\"\n      />\n      <rect\n        id=\"svg_11\"\n        fill=\"#b2b2b2\"\n        height=\"4.4\"\n        rx=\"1\"\n        stroke=\"null\"\n        width=\"3.925\"\n        x=\"87.58249\"\n        y=\"3.49362\"\n      />\n      <rect\n        id=\"svg_12\"\n        fill=\"#b2b2b2\"\n        height=\"4.4\"\n        rx=\"1\"\n        stroke=\"null\"\n        width=\"3.925\"\n        x=\"94.6847\"\n        y=\"3.62876\"\n      />\n      <rect\n        id=\"svg_13\"\n        fill=\"currentColor\"\n        fill-opacity=\"0.08\"\n        height=\"21.51892\"\n        rx=\"2\"\n        stroke=\"null\"\n        width=\"45.63141\"\n        x=\"56.05157\"\n        y=\"14.613\"\n      />\n      <rect\n        id=\"svg_14\"\n        fill=\"currentColor\"\n        fill-opacity=\"0.08\"\n        height=\"20.97838\"\n        rx=\"2\"\n        stroke=\"null\"\n        width=\"22.82978\"\n        x=\"29.38527\"\n        y=\"14.613\"\n      />\n      <rect\n        id=\"svg_15\"\n        fill=\"currentColor\"\n        fill-opacity=\"0.08\"\n        height=\"21.65405\"\n        rx=\"2\"\n        stroke=\"null\"\n        width=\"72.45771\"\n        x=\"28.97986\"\n        y=\"39.48203\"\n      />\n    </g>\n  </svg>\n</template>\n"
  },
  {
    "path": "packages/effects/layouts/src/widgets/preferences/index.ts",
    "content": "export { default as Preferences } from './preferences.vue';\nexport { default as PreferencesButton } from './preferences-button.vue';\nexport * from './use-open-preferences';\n"
  },
  {
    "path": "packages/effects/layouts/src/widgets/preferences/preferences-button.vue",
    "content": "<script lang=\"ts\" setup>\nimport { Settings } from '@vben/icons';\nimport { VbenIconButton } from '@vben-core/shadcn-ui';\n\nimport Preferences from './preferences.vue';\n\nconst emit = defineEmits<{ clearPreferencesAndLogout: [] }>();\n\nfunction clearPreferencesAndLogout() {\n  emit('clearPreferencesAndLogout');\n}\n</script>\n<template>\n  <Preferences @clear-preferences-and-logout=\"clearPreferencesAndLogout\">\n    <VbenIconButton class=\"hover:animate-[shrink_0.3s_ease-in-out]\">\n      <Settings class=\"text-foreground size-4\" />\n    </VbenIconButton>\n  </Preferences>\n</template>\n"
  },
  {
    "path": "packages/effects/layouts/src/widgets/preferences/preferences-drawer.vue",
    "content": "<script setup lang=\"ts\">\nimport type { SupportedLanguagesType } from '@vben/locales';\nimport type {\n  BreadcrumbStyleType,\n  BuiltinThemeType,\n  ContentCompactType,\n  LayoutHeaderMenuAlignType,\n  LayoutHeaderModeType,\n  LayoutType,\n  NavigationStyleType,\n  PreferencesButtonPositionType,\n  ThemeModeType,\n} from '@vben/types';\n\nimport type { SegmentedItem } from '@vben-core/shadcn-ui';\n\nimport { computed, ref } from 'vue';\n\nimport { Copy, Pin, PinOff, RotateCw } from '@vben/icons';\nimport { $t, loadLocaleMessages } from '@vben/locales';\nimport {\n  clearPreferencesCache,\n  preferences,\n  resetPreferences,\n  usePreferences,\n} from '@vben/preferences';\n\nimport { useVbenDrawer } from '@vben-core/popup-ui';\nimport {\n  VbenButton,\n  VbenIconButton,\n  VbenSegmented,\n} from '@vben-core/shadcn-ui';\nimport { globalShareState } from '@vben-core/shared/global-state';\n\nimport { useClipboard } from '@vueuse/core';\n\nimport {\n  Animation,\n  Block,\n  Breadcrumb,\n  BuiltinTheme,\n  ColorMode,\n  Content,\n  Copyright,\n  Footer,\n  General,\n  GlobalShortcutKeys,\n  Header,\n  Layout,\n  Navigation,\n  Radius,\n  Sidebar,\n  Tabbar,\n  Theme,\n  Widget,\n} from './blocks';\n\nconst emit = defineEmits<{ clearPreferencesAndLogout: [] }>();\n\nconst message = globalShareState.getMessage();\n\nconst appLocale = defineModel<SupportedLanguagesType>('appLocale');\nconst appDynamicTitle = defineModel<boolean>('appDynamicTitle');\nconst appLayout = defineModel<LayoutType>('appLayout');\nconst appColorGrayMode = defineModel<boolean>('appColorGrayMode');\nconst appColorWeakMode = defineModel<boolean>('appColorWeakMode');\nconst appContentCompact = defineModel<ContentCompactType>('appContentCompact');\nconst appWatermark = defineModel<boolean>('appWatermark');\nconst appWatermarkContent = defineModel<string>('appWatermarkContent');\nconst appEnableCheckUpdates = defineModel<boolean>('appEnableCheckUpdates');\nconst appEnableStickyPreferencesNavigationBar = defineModel<boolean>(\n  'appEnableStickyPreferencesNavigationBar',\n);\nconst appPreferencesButtonPosition = defineModel<PreferencesButtonPositionType>(\n  'appPreferencesButtonPosition',\n);\n\nconst transitionProgress = defineModel<boolean>('transitionProgress');\nconst transitionName = defineModel<string>('transitionName');\nconst transitionLoading = defineModel<boolean>('transitionLoading');\nconst transitionEnable = defineModel<boolean>('transitionEnable');\n\nconst themeColorPrimary = defineModel<string>('themeColorPrimary');\nconst themeBuiltinType = defineModel<BuiltinThemeType>('themeBuiltinType');\nconst themeMode = defineModel<ThemeModeType>('themeMode');\nconst themeRadius = defineModel<string>('themeRadius');\nconst themeSemiDarkSidebar = defineModel<boolean>('themeSemiDarkSidebar');\nconst themeSemiDarkHeader = defineModel<boolean>('themeSemiDarkHeader');\n\nconst sidebarEnable = defineModel<boolean>('sidebarEnable');\nconst sidebarWidth = defineModel<number>('sidebarWidth');\nconst sidebarCollapsed = defineModel<boolean>('sidebarCollapsed');\nconst sidebarCollapsedShowTitle = defineModel<boolean>(\n  'sidebarCollapsedShowTitle',\n);\nconst sidebarAutoActivateChild = defineModel<boolean>(\n  'sidebarAutoActivateChild',\n);\nconst sidebarExpandOnHover = defineModel<boolean>('sidebarExpandOnHover');\nconst sidebarCollapsedButton = defineModel<boolean>('sidebarCollapsedButton');\nconst sidebarFixedButton = defineModel<boolean>('sidebarFixedButton');\nconst headerEnable = defineModel<boolean>('headerEnable');\nconst headerMode = defineModel<LayoutHeaderModeType>('headerMode');\nconst headerMenuAlign =\n  defineModel<LayoutHeaderMenuAlignType>('headerMenuAlign');\n\nconst breadcrumbEnable = defineModel<boolean>('breadcrumbEnable');\nconst breadcrumbShowIcon = defineModel<boolean>('breadcrumbShowIcon');\nconst breadcrumbShowHome = defineModel<boolean>('breadcrumbShowHome');\nconst breadcrumbStyleType = defineModel<BreadcrumbStyleType>(\n  'breadcrumbStyleType',\n);\nconst breadcrumbHideOnlyOne = defineModel<boolean>('breadcrumbHideOnlyOne');\n\nconst tabbarEnable = defineModel<boolean>('tabbarEnable');\nconst tabbarShowIcon = defineModel<boolean>('tabbarShowIcon');\nconst tabbarShowMore = defineModel<boolean>('tabbarShowMore');\nconst tabbarShowMaximize = defineModel<boolean>('tabbarShowMaximize');\nconst tabbarPersist = defineModel<boolean>('tabbarPersist');\nconst tabbarDraggable = defineModel<boolean>('tabbarDraggable');\nconst tabbarWheelable = defineModel<boolean>('tabbarWheelable');\nconst tabbarStyleType = defineModel<string>('tabbarStyleType');\nconst tabbarMaxCount = defineModel<number>('tabbarMaxCount');\nconst tabbarMiddleClickToClose = defineModel<boolean>(\n  'tabbarMiddleClickToClose',\n);\n\nconst navigationStyleType = defineModel<NavigationStyleType>(\n  'navigationStyleType',\n);\nconst navigationSplit = defineModel<boolean>('navigationSplit');\nconst navigationAccordion = defineModel<boolean>('navigationAccordion');\n\n// const logoVisible = defineModel<boolean>('logoVisible');\n\nconst footerEnable = defineModel<boolean>('footerEnable');\nconst footerFixed = defineModel<boolean>('footerFixed');\n\nconst copyrightSettingShow = defineModel<boolean>('copyrightSettingShow');\nconst copyrightEnable = defineModel<boolean>('copyrightEnable');\nconst copyrightCompanyName = defineModel<string>('copyrightCompanyName');\nconst copyrightCompanySiteLink = defineModel<string>(\n  'copyrightCompanySiteLink',\n);\nconst copyrightDate = defineModel<string>('copyrightDate');\nconst copyrightIcp = defineModel<string>('copyrightIcp');\nconst copyrightIcpLink = defineModel<string>('copyrightIcpLink');\n\nconst shortcutKeysEnable = defineModel<boolean>('shortcutKeysEnable');\nconst shortcutKeysGlobalSearch = defineModel<boolean>(\n  'shortcutKeysGlobalSearch',\n);\nconst shortcutKeysGlobalLogout = defineModel<boolean>(\n  'shortcutKeysGlobalLogout',\n);\n\nconst shortcutKeysGlobalLockScreen = defineModel<boolean>(\n  'shortcutKeysGlobalLockScreen',\n);\n\nconst widgetGlobalSearch = defineModel<boolean>('widgetGlobalSearch');\nconst widgetFullscreen = defineModel<boolean>('widgetFullscreen');\nconst widgetLanguageToggle = defineModel<boolean>('widgetLanguageToggle');\nconst widgetNotification = defineModel<boolean>('widgetNotification');\nconst widgetThemeToggle = defineModel<boolean>('widgetThemeToggle');\nconst widgetSidebarToggle = defineModel<boolean>('widgetSidebarToggle');\nconst widgetLockScreen = defineModel<boolean>('widgetLockScreen');\nconst widgetRefresh = defineModel<boolean>('widgetRefresh');\n\nconst {\n  diffPreference,\n  isDark,\n  isFullContent,\n  isHeaderNav,\n  isHeaderSidebarNav,\n  isMixedNav,\n  isSideMixedNav,\n  isSideMode,\n  isSideNav,\n} = usePreferences();\nconst { copy } = useClipboard({ legacy: true });\n\nconst [Drawer] = useVbenDrawer();\n\nconst activeTab = ref('appearance');\n\nconst tabs = computed((): SegmentedItem[] => {\n  return [\n    {\n      label: $t('preferences.appearance'),\n      value: 'appearance',\n    },\n    {\n      label: $t('preferences.layout'),\n      value: 'layout',\n    },\n    {\n      label: $t('preferences.shortcutKeys.title'),\n      value: 'shortcutKey',\n    },\n    {\n      label: $t('preferences.general'),\n      value: 'general',\n    },\n  ];\n});\n\nconst showBreadcrumbConfig = computed(() => {\n  return (\n    !isFullContent.value &&\n    !isMixedNav.value &&\n    !isHeaderNav.value &&\n    preferences.header.enable\n  );\n});\n\nasync function handleCopy() {\n  await copy(JSON.stringify(diffPreference.value, null, 2));\n\n  message.copyPreferencesSuccess?.(\n    $t('preferences.copyPreferencesSuccessTitle'),\n    $t('preferences.copyPreferencesSuccess'),\n  );\n}\n\nasync function handleClearCache() {\n  resetPreferences();\n  clearPreferencesCache();\n  emit('clearPreferencesAndLogout');\n}\n\nasync function handleReset() {\n  if (!diffPreference.value) {\n    return;\n  }\n  resetPreferences();\n  await loadLocaleMessages(preferences.app.locale);\n}\n</script>\n\n<template>\n  <div>\n    <Drawer\n      :description=\"$t('preferences.subtitle')\"\n      :title=\"$t('preferences.title')\"\n      class=\"!border-0 sm:max-w-sm\"\n    >\n      <template #extra>\n        <div class=\"flex items-center\">\n          <VbenIconButton\n            :disabled=\"!diffPreference\"\n            :tooltip=\"$t('preferences.resetTip')\"\n            class=\"relative\"\n            @click=\"handleReset\"\n          >\n            <span\n              v-if=\"diffPreference\"\n              class=\"bg-primary absolute right-0.5 top-0.5 h-2 w-2 rounded\"\n            ></span>\n            <RotateCw class=\"size-4\" />\n          </VbenIconButton>\n          <VbenIconButton\n            :tooltip=\"\n              appEnableStickyPreferencesNavigationBar\n                ? $t('preferences.disableStickyPreferencesNavigationBar')\n                : $t('preferences.enableStickyPreferencesNavigationBar')\n            \"\n            class=\"relative\"\n            @click=\"\n              () =>\n                (appEnableStickyPreferencesNavigationBar =\n                  !appEnableStickyPreferencesNavigationBar)\n            \"\n          >\n            <PinOff\n              v-if=\"appEnableStickyPreferencesNavigationBar\"\n              class=\"size-4\"\n            />\n            <Pin v-else class=\"size-4\" />\n          </VbenIconButton>\n        </div>\n      </template>\n\n      <div>\n        <VbenSegmented\n          v-model=\"activeTab\"\n          :tabs=\"tabs\"\n          :class=\"{\n            'sticky-tabs-header': appEnableStickyPreferencesNavigationBar,\n          }\"\n        >\n          <template #general>\n            <Block :title=\"$t('preferences.general')\">\n              <General\n                v-model:app-dynamic-title=\"appDynamicTitle\"\n                v-model:app-enable-check-updates=\"appEnableCheckUpdates\"\n                v-model:app-locale=\"appLocale\"\n                v-model:app-watermark=\"appWatermark\"\n                v-model:app-watermark-content=\"appWatermarkContent\"\n              />\n            </Block>\n\n            <Block :title=\"$t('preferences.animation.title')\">\n              <Animation\n                v-model:transition-enable=\"transitionEnable\"\n                v-model:transition-loading=\"transitionLoading\"\n                v-model:transition-name=\"transitionName\"\n                v-model:transition-progress=\"transitionProgress\"\n              />\n            </Block>\n          </template>\n          <template #appearance>\n            <Block :title=\"$t('preferences.theme.title')\">\n              <Theme\n                v-model=\"themeMode\"\n                v-model:theme-semi-dark-header=\"themeSemiDarkHeader\"\n                v-model:theme-semi-dark-sidebar=\"themeSemiDarkSidebar\"\n              />\n            </Block>\n            <Block :title=\"$t('preferences.theme.builtin.title')\">\n              <BuiltinTheme\n                v-model=\"themeBuiltinType\"\n                v-model:theme-color-primary=\"themeColorPrimary\"\n                :is-dark=\"isDark\"\n              />\n            </Block>\n            <Block :title=\"$t('preferences.theme.radius')\">\n              <Radius v-model=\"themeRadius\" />\n            </Block>\n            <Block :title=\"$t('preferences.other')\">\n              <ColorMode\n                v-model:app-color-gray-mode=\"appColorGrayMode\"\n                v-model:app-color-weak-mode=\"appColorWeakMode\"\n              />\n            </Block>\n          </template>\n          <template #layout>\n            <Block :title=\"$t('preferences.layout')\">\n              <Layout v-model=\"appLayout\" />\n            </Block>\n            <Block :title=\"$t('preferences.content')\">\n              <Content v-model=\"appContentCompact\" />\n            </Block>\n\n            <Block :title=\"$t('preferences.sidebar.title')\">\n              <Sidebar\n                v-model:sidebar-auto-activate-child=\"sidebarAutoActivateChild\"\n                v-model:sidebar-collapsed=\"sidebarCollapsed\"\n                v-model:sidebar-collapsed-show-title=\"sidebarCollapsedShowTitle\"\n                v-model:sidebar-enable=\"sidebarEnable\"\n                v-model:sidebar-expand-on-hover=\"sidebarExpandOnHover\"\n                v-model:sidebar-width=\"sidebarWidth\"\n                v-model:sidebar-collapsed-button=\"sidebarCollapsedButton\"\n                v-model:sidebar-fixed-button=\"sidebarFixedButton\"\n                :current-layout=\"appLayout\"\n                :disabled=\"!isSideMode\"\n              />\n            </Block>\n\n            <Block :title=\"$t('preferences.header.title')\">\n              <Header\n                v-model:header-enable=\"headerEnable\"\n                v-model:header-menu-align=\"headerMenuAlign\"\n                v-model:header-mode=\"headerMode\"\n                :disabled=\"isFullContent\"\n              />\n            </Block>\n\n            <Block :title=\"$t('preferences.navigationMenu.title')\">\n              <Navigation\n                v-model:navigation-accordion=\"navigationAccordion\"\n                v-model:navigation-split=\"navigationSplit\"\n                v-model:navigation-style-type=\"navigationStyleType\"\n                :disabled=\"isFullContent\"\n                :disabled-navigation-split=\"!isMixedNav\"\n              />\n            </Block>\n\n            <Block :title=\"$t('preferences.breadcrumb.title')\">\n              <Breadcrumb\n                v-model:breadcrumb-enable=\"breadcrumbEnable\"\n                v-model:breadcrumb-hide-only-one=\"breadcrumbHideOnlyOne\"\n                v-model:breadcrumb-show-home=\"breadcrumbShowHome\"\n                v-model:breadcrumb-show-icon=\"breadcrumbShowIcon\"\n                v-model:breadcrumb-style-type=\"breadcrumbStyleType\"\n                :disabled=\"\n                  !showBreadcrumbConfig ||\n                  !(isSideNav || isSideMixedNav || isHeaderSidebarNav)\n                \"\n              />\n            </Block>\n            <Block :title=\"$t('preferences.tabbar.title')\">\n              <Tabbar\n                v-model:tabbar-draggable=\"tabbarDraggable\"\n                v-model:tabbar-enable=\"tabbarEnable\"\n                v-model:tabbar-persist=\"tabbarPersist\"\n                v-model:tabbar-show-icon=\"tabbarShowIcon\"\n                v-model:tabbar-show-maximize=\"tabbarShowMaximize\"\n                v-model:tabbar-show-more=\"tabbarShowMore\"\n                v-model:tabbar-style-type=\"tabbarStyleType\"\n                v-model:tabbar-wheelable=\"tabbarWheelable\"\n                v-model:tabbar-max-count=\"tabbarMaxCount\"\n                v-model:tabbar-middle-click-to-close=\"tabbarMiddleClickToClose\"\n              />\n            </Block>\n            <Block :title=\"$t('preferences.widget.title')\">\n              <Widget\n                v-model:app-preferences-button-position=\"\n                  appPreferencesButtonPosition\n                \"\n                v-model:widget-fullscreen=\"widgetFullscreen\"\n                v-model:widget-global-search=\"widgetGlobalSearch\"\n                v-model:widget-language-toggle=\"widgetLanguageToggle\"\n                v-model:widget-lock-screen=\"widgetLockScreen\"\n                v-model:widget-notification=\"widgetNotification\"\n                v-model:widget-refresh=\"widgetRefresh\"\n                v-model:widget-sidebar-toggle=\"widgetSidebarToggle\"\n                v-model:widget-theme-toggle=\"widgetThemeToggle\"\n              />\n            </Block>\n            <Block :title=\"$t('preferences.footer.title')\">\n              <Footer\n                v-model:footer-enable=\"footerEnable\"\n                v-model:footer-fixed=\"footerFixed\"\n              />\n            </Block>\n            <Block\n              v-if=\"copyrightSettingShow\"\n              :title=\"$t('preferences.copyright.title')\"\n            >\n              <Copyright\n                v-model:copyright-company-name=\"copyrightCompanyName\"\n                v-model:copyright-company-site-link=\"copyrightCompanySiteLink\"\n                v-model:copyright-date=\"copyrightDate\"\n                v-model:copyright-enable=\"copyrightEnable\"\n                v-model:copyright-icp=\"copyrightIcp\"\n                v-model:copyright-icp-link=\"copyrightIcpLink\"\n                :disabled=\"!footerEnable\"\n              />\n            </Block>\n          </template>\n\n          <template #shortcutKey>\n            <Block :title=\"$t('preferences.shortcutKeys.global')\">\n              <GlobalShortcutKeys\n                v-model:shortcut-keys-enable=\"shortcutKeysEnable\"\n                v-model:shortcut-keys-global-search=\"shortcutKeysGlobalSearch\"\n                v-model:shortcut-keys-lock-screen=\"shortcutKeysGlobalLockScreen\"\n                v-model:shortcut-keys-logout=\"shortcutKeysGlobalLogout\"\n              />\n            </Block>\n          </template>\n        </VbenSegmented>\n      </div>\n\n      <template #footer>\n        <VbenButton\n          :disabled=\"!diffPreference\"\n          class=\"mx-4 w-full\"\n          size=\"sm\"\n          variant=\"default\"\n          @click=\"handleCopy\"\n        >\n          <Copy class=\"mr-2 size-3\" />\n          {{ $t('preferences.copyPreferences') }}\n        </VbenButton>\n        <VbenButton\n          :disabled=\"!diffPreference\"\n          class=\"mr-4 w-full\"\n          size=\"sm\"\n          variant=\"ghost\"\n          @click=\"handleClearCache\"\n        >\n          {{ $t('preferences.clearAndLogout') }}\n        </VbenButton>\n      </template>\n    </Drawer>\n  </div>\n</template>\n\n<style scoped>\n:deep(.sticky-tabs-header [role='tablist']) {\n  position: sticky;\n  top: -12px;\n  z-index: 10;\n}\n</style>\n"
  },
  {
    "path": "packages/effects/layouts/src/widgets/preferences/preferences.vue",
    "content": "<script lang=\"ts\" setup>\nimport { Settings } from '@vben/icons';\nimport { $t, loadLocaleMessages } from '@vben/locales';\nimport { preferences, updatePreferences } from '@vben/preferences';\nimport { capitalizeFirstLetter } from '@vben/utils';\nimport { useVbenDrawer } from '@vben-core/popup-ui';\nimport { VbenButton } from '@vben-core/shadcn-ui';\nimport { computed } from 'vue';\n\nimport PreferencesDrawer from './preferences-drawer.vue';\n\nconst [Drawer, drawerApi] = useVbenDrawer({\n  connectedComponent: PreferencesDrawer,\n});\n\n/**\n * preferences 转成 vue props\n * preferences.widget.fullscreen=>widgetFullscreen\n */\nconst attrs = computed(() => {\n  const result: Record<string, any> = {};\n  for (const [key, value] of Object.entries(preferences)) {\n    for (const [subKey, subValue] of Object.entries(value)) {\n      result[`${key}${capitalizeFirstLetter(subKey)}`] = subValue;\n    }\n  }\n  return result;\n});\n\n/**\n * preferences 转成 vue listener\n * preferences.widget.fullscreen=>@update:widgetFullscreen\n */\nconst listen = computed(() => {\n  const result: Record<string, any> = {};\n  for (const [key, value] of Object.entries(preferences)) {\n    if (typeof value === 'object') {\n      for (const subKey of Object.keys(value)) {\n        result[`update:${key}${capitalizeFirstLetter(subKey)}`] = (\n          val: any,\n        ) => {\n          updatePreferences({ [key]: { [subKey]: val } });\n          if (key === 'app' && subKey === 'locale') {\n            loadLocaleMessages(val);\n          }\n        };\n      }\n    } else {\n      result[key] = value;\n    }\n  }\n  return result;\n});\n</script>\n<template>\n  <div>\n    <Drawer v-bind=\"{ ...$attrs, ...attrs }\" v-on=\"listen\" />\n\n    <div @click=\"() => drawerApi.open()\">\n      <slot>\n        <VbenButton\n          :title=\"$t('preferences.title')\"\n          class=\"bg-primary flex-col-center size-10 cursor-pointer rounded-l-lg rounded-r-none border-none\"\n        >\n          <Settings class=\"size-5\" />\n        </VbenButton>\n      </slot>\n    </div>\n  </div>\n</template>\n"
  },
  {
    "path": "packages/effects/layouts/src/widgets/preferences/use-open-preferences.ts",
    "content": "import { ref } from 'vue';\n\nconst openPreferences = ref(false);\n\nfunction useOpenPreferences() {\n  function handleOpenPreference() {\n    openPreferences.value = true;\n  }\n\n  return {\n    handleOpenPreference,\n    openPreferences,\n  };\n}\n\nexport { useOpenPreferences };\n"
  },
  {
    "path": "packages/effects/layouts/src/widgets/theme-toggle/index.ts",
    "content": "export { default as ThemeToggle } from './theme-toggle.vue';\n"
  },
  {
    "path": "packages/effects/layouts/src/widgets/theme-toggle/theme-button.vue",
    "content": "<script lang=\"ts\" setup>\nimport { computed, nextTick } from 'vue';\n\nimport { VbenButton } from '@vben-core/shadcn-ui';\n\ninterface Props {\n  /**\n   * 类型\n   */\n  type?: 'icon' | 'normal';\n}\n\ndefineOptions({\n  name: 'ThemeToggleButton',\n});\n\nconst props = withDefaults(defineProps<Props>(), {\n  type: 'normal',\n});\n\nconst isDark = defineModel<boolean>();\n\nconst theme = computed(() => {\n  return isDark.value ? 'light' : 'dark';\n});\n\nconst bindProps = computed(() => {\n  const type = props.type;\n\n  return type === 'normal'\n    ? {\n        variant: 'heavy' as const,\n      }\n    : {\n        class: 'rounded-full',\n        size: 'icon' as const,\n        style: { padding: '7px' },\n        variant: 'icon' as const,\n      };\n});\n\nfunction toggleTheme(event: MouseEvent) {\n  const isAppearanceTransition =\n    // @ts-expect-error\n    document.startViewTransition &&\n    !window.matchMedia('(prefers-reduced-motion: reduce)').matches;\n  if (!isAppearanceTransition || !event) {\n    isDark.value = !isDark.value;\n    return;\n  }\n  const x = event.clientX;\n  const y = event.clientY;\n  const endRadius = Math.hypot(\n    Math.max(x, innerWidth - x),\n    Math.max(y, innerHeight - y),\n  );\n  // @ts-ignore startViewTransition\n  const transition = document.startViewTransition(async () => {\n    isDark.value = !isDark.value;\n    await nextTick();\n  });\n  transition.ready.then(() => {\n    const clipPath = [\n      `circle(0px at ${x}px ${y}px)`,\n      `circle(${endRadius}px at ${x}px ${y}px)`,\n    ];\n    const animate = document.documentElement.animate(\n      {\n        clipPath: isDark.value ? [...clipPath].reverse() : clipPath,\n      },\n      {\n        duration: 450,\n        easing: 'ease-in',\n        pseudoElement: isDark.value\n          ? '::view-transition-old(root)'\n          : '::view-transition-new(root)',\n      },\n    );\n    animate.onfinish = () => {\n      transition.skipTransition();\n    };\n  });\n}\n</script>\n\n<template>\n  <VbenButton\n    :aria-label=\"theme\"\n    :class=\"[`is-${theme}`]\"\n    aria-live=\"polite\"\n    class=\"theme-toggle cursor-pointer border-none bg-none hover:animate-[shrink_0.3s_ease-in-out]\"\n    v-bind=\"bindProps\"\n    @click.stop=\"toggleTheme\"\n  >\n    <svg aria-hidden=\"true\" height=\"24\" viewBox=\"0 0 24 24\" width=\"24\">\n      <mask\n        id=\"theme-toggle-moon\"\n        class=\"theme-toggle__moon\"\n        fill=\"hsl(var(--foreground)/80%)\"\n        stroke=\"none\"\n      >\n        <rect fill=\"white\" height=\"100%\" width=\"100%\" x=\"0\" y=\"0\" />\n        <circle cx=\"40\" cy=\"8\" fill=\"black\" r=\"11\" />\n      </mask>\n      <circle\n        id=\"sun\"\n        class=\"theme-toggle__sun\"\n        cx=\"12\"\n        cy=\"12\"\n        mask=\"url(#theme-toggle-moon)\"\n        r=\"11\"\n      />\n      <g class=\"theme-toggle__sun-beams\">\n        <line x1=\"12\" x2=\"12\" y1=\"1\" y2=\"3\" />\n        <line x1=\"12\" x2=\"12\" y1=\"21\" y2=\"23\" />\n        <line x1=\"4.22\" x2=\"5.64\" y1=\"4.22\" y2=\"5.64\" />\n        <line x1=\"18.36\" x2=\"19.78\" y1=\"18.36\" y2=\"19.78\" />\n        <line x1=\"1\" x2=\"3\" y1=\"12\" y2=\"12\" />\n        <line x1=\"21\" x2=\"23\" y1=\"12\" y2=\"12\" />\n        <line x1=\"4.22\" x2=\"5.64\" y1=\"19.78\" y2=\"18.36\" />\n        <line x1=\"18.36\" x2=\"19.78\" y1=\"5.64\" y2=\"4.22\" />\n      </g>\n    </svg>\n  </VbenButton>\n</template>\n\n<style scoped>\n.theme-toggle {\n  &__moon {\n    & > circle {\n      transition: transform 0.5s cubic-bezier(0, 0, 0.3, 1);\n    }\n  }\n\n  &__sun {\n    @apply fill-foreground/90 stroke-none;\n\n    transition: transform 1.6s cubic-bezier(0.25, 0, 0.2, 1);\n    transform-origin: center center;\n\n    &:hover > svg > & {\n      @apply fill-foreground/90;\n    }\n  }\n\n  &__sun-beams {\n    @apply stroke-foreground/90 stroke-[2px];\n\n    transition:\n      transform 1.6s cubic-bezier(0.5, 1.5, 0.75, 1.25),\n      opacity 0.6s cubic-bezier(0.25, 0, 0.3, 1);\n    transform-origin: center center;\n\n    &:hover > svg > & {\n      @apply stroke-foreground;\n    }\n  }\n\n  &.is-light {\n    .theme-toggle__sun {\n      @apply scale-50;\n    }\n\n    .theme-toggle__sun-beams {\n      transform: rotateZ(0.25turn);\n    }\n  }\n\n  &.is-dark {\n    .theme-toggle__moon {\n      & > circle {\n        transform: translateX(-20px);\n      }\n    }\n\n    .theme-toggle__sun-beams {\n      @apply opacity-0;\n    }\n  }\n\n  &:hover > svg {\n    .theme-toggle__sun,\n    .theme-toggle__moon {\n      @apply fill-foreground;\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "packages/effects/layouts/src/widgets/theme-toggle/theme-toggle.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { ThemeModeType } from '@vben/types';\n\nimport { MoonStar, Sun, SunMoon } from '@vben/icons';\nimport { $t } from '@vben/locales';\nimport {\n  preferences,\n  updatePreferences,\n  usePreferences,\n} from '@vben/preferences';\nimport {\n  ToggleGroup,\n  ToggleGroupItem,\n  VbenTooltip,\n} from '@vben-core/shadcn-ui';\n\nimport ThemeButton from './theme-button.vue';\n\ndefineOptions({\n  name: 'ThemeToggle',\n});\n\nwithDefaults(defineProps<{ shouldOnHover?: boolean }>(), {\n  shouldOnHover: false,\n});\n\nfunction handleChange(isDark: boolean | undefined) {\n  updatePreferences({\n    theme: { mode: isDark ? 'dark' : 'light' },\n  });\n}\n\nconst { isDark } = usePreferences();\n\nconst PRESETS = [\n  {\n    icon: Sun,\n    name: 'light',\n    title: $t('preferences.theme.light'),\n  },\n  {\n    icon: MoonStar,\n    name: 'dark',\n    title: $t('preferences.theme.dark'),\n  },\n  {\n    icon: SunMoon,\n    name: 'auto',\n    title: $t('preferences.followSystem'),\n  },\n];\n</script>\n<template>\n  <div>\n    <VbenTooltip :disabled=\"!shouldOnHover\" side=\"bottom\">\n      <template #trigger>\n        <ThemeButton\n          :model-value=\"isDark\"\n          type=\"icon\"\n          @update:model-value=\"handleChange\"\n        />\n      </template>\n      <ToggleGroup\n        :model-value=\"preferences.theme.mode\"\n        class=\"gap-2\"\n        type=\"single\"\n        variant=\"outline\"\n        @update:model-value=\"\n          (val) => updatePreferences({ theme: { mode: val as ThemeModeType } })\n        \"\n      >\n        <ToggleGroupItem\n          v-for=\"item in PRESETS\"\n          :key=\"item.name\"\n          :value=\"item.name\"\n        >\n          <component :is=\"item.icon\" class=\"size-5\" />\n        </ToggleGroupItem>\n      </ToggleGroup>\n    </VbenTooltip>\n  </div>\n</template>\n"
  },
  {
    "path": "packages/effects/layouts/src/widgets/user-dropdown/index.ts",
    "content": "export { default as UserDropdown } from './user-dropdown.vue';\n"
  },
  {
    "path": "packages/effects/layouts/src/widgets/user-dropdown/user-dropdown.vue",
    "content": "<script setup lang=\"ts\">\nimport type { Component } from 'vue';\n\nimport type { AnyFunction } from '@vben/types';\n\nimport { computed, useTemplateRef, watch } from 'vue';\n\nimport { useHoverToggle } from '@vben/hooks';\nimport { LockKeyhole, LogOut } from '@vben/icons';\nimport { $t } from '@vben/locales';\nimport { preferences, usePreferences } from '@vben/preferences';\nimport { useAccessStore } from '@vben/stores';\nimport { isWindowsOs } from '@vben/utils';\n\nimport { useVbenModal } from '@vben-core/popup-ui';\nimport {\n  Badge,\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuLabel,\n  DropdownMenuSeparator,\n  DropdownMenuShortcut,\n  DropdownMenuTrigger,\n  VbenAvatar,\n  VbenIcon,\n} from '@vben-core/shadcn-ui';\n\nimport { useMagicKeys, whenever } from '@vueuse/core';\n\nimport { LockScreenModal } from '../lock-screen';\n\ninterface Props {\n  /**\n   * 头像\n   */\n  avatar?: string;\n  /**\n   * @zh_CN 描述\n   */\n  description?: string;\n  /**\n   * 是否启用快捷键\n   */\n  enableShortcutKey?: boolean;\n  /**\n   * 菜单数组\n   */\n  menus?: Array<{\n    handler: AnyFunction;\n    icon?: Component | Function | string;\n    text: string;\n  }>;\n\n  /**\n   * 标签文本\n   */\n  tagText?: string;\n  /**\n   * 文本\n   */\n  text?: string;\n  /** 触发方式 */\n  trigger?: 'both' | 'click' | 'hover';\n  /** hover触发时，延迟响应的时间 */\n  hoverDelay?: number;\n}\n\ndefineOptions({\n  name: 'UserDropdown',\n});\n\nconst props = withDefaults(defineProps<Props>(), {\n  avatar: '',\n  description: '',\n  enableShortcutKey: true,\n  menus: () => [],\n  showShortcutKey: true,\n  tagText: '',\n  text: '',\n  trigger: 'click',\n  hoverDelay: 500,\n});\n\nconst emit = defineEmits<{ logout: [] }>();\n\nconst { globalLockScreenShortcutKey, globalLogoutShortcutKey } =\n  usePreferences();\nconst accessStore = useAccessStore();\nconst [LockModal, lockModalApi] = useVbenModal({\n  connectedComponent: LockScreenModal,\n});\nconst [LogoutModal, logoutModalApi] = useVbenModal({\n  onConfirm() {\n    handleSubmitLogout();\n  },\n});\n\nconst refTrigger = useTemplateRef('refTrigger');\nconst refContent = useTemplateRef('refContent');\nconst [openPopover, hoverWatcher] = useHoverToggle(\n  [refTrigger, refContent],\n  () => props.hoverDelay,\n);\n\nwatch(\n  () => props.trigger === 'hover' || props.trigger === 'both',\n  (val) => {\n    if (val) {\n      hoverWatcher.enable();\n    } else {\n      hoverWatcher.disable();\n    }\n  },\n  {\n    immediate: true,\n  },\n);\n\nconst altView = computed(() => (isWindowsOs() ? 'Alt' : '⌥'));\n\nconst enableLogoutShortcutKey = computed(() => {\n  return props.enableShortcutKey && globalLogoutShortcutKey.value;\n});\n\nconst enableLockScreenShortcutKey = computed(() => {\n  return props.enableShortcutKey && globalLockScreenShortcutKey.value;\n});\n\nconst enableShortcutKey = computed(() => {\n  return props.enableShortcutKey && preferences.shortcutKeys.enable;\n});\n\nfunction handleOpenLock() {\n  lockModalApi.open();\n}\n\nfunction handleSubmitLock(lockScreenPassword: string) {\n  lockModalApi.close();\n  accessStore.lockScreen(lockScreenPassword);\n}\n\nfunction handleLogout() {\n  // emit\n  logoutModalApi.open();\n  openPopover.value = false;\n}\n\nfunction handleSubmitLogout() {\n  emit('logout');\n  logoutModalApi.close();\n}\n\nif (enableShortcutKey.value) {\n  const keys = useMagicKeys();\n  whenever(keys['Alt+KeyQ']!, () => {\n    if (enableLogoutShortcutKey.value) {\n      handleLogout();\n    }\n  });\n\n  whenever(keys['Alt+KeyL']!, () => {\n    if (enableLockScreenShortcutKey.value) {\n      handleOpenLock();\n    }\n  });\n}\n</script>\n\n<template>\n  <LockModal\n    v-if=\"preferences.widget.lockScreen\"\n    :avatar=\"avatar\"\n    :text=\"text\"\n    @submit=\"handleSubmitLock\"\n  />\n\n  <LogoutModal\n    :cancel-text=\"$t('common.cancel')\"\n    :confirm-text=\"$t('common.confirm')\"\n    :fullscreen-button=\"false\"\n    :title=\"$t('common.prompt')\"\n    centered\n    content-class=\"px-8 min-h-10\"\n    footer-class=\"border-none mb-3 mr-3\"\n    header-class=\"border-none\"\n  >\n    {{ $t('ui.widgets.logoutTip') }}\n  </LogoutModal>\n\n  <DropdownMenu v-model:open=\"openPopover\">\n    <DropdownMenuTrigger ref=\"refTrigger\" :disabled=\"props.trigger === 'hover'\">\n      <div class=\"hover:bg-accent ml-1 mr-2 cursor-pointer rounded-full p-1.5\">\n        <div class=\"hover:text-accent-foreground flex-center\">\n          <VbenAvatar :alt=\"text\" :src=\"avatar\" class=\"size-8\" dot />\n        </div>\n      </div>\n    </DropdownMenuTrigger>\n    <DropdownMenuContent class=\"mr-2 min-w-[240px] p-0 pb-1\">\n      <div ref=\"refContent\">\n        <DropdownMenuLabel class=\"flex items-center p-3\">\n          <VbenAvatar\n            :alt=\"text\"\n            :src=\"avatar\"\n            class=\"size-12\"\n            dot\n            dot-class=\"bottom-0 right-1 border-2 size-4 bg-green-500\"\n          />\n          <div class=\"ml-2 w-full\">\n            <div\n              v-if=\"tagText || text || $slots.tagText\"\n              class=\"text-foreground mb-1 flex items-center text-sm font-medium\"\n            >\n              <div\n                class=\"max-w-[100px] overflow-hidden text-ellipsis break-keep\"\n                :title=\"text\"\n              >\n                {{ text }}\n              </div>\n              <slot name=\"tagText\">\n                <Badge v-if=\"tagText\" class=\"ml-2 text-green-400\">\n                  <div\n                    class=\"max-w-[50px] overflow-hidden text-ellipsis\"\n                    :title=\"tagText\"\n                  >\n                    {{ tagText }}\n                  </div>\n                </Badge>\n              </slot>\n            </div>\n            <div class=\"text-muted-foreground text-xs font-normal\">\n              {{ description }}\n            </div>\n          </div>\n        </DropdownMenuLabel>\n        <DropdownMenuSeparator v-if=\"menus?.length\" />\n        <DropdownMenuItem\n          v-for=\"menu in menus\"\n          :key=\"menu.text\"\n          class=\"mx-1 flex cursor-pointer items-center rounded-sm py-1 leading-8\"\n          @click=\"menu.handler\"\n        >\n          <VbenIcon :icon=\"menu.icon\" class=\"mr-2 size-4\" />\n          {{ menu.text }}\n        </DropdownMenuItem>\n        <DropdownMenuSeparator />\n        <DropdownMenuItem\n          v-if=\"preferences.widget.lockScreen\"\n          class=\"mx-1 flex cursor-pointer items-center rounded-sm py-1 leading-8\"\n          @click=\"handleOpenLock\"\n        >\n          <LockKeyhole class=\"mr-2 size-4\" />\n          {{ $t('ui.widgets.lockScreen.title') }}\n          <DropdownMenuShortcut v-if=\"enableLockScreenShortcutKey\">\n            {{ altView }} L\n          </DropdownMenuShortcut>\n        </DropdownMenuItem>\n        <DropdownMenuSeparator v-if=\"preferences.widget.lockScreen\" />\n        <DropdownMenuItem\n          class=\"mx-1 flex cursor-pointer items-center rounded-sm py-1 leading-8\"\n          @click=\"handleLogout\"\n        >\n          <LogOut class=\"mr-2 size-4\" />\n          {{ $t('common.logout') }}\n          <DropdownMenuShortcut v-if=\"enableLogoutShortcutKey\">\n            {{ altView }} Q\n          </DropdownMenuShortcut>\n        </DropdownMenuItem>\n      </div>\n    </DropdownMenuContent>\n  </DropdownMenu>\n</template>\n"
  },
  {
    "path": "packages/effects/layouts/tsconfig.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/tsconfig\",\n  \"extends\": \"@vben/tsconfig/web.json\",\n  \"include\": [\"src\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "packages/effects/plugins/README.md",
    "content": "# @vben/plugins\n\n该目录用于存放项目中集成的第三方库及其相关插件。每个插件都包含了可重用的逻辑、配置和组件，方便在项目中进行统一管理和调用。\n\n## 注意\n\n所有的第三方插件都必须以 `subpath` 形式引入，例：\n\n以 `echarts` 为例，引入方式如下：\n\n**packages.json**\n\n```json\n\"exports\": {\n    \"./echarts\": {\n      \"types\": \"./src/echarts/index.ts\",\n      \"default\": \"./src/echarts/index.ts\"\n    }\n  }\n```\n\n**使用方式**\n\n```ts\nimport { useEcharts } from '@vben/plugins/echarts';\n```\n\n这样做的好处是，应用可以自行选择是否使用插件，而不会因为插件的引入及副作用而导致打包体积增大，只引入需要的插件即可。\n"
  },
  {
    "path": "packages/effects/plugins/package.json",
    "content": "{\n  \"name\": \"@vben/plugins\",\n  \"version\": \"5.5.9\",\n  \"homepage\": \"https://github.com/vbenjs/vue-vben-admin\",\n  \"bugs\": \"https://github.com/vbenjs/vue-vben-admin/issues\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/vbenjs/vue-vben-admin.git\",\n    \"directory\": \"packages/effects/plugins\"\n  },\n  \"license\": \"MIT\",\n  \"type\": \"module\",\n  \"sideEffects\": [\n    \"**/*.css\"\n  ],\n  \"exports\": {\n    \"./echarts\": {\n      \"types\": \"./src/echarts/index.ts\",\n      \"default\": \"./src/echarts/index.ts\"\n    },\n    \"./vxe-table\": {\n      \"types\": \"./src/vxe-table/index.ts\",\n      \"default\": \"./src/vxe-table/index.ts\"\n    },\n    \"./motion\": {\n      \"types\": \"./src/motion/index.ts\",\n      \"default\": \"./src/motion/index.ts\"\n    }\n  },\n  \"dependencies\": {\n    \"@vben-core/form-ui\": \"workspace:*\",\n    \"@vben-core/shadcn-ui\": \"workspace:*\",\n    \"@vben-core/shared\": \"workspace:*\",\n    \"@vben/hooks\": \"workspace:*\",\n    \"@vben/icons\": \"workspace:*\",\n    \"@vben/locales\": \"workspace:*\",\n    \"@vben/preferences\": \"workspace:*\",\n    \"@vben/types\": \"workspace:*\",\n    \"@vben/utils\": \"workspace:*\",\n    \"@vueuse/core\": \"catalog:\",\n    \"@vueuse/motion\": \"catalog:\",\n    \"@vxe-ui/plugin-render-antd\": \"^4.0.18\",\n    \"echarts\": \"catalog:\",\n    \"vue\": \"catalog:\",\n    \"vxe-pc-ui\": \"catalog:\",\n    \"vxe-table\": \"catalog:\"\n  }\n}\n"
  },
  {
    "path": "packages/effects/plugins/src/echarts/echarts-ui.vue",
    "content": "<script setup lang=\"ts\">\ninterface Props {\n  height?: string;\n  width?: string;\n}\n\nwithDefaults(defineProps<Props>(), {\n  height: '300px',\n  width: '100%',\n});\n</script>\n\n<template>\n  <div v-bind=\"$attrs\" :style=\"{ height, width }\"></div>\n</template>\n"
  },
  {
    "path": "packages/effects/plugins/src/echarts/echarts.ts",
    "content": "import type {\n  // 系列类型的定义后缀都为 SeriesOption\n  BarSeriesOption,\n  LineSeriesOption,\n} from 'echarts/charts';\nimport type {\n  DatasetComponentOption,\n  GridComponentOption,\n  // 组件类型的定义后缀都为 ComponentOption\n  TitleComponentOption,\n  TooltipComponentOption,\n} from 'echarts/components';\nimport type { ComposeOption } from 'echarts/core';\n\nimport {\n  BarChart,\n  GaugeChart,\n  LineChart,\n  MapChart,\n  PieChart,\n  RadarChart,\n} from 'echarts/charts';\nimport {\n  // 数据集组件\n  DatasetComponent,\n  GridComponent,\n  LegendComponent,\n  TitleComponent,\n  ToolboxComponent,\n  TooltipComponent,\n  // 内置数据转换器组件 (filter, sort)\n  TransformComponent,\n  VisualMapComponent,\n} from 'echarts/components';\nimport * as echarts from 'echarts/core';\nimport { LabelLayout, UniversalTransition } from 'echarts/features';\nimport { CanvasRenderer } from 'echarts/renderers';\n\n// 通过 ComposeOption 来组合出一个只有必须组件和图表的 Option 类型\nexport type ECOption = ComposeOption<\n  | BarSeriesOption\n  | DatasetComponentOption\n  | GridComponentOption\n  | LineSeriesOption\n  | TitleComponentOption\n  | TooltipComponentOption\n>;\n\n// 注册必须的组件\necharts.use([\n  TitleComponent,\n  PieChart,\n  RadarChart,\n  TooltipComponent,\n  GridComponent,\n  DatasetComponent,\n  TransformComponent,\n  BarChart,\n  LineChart,\n  LabelLayout,\n  UniversalTransition,\n  CanvasRenderer,\n  LegendComponent,\n  ToolboxComponent,\n  GaugeChart,\n  VisualMapComponent,\n  MapChart,\n]);\n\nexport default echarts;\n"
  },
  {
    "path": "packages/effects/plugins/src/echarts/index.ts",
    "content": "export * from './echarts';\nexport { default as EchartsUI } from './echarts-ui.vue';\nexport * from './use-echarts';\n"
  },
  {
    "path": "packages/effects/plugins/src/echarts/use-echarts.ts",
    "content": "import type { EChartsOption } from 'echarts';\n\nimport type { Ref } from 'vue';\n\nimport type { Nullable } from '@vben/types';\n\nimport type EchartsUI from './echarts-ui.vue';\n\nimport { computed, nextTick, watch } from 'vue';\n\nimport { usePreferences } from '@vben/preferences';\n\nimport {\n  tryOnUnmounted,\n  useDebounceFn,\n  useResizeObserver,\n  useTimeoutFn,\n  useWindowSize,\n} from '@vueuse/core';\n\nimport echarts from './echarts';\n\ntype EchartsUIType = typeof EchartsUI | undefined;\n\ntype EchartsThemeType = 'dark' | 'light' | null;\n\nfunction useEcharts(chartRef: Ref<EchartsUIType>) {\n  let chartInstance: echarts.ECharts | null = null;\n  let cacheOptions: EChartsOption = {};\n\n  const { isDark } = usePreferences();\n  const { height, width } = useWindowSize();\n  const resizeHandler: () => void = useDebounceFn(resize, 200);\n\n  const getChartEl = (): HTMLElement | null => {\n    const refValue = chartRef?.value as unknown;\n    if (!refValue) return null;\n    if (refValue instanceof HTMLElement) {\n      return refValue;\n    }\n    const maybeComponent = refValue as { $el?: HTMLElement };\n    return maybeComponent.$el ?? null;\n  };\n\n  const isElHidden = (el: HTMLElement | null): boolean => {\n    if (!el) return true;\n    return el.offsetHeight === 0 || el.offsetWidth === 0;\n  };\n\n  const getOptions = computed((): EChartsOption => {\n    if (!isDark.value) {\n      return {};\n    }\n\n    return {\n      backgroundColor: 'transparent',\n    };\n  });\n\n  const initCharts = (t?: EchartsThemeType) => {\n    const el = chartRef?.value?.$el;\n    if (!el) {\n      return;\n    }\n    chartInstance = echarts.init(el, t || isDark.value ? 'dark' : null);\n\n    return chartInstance;\n  };\n\n  const renderEcharts = (\n    options: EChartsOption,\n    clear = true,\n  ): Promise<Nullable<echarts.ECharts>> => {\n    cacheOptions = options;\n    const currentOptions = {\n      ...options,\n      ...getOptions.value,\n    };\n    return new Promise((resolve) => {\n      if (chartRef.value?.offsetHeight === 0) {\n        useTimeoutFn(async () => {\n          resolve(await renderEcharts(currentOptions));\n        }, 30);\n        return;\n      }\n      nextTick(() => {\n        const el = getChartEl();\n        if (isElHidden(el)) {\n          useTimeoutFn(async () => {\n            resolve(await renderEcharts(currentOptions));\n          }, 30);\n          return;\n        }\n        useTimeoutFn(() => {\n          if (!chartInstance) {\n            const instance = initCharts();\n            if (!instance) return;\n          }\n          clear && chartInstance?.clear();\n          chartInstance?.setOption(currentOptions);\n          resolve(chartInstance);\n        }, 30);\n      });\n    });\n  };\n\n  function resize(withAnimation = true) {\n    const el = getChartEl();\n    if (isElHidden(el)) {\n      return;\n    }\n    chartInstance?.resize({\n      animation: withAnimation\n        ? {\n            duration: 300,\n            easing: 'quadraticIn',\n          }\n        : undefined,\n    });\n  }\n\n  watch([width, height], () => {\n    resizeHandler?.();\n  });\n\n  useResizeObserver(chartRef as never, resizeHandler);\n\n  watch(isDark, () => {\n    if (chartInstance) {\n      chartInstance.dispose();\n      initCharts();\n      renderEcharts(cacheOptions);\n      resize();\n    }\n  });\n\n  tryOnUnmounted(() => {\n    // 销毁实例，释放资源\n    chartInstance?.dispose();\n  });\n  return {\n    renderEcharts,\n    resize,\n    getChartInstance: () => chartInstance,\n  };\n}\n\nexport { useEcharts };\n\nexport type { EchartsUIType };\n"
  },
  {
    "path": "packages/effects/plugins/src/motion/index.ts",
    "content": "export * from './types';\n\nexport {\n  MotionComponent as Motion,\n  MotionDirective,\n  MotionGroupComponent as MotionGroup,\n  MotionPlugin,\n} from '@vueuse/motion';\n"
  },
  {
    "path": "packages/effects/plugins/src/motion/types.ts",
    "content": "export const MotionPresets = [\n  'fade',\n  'fadeVisible',\n  'fadeVisibleOnce',\n  'rollBottom',\n  'rollLeft',\n  'rollRight',\n  'rollTop',\n  'rollVisibleBottom',\n  'rollVisibleLeft',\n  'rollVisibleRight',\n  'rollVisibleTop',\n  'pop',\n  'popVisible',\n  'popVisibleOnce',\n  'slideBottom',\n  'slideLeft',\n  'slideRight',\n  'slideTop',\n  'slideVisibleBottom',\n  'slideVisibleLeft',\n  'slideVisibleRight',\n  'slideVisibleTop',\n] as const;\n\nexport type MotionPreset = (typeof MotionPresets)[number];\n"
  },
  {
    "path": "packages/effects/plugins/src/vxe-table/api.ts",
    "content": "import type { VxeGridInstance } from 'vxe-table';\n\nimport type { ExtendedFormApi } from '@vben-core/form-ui';\n\nimport type { VxeGridProps } from './types';\n\nimport { toRaw } from 'vue';\n\nimport { Store } from '@vben-core/shared/store';\nimport {\n  bindMethods,\n  isBoolean,\n  isFunction,\n  mergeWithArrayOverride,\n  StateHandler,\n} from '@vben-core/shared/utils';\n\nfunction getDefaultState(): VxeGridProps {\n  return {\n    class: '',\n    gridClass: '',\n    gridOptions: {},\n    gridEvents: {},\n    formOptions: undefined,\n    showSearchForm: true,\n  };\n}\n\nexport class VxeGridApi<T extends Record<string, any> = any> {\n  public formApi = {} as ExtendedFormApi;\n\n  // private prevState: null | VxeGridProps = null;\n  public grid = {} as VxeGridInstance<T>;\n  public state: null | VxeGridProps<T> = null;\n\n  public store: Store<VxeGridProps<T>>;\n\n  private isMounted = false;\n\n  private stateHandler: StateHandler;\n\n  constructor(options: VxeGridProps = {}) {\n    const storeState = { ...options };\n\n    const defaultState = getDefaultState();\n    this.store = new Store<VxeGridProps>(\n      mergeWithArrayOverride(storeState, defaultState),\n      {\n        onUpdate: () => {\n          // this.prevState = this.state;\n          this.state = this.store.state;\n        },\n      },\n    );\n\n    this.state = this.store.state;\n    this.stateHandler = new StateHandler();\n    bindMethods(this);\n  }\n\n  mount(instance: null | VxeGridInstance, formApi: ExtendedFormApi) {\n    if (!this.isMounted && instance) {\n      this.grid = instance;\n      this.formApi = formApi;\n      this.stateHandler.setConditionTrue();\n      this.isMounted = true;\n    }\n  }\n\n  async query(params: Record<string, any> = {}) {\n    try {\n      await this.grid.commitProxy('query', toRaw(params));\n    } catch (error) {\n      console.error('Error occurred while querying:', error);\n    }\n  }\n\n  async reload(params: Record<string, any> = {}) {\n    try {\n      await this.grid.commitProxy('reload', toRaw(params));\n    } catch (error) {\n      console.error('Error occurred while reloading:', error);\n    }\n  }\n\n  setGridOptions(options: Partial<VxeGridProps['gridOptions']>) {\n    this.setState({\n      gridOptions: options,\n    });\n  }\n\n  setLoading(isLoading: boolean) {\n    this.setState({\n      gridOptions: {\n        loading: isLoading,\n      },\n    });\n  }\n\n  setState(\n    stateOrFn:\n      | ((prev: VxeGridProps<T>) => Partial<VxeGridProps<T>>)\n      | Partial<VxeGridProps<T>>,\n  ) {\n    if (isFunction(stateOrFn)) {\n      this.store.setState((prev) => {\n        return mergeWithArrayOverride(stateOrFn(prev), prev);\n      });\n    } else {\n      this.store.setState((prev) => mergeWithArrayOverride(stateOrFn, prev));\n    }\n  }\n\n  toggleSearchForm(show?: boolean) {\n    this.setState({\n      showSearchForm: isBoolean(show) ? show : !this.state?.showSearchForm,\n    });\n    // nextTick(() => {\n    //   this.grid.recalculate();\n    // });\n    return this.state?.showSearchForm;\n  }\n\n  unmount() {\n    this.isMounted = false;\n    this.stateHandler.reset();\n  }\n}\n"
  },
  {
    "path": "packages/effects/plugins/src/vxe-table/extends.ts",
    "content": "import type { Recordable } from '@vben/types';\nimport type { VxeGridProps, VxeUIExport } from 'vxe-table';\n\nimport type { VxeGridApi } from './api';\n\nimport { formatDate, formatDateTime, isFunction } from '@vben/utils';\n\nexport function extendProxyOptions(\n  api: VxeGridApi,\n  options: VxeGridProps,\n  getFormValues: () => Recordable<any>,\n) {\n  [\n    'query',\n    'querySuccess',\n    'queryError',\n    'queryAll',\n    'queryAllSuccess',\n    'queryAllError',\n  ].forEach((key) => {\n    extendProxyOption(key, api, options, getFormValues);\n  });\n}\n\nfunction extendProxyOption(\n  key: string,\n  api: VxeGridApi,\n  options: VxeGridProps,\n  getFormValues: () => Recordable<any>,\n) {\n  const { proxyConfig } = options;\n  const configFn = (proxyConfig?.ajax as Recordable<any>)?.[key];\n  if (!isFunction(configFn)) {\n    return options;\n  }\n\n  const wrapperFn = async (\n    params: Recordable<any>,\n    customValues: Recordable<any>,\n    ...args: Recordable<any>[]\n  ) => {\n    const formValues = getFormValues();\n    const data = await configFn(\n      params,\n      {\n        /**\n         * 开启toolbarConfig.refresh功能\n         * 点击刷新按钮 这里的值为PointerEvent 会携带错误参数\n         */\n        ...(customValues instanceof PointerEvent ? {} : customValues),\n        ...formValues,\n      },\n      ...args,\n    );\n    return data;\n  };\n  api.setState({\n    gridOptions: {\n      proxyConfig: {\n        ajax: {\n          [key]: wrapperFn,\n        },\n      },\n    },\n  });\n}\n\nexport function extendsDefaultFormatter(vxeUI: VxeUIExport) {\n  vxeUI.formats.add('formatDate', {\n    tableCellFormatMethod({ cellValue }) {\n      return formatDate(cellValue);\n    },\n  });\n\n  vxeUI.formats.add('formatDateTime', {\n    tableCellFormatMethod({ cellValue }) {\n      return formatDateTime(cellValue);\n    },\n  });\n}\n"
  },
  {
    "path": "packages/effects/plugins/src/vxe-table/index.ts",
    "content": "export { setupVbenVxeTable } from './init';\nexport type { VxeTableGridOptions } from './types';\nexport * from './use-vxe-grid';\nexport { default as VbenVxeGrid } from './use-vxe-grid.vue';\nexport type { VxeGridDefines } from 'vxe-table';\nexport type {\n  VxeGridListeners,\n  VxeGridProps,\n  VxeGridPropTypes,\n} from 'vxe-table';\n"
  },
  {
    "path": "packages/effects/plugins/src/vxe-table/init.ts",
    "content": "import type { SetupVxeTable } from './types';\n\nimport { defineComponent, watch } from 'vue';\n\nimport { usePreferences } from '@vben/preferences';\n\nimport { useVbenForm } from '@vben-core/form-ui';\n\n/**\n * 该插件提供了在表格中渲染第三方组件，用于兼容 ant-design-vue 组件库\n *\n * 解决组件问题(如select浮层与失焦冲突)\n *\n * @see https://vxeui.com/other4/#/plugin-render-antd/start/full/npmInstall\n * @see https://vxetable.cn/other4/#/table/other/antd\n */\nimport VxeUIPluginRenderAntd from '@vxe-ui/plugin-render-antd';\nimport {\n  VxeButton,\n  VxeCheckbox,\n\n  // VxeFormGather,\n  // VxeForm,\n  // VxeFormItem,\n  VxeIcon,\n  VxeInput,\n  VxeLoading,\n  VxeModal,\n  VxeNumberInput,\n  VxePager,\n  // VxeList,\n  // VxeModal,\n  // VxeOptgroup,\n  // VxeOption,\n  // VxePulldown,\n  // VxeRadio,\n  // VxeRadioButton,\n  VxeRadioGroup,\n  VxeSelect,\n  VxeTooltip,\n  VxeUI,\n  VxeUpload,\n  // VxeSwitch,\n  // VxeTextarea,\n} from 'vxe-pc-ui';\nimport enUS from 'vxe-pc-ui/lib/language/en-US';\n// 导入默认的语言\nimport zhCN from 'vxe-pc-ui/lib/language/zh-CN';\nimport {\n  VxeColgroup,\n  VxeColumn,\n  VxeGrid,\n  VxeTable,\n  VxeToolbar,\n} from 'vxe-table';\n\nimport { extendsDefaultFormatter } from './extends';\n\nimport '@vxe-ui/plugin-render-antd/dist/style.css';\n\n// 是否加载过\nlet isInit = false;\n\n// eslint-disable-next-line import/no-mutable-exports\nexport let useTableForm: typeof useVbenForm;\n\n// 部分组件，如果没注册，vxe-table 会报错，这里实际没用组件，只是为了不报错，同时可以减少打包体积\nconst createVirtualComponent = (name = '') => {\n  return defineComponent({\n    name,\n  });\n};\n\nexport function initVxeTable() {\n  if (isInit) {\n    return;\n  }\n\n  VxeUI.component(VxeTable);\n  VxeUI.component(VxeColumn);\n  VxeUI.component(VxeColgroup);\n  VxeUI.component(VxeGrid);\n  VxeUI.component(VxeToolbar);\n\n  VxeUI.component(VxeButton);\n  // VxeUI.component(VxeButtonGroup);\n  VxeUI.component(VxeCheckbox);\n  // VxeUI.component(VxeCheckboxGroup);\n  VxeUI.component(createVirtualComponent('VxeForm'));\n  // VxeUI.component(VxeFormGather);\n  // VxeUI.component(VxeFormItem);\n  VxeUI.component(VxeIcon);\n  VxeUI.component(VxeInput);\n  // VxeUI.component(VxeList);\n  VxeUI.component(VxeLoading);\n  VxeUI.component(VxeModal);\n  VxeUI.component(VxeNumberInput);\n  // VxeUI.component(VxeOptgroup);\n  // VxeUI.component(VxeOption);\n  VxeUI.component(VxePager);\n  // VxeUI.component(VxePulldown);\n  // VxeUI.component(VxeRadio);\n  // VxeUI.component(VxeRadioButton);\n  VxeUI.component(VxeRadioGroup);\n  VxeUI.component(VxeSelect);\n  // VxeUI.component(VxeSwitch);\n  // VxeUI.component(VxeTextarea);\n  VxeUI.component(VxeTooltip);\n  VxeUI.component(VxeUpload);\n  VxeUI.use(VxeUIPluginRenderAntd);\n\n  isInit = true;\n}\n\nexport function setupVbenVxeTable(setupOptions: SetupVxeTable) {\n  const { configVxeTable, useVbenForm } = setupOptions;\n\n  initVxeTable();\n  useTableForm = useVbenForm;\n\n  const { isDark, locale } = usePreferences();\n\n  const localMap = {\n    'zh-CN': zhCN,\n    'en-US': enUS,\n  };\n\n  watch(\n    [() => isDark.value, () => locale.value],\n    ([isDarkValue, localeValue]) => {\n      VxeUI.setTheme(isDarkValue ? 'dark' : 'light');\n      VxeUI.setI18n(localeValue, localMap[localeValue]);\n      VxeUI.setLanguage(localeValue);\n    },\n    {\n      immediate: true,\n    },\n  );\n\n  extendsDefaultFormatter(VxeUI);\n\n  configVxeTable(VxeUI);\n}\n"
  },
  {
    "path": "packages/effects/plugins/src/vxe-table/style.css",
    "content": ":root .vxe-grid {\n  --vxe-ui-font-color: hsl(var(--foreground));\n  --vxe-ui-font-primary-color: hsl(var(--primary));\n\n  /* --vxe-ui-font-lighten-color: #babdc0;\n  --vxe-ui-font-darken-color: #86898e; */\n  --vxe-ui-font-disabled-color: hsl(var(--foreground) / 50%);\n\n  /* base */\n  --vxe-ui-base-popup-border-color: hsl(var(--border));\n  --vxe-ui-input-disabled-color: hsl(var(--border) / 60%);\n\n  /* --vxe-ui-base-popup-box-shadow: 0px 12px 30px 8px rgb(0 0 0 / 50%); */\n\n  /* layout */\n  --vxe-ui-layout-background-color: hsl(var(--background));\n  --vxe-ui-table-resizable-line-color: hsl(var(--heavy));\n\n  /* --vxe-ui-table-fixed-left-scrolling-box-shadow: 8px 0px 10px -5px hsl(var(--accent));\n  --vxe-ui-table-fixed-right-scrolling-box-shadow: -8px 0px 10px -5px hsl(var(--accent)); */\n\n  /* input */\n  --vxe-ui-input-border-color: hsl(var(--border));\n\n  /* --vxe-ui-input-placeholder-color: #8d9095; */\n\n  /* --vxe-ui-input-disabled-background-color: #262727; */\n\n  /* loading */\n  --vxe-ui-loading-background-color: hsl(var(--overlay-content));\n\n  /* table */\n  --vxe-ui-table-header-background-color: hsl(0deg 0% 98%);\n  --vxe-ui-table-border-color: hsl(var(--border));\n  --vxe-ui-table-row-hover-background-color: hsl(var(--accent-hover));\n  --vxe-ui-table-row-striped-background-color: hsl(var(--accent) / 60%);\n  --vxe-ui-table-row-hover-striped-background-color: hsl(var(--accent));\n  --vxe-ui-table-row-radio-checked-background-color: hsl(var(--accent));\n  --vxe-ui-table-row-hover-radio-checked-background-color: hsl(\n    var(--accent-hover)\n  );\n  --vxe-ui-table-row-checkbox-checked-background-color: hsl(var(--accent));\n  --vxe-ui-table-row-hover-checkbox-checked-background-color: hsl(\n    var(--accent-hover)\n  );\n  --vxe-ui-table-row-current-background-color: hsl(var(--accent));\n  --vxe-ui-table-row-hover-current-background-color: hsl(var(--accent-hover));\n  --vxe-ui-font-primary-tinge-color: hsl(var(--primary));\n  --vxe-ui-font-primary-lighten-color: hsl(var(--primary) / 60%);\n  --vxe-ui-font-primary-darken-color: hsl(var(--primary));\n\n  /* 拖拽列宽颜色 */\n  --vxe-ui-table-resizable-drag-line-color: hsl(var(--primary));\n\n  /* --vxe-ui-table-fixed-scrolling-box-shadow-color: rgb(0 0 0 / 80%); */\n\n  height: auto !important;\n}\n\nhtml[data-vxe-ui-theme='dark'] .vxe-grid {\n  --vxe-ui-table-header-background-color: hsl(var(--accent) / 50%);\n}\n\n.vxe-pager {\n  .vxe-pager--prev-btn:not(.is--disabled):active,\n  .vxe-pager--next-btn:not(.is--disabled):active,\n  .vxe-pager--num-btn:not(.is--disabled):active,\n  .vxe-pager--jump-prev:not(.is--disabled):active,\n  .vxe-pager--jump-next:not(.is--disabled):active,\n  .vxe-pager--prev-btn:not(.is--disabled):focus,\n  .vxe-pager--next-btn:not(.is--disabled):focus,\n  .vxe-pager--num-btn:not(.is--disabled):focus,\n  .vxe-pager--jump-prev:not(.is--disabled):focus,\n  .vxe-pager--jump-next:not(.is--disabled):focus {\n    color: hsl(var(--accent-foreground));\n    background-color: hsl(var(--accent));\n    border: 1px solid hsl(var(--border));\n    box-shadow: 0 0 0 1px hsl(var(--border));\n  }\n\n  .vxe-pager--wrapper {\n    display: flex;\n    align-items: center;\n  }\n\n  .vxe-pager--sizes {\n    margin-right: auto;\n  }\n}\n\n.vxe-pager--wrapper {\n  @apply justify-center md:justify-end;\n}\n\n.vxe-tools--operate {\n  margin-right: 0.25rem;\n  margin-left: 0.75rem;\n}\n\n.vxe-table-custom--checkbox-option:hover {\n  background: none !important;\n}\n\n.vxe-toolbar {\n  padding: 0;\n}\n\n/** 关闭搜索表单 */\n.vxe-grid:not(.vxe-grid--form-wrapper form) .vxe-toolbar {\n  padding: 0.6em 0 !important;\n}\n\n/** 开启搜索表单 */\n.vxe-grid:has(.vxe-grid--form-wrapper form) .vxe-toolbar {\n  padding: 0 0 0.6em !important;\n}\n\n.vxe-tools--operate:not(:has(button)) {\n  margin-left: 0;\n}\n\n.vxe-grid--layout-header-wrapper {\n  overflow: visible;\n}\n\n.vxe-grid--layout-body-content-wrapper {\n  overflow: hidden;\n}\n\n/* modal/drawer中使用 tooltip需要设置更高的z-index 防止被遮挡 */\nbody:has(div[data-dismissable-layer]) .vxe-tooltip--wrapper {\n  /* tooltip直接在style中设置 需要!important来实现优先级覆盖 */\n  z-index: calc(var(--popup-z-index) + 1) !important;\n}\n\n/*\n在border: inner场景下 去除下边的边框\nTODO: 最后一条数据hover/check仍会显示边框\n*/\n.vxe-table.border--inner .vxe-table--border-line {\n  border-radius: var(--vxe-ui-table-border-radius)\n    var(--vxe-ui-table-border-radius) 0 0;\n}\n\n/* modal/drawer里使用列配置 重置列弹窗被遮挡 */\n.vxe-dynamics--modal > .vxe-modal--wrapper {\n  z-index: calc(var(--popup-z-index) + 1) !important;\n}\n"
  },
  {
    "path": "packages/effects/plugins/src/vxe-table/types.ts",
    "content": "import type {\n  VxeGridListeners,\n  VxeGridPropTypes,\n  VxeGridProps as VxeTableGridProps,\n  VxeUIExport,\n} from 'vxe-table';\n\nimport type { Ref } from 'vue';\n\nimport type { ClassType, DeepPartial } from '@vben/types';\n\nimport type { BaseFormComponentType, VbenFormProps } from '@vben-core/form-ui';\n\nimport type { VxeGridApi } from './api';\n\nimport { useVbenForm } from '@vben-core/form-ui';\n\nexport interface VxePaginationInfo {\n  currentPage: number;\n  pageSize: number;\n  total: number;\n}\n\ninterface ToolbarConfigOptions extends VxeGridPropTypes.ToolbarConfig {\n  /** 是否显示切换搜索表单的按钮 */\n  search?: boolean;\n}\n\nexport interface VxeTableGridOptions<T = any> extends VxeTableGridProps<T> {\n  /** 工具栏配置 */\n  toolbarConfig?: ToolbarConfigOptions;\n}\n\nexport interface SeparatorOptions {\n  show?: boolean;\n  backgroundColor?: string;\n}\n\nexport interface VxeGridProps<\n  T extends Record<string, any> = any,\n  D extends BaseFormComponentType = BaseFormComponentType,\n> {\n  /**\n   * 标题\n   */\n  tableTitle?: string;\n  /**\n   * 标题帮助\n   */\n  tableTitleHelp?: string;\n  /**\n   * 组件class\n   */\n  class?: ClassType;\n  /**\n   * vxe-grid class\n   */\n  gridClass?: ClassType;\n  /**\n   * vxe-grid 配置\n   */\n  gridOptions?: DeepPartial<VxeTableGridOptions<T>>;\n  /**\n   * vxe-grid 事件\n   */\n  gridEvents?: Partial<VxeGridListeners<T>>;\n  /**\n   * 表单配置\n   */\n  formOptions?: VbenFormProps<D>;\n  /**\n   * 显示搜索表单\n   */\n  showSearchForm?: boolean;\n  /**\n   * 搜索表单与表格主体之间的分隔条\n   */\n  separator?: boolean | SeparatorOptions;\n}\n\nexport type ExtendedVxeGridApi<\n  D extends Record<string, any> = any,\n  F extends BaseFormComponentType = BaseFormComponentType,\n> = VxeGridApi<D> & {\n  useStore: <T = NoInfer<VxeGridProps<D, F>>>(\n    selector?: (state: NoInfer<VxeGridProps<any, any>>) => T,\n  ) => Readonly<Ref<T>>;\n};\n\nexport interface SetupVxeTable {\n  configVxeTable: (ui: VxeUIExport) => void;\n  useVbenForm: typeof useVbenForm;\n}\n"
  },
  {
    "path": "packages/effects/plugins/src/vxe-table/use-vxe-grid.ts",
    "content": "import type { VxeGridSlots, VxeGridSlotTypes } from 'vxe-table';\n\nimport type { SlotsType } from 'vue';\n\nimport type { BaseFormComponentType } from '@vben-core/form-ui';\n\nimport type { ExtendedVxeGridApi, VxeGridProps } from './types';\n\nimport { defineComponent, h, onBeforeUnmount } from 'vue';\n\nimport { useStore } from '@vben-core/shared/store';\n\nimport { VxeGridApi } from './api';\nimport VxeGrid from './use-vxe-grid.vue';\n\ntype FilteredSlots<T> = {\n  [K in keyof VxeGridSlots<T> as K extends 'form'\n    ? never\n    : K]: VxeGridSlots<T>[K];\n};\n\nexport function useVbenVxeGrid<\n  T extends Record<string, any> = any,\n  D extends BaseFormComponentType = BaseFormComponentType,\n>(options: VxeGridProps<T, D>) {\n  // const IS_REACTIVE = isReactive(options);\n  const api = new VxeGridApi(options);\n  const extendedApi: ExtendedVxeGridApi<T, D> = api as ExtendedVxeGridApi<T, D>;\n  extendedApi.useStore = (selector) => {\n    return useStore(api.store, selector);\n  };\n\n  const Grid = defineComponent(\n    (props: VxeGridProps<T>, { attrs, slots }) => {\n      onBeforeUnmount(() => {\n        api.unmount();\n      });\n      api.setState({ ...props, ...attrs });\n      return () => h(VxeGrid, { ...props, ...attrs, api: extendedApi }, slots);\n    },\n    {\n      name: 'VbenVxeGrid',\n      inheritAttrs: false,\n      slots: Object as SlotsType<\n        {\n          // 表格标题\n          'table-title': undefined;\n          // 工具栏左侧部分\n          'toolbar-actions': VxeGridSlotTypes.DefaultSlotParams<T>;\n          // 工具栏右侧部分\n          'toolbar-tools': VxeGridSlotTypes.DefaultSlotParams<T>;\n        } & FilteredSlots<T>\n      >,\n    },\n  );\n  // Add reactivity support\n  // if (IS_REACTIVE) {\n  //   watch(\n  //     () => options,\n  //     () => {\n  //       api.setState(options);\n  //     },\n  //     { immediate: true },\n  //   );\n  // }\n\n  return [Grid, extendedApi] as const;\n}\n\nexport type UseVbenVxeGrid = typeof useVbenVxeGrid;\n"
  },
  {
    "path": "packages/effects/plugins/src/vxe-table/use-vxe-grid.vue",
    "content": "<script lang=\"ts\" setup>\nimport type {\n  VxeGridDefines,\n  VxeGridInstance,\n  VxeGridListeners,\n  VxeGridPropTypes,\n  VxeGridProps as VxeTableGridProps,\n  VxeToolbarPropTypes,\n} from 'vxe-table';\n\nimport type { SetupContext } from 'vue';\n\nimport type { VbenFormProps } from '@vben-core/form-ui';\n\nimport type { ExtendedVxeGridApi, VxeGridProps } from './types';\n\nimport {\n  computed,\n  nextTick,\n  onMounted,\n  onUnmounted,\n  toRaw,\n  useSlots,\n  useTemplateRef,\n  watch,\n} from 'vue';\n\nimport { usePriorityValues } from '@vben/hooks';\nimport { EmptyIcon } from '@vben/icons';\nimport { $t } from '@vben/locales';\nimport { usePreferences } from '@vben/preferences';\nimport {\n  cloneDeep,\n  cn,\n  isBoolean,\n  isEqual,\n  mergeWithArrayOverride,\n} from '@vben/utils';\n\nimport { VbenHelpTooltip, VbenLoading } from '@vben-core/shadcn-ui';\n\nimport { VxeButton } from 'vxe-pc-ui';\nimport { VxeGrid, VxeUI } from 'vxe-table';\n\nimport { extendProxyOptions } from './extends';\nimport { useTableForm } from './init';\n\nimport 'vxe-table/styles/cssvar.scss';\nimport 'vxe-pc-ui/styles/cssvar.scss';\nimport './style.css';\n\ninterface Props extends VxeGridProps {\n  api: ExtendedVxeGridApi;\n}\n\nconst props = withDefaults(defineProps<Props>(), {});\n\nconst FORM_SLOT_PREFIX = 'form-';\n\nconst TOOLBAR_ACTIONS = 'toolbar-actions';\nconst TOOLBAR_TOOLS = 'toolbar-tools';\nconst TABLE_TITLE = 'table-title';\n\nconst gridRef = useTemplateRef<VxeGridInstance>('gridRef');\n\nconst state = props.api?.useStore?.();\n\nconst {\n  gridOptions,\n  class: className,\n  gridClass,\n  gridEvents,\n  formOptions,\n  tableTitle,\n  tableTitleHelp,\n  showSearchForm,\n  separator,\n} = usePriorityValues(props, state);\n\nconst { isMobile } = usePreferences();\nconst isSeparator = computed(() => {\n  if (\n    !formOptions.value ||\n    showSearchForm.value === false ||\n    separator.value === false\n  ) {\n    return false;\n  }\n  if (separator.value === true || separator.value === undefined) {\n    return true;\n  }\n  return separator.value.show !== false;\n});\nconst separatorBg = computed(() => {\n  return !separator.value ||\n    isBoolean(separator.value) ||\n    !separator.value.backgroundColor\n    ? undefined\n    : separator.value.backgroundColor;\n});\nconst slots: SetupContext['slots'] = useSlots();\n\nconst [Form, formApi] = useTableForm({\n  compact: true,\n  handleSubmit: async () => {\n    const formValues = await formApi.getValues();\n    formApi.setLatestSubmissionValues(toRaw(formValues));\n    props.api.reload(formValues);\n  },\n  handleReset: async () => {\n    const prevValues = await formApi.getValues();\n    await formApi.resetForm();\n    const formValues = await formApi.getValues();\n    formApi.setLatestSubmissionValues(formValues);\n    // 如果值发生了变化，submitOnChange会触发刷新。所以只在submitOnChange为false或者值没有发生变化时，手动刷新\n    if (isEqual(prevValues, formValues) || !formOptions.value?.submitOnChange) {\n      props.api.reload(formValues);\n    }\n  },\n  commonConfig: {\n    componentProps: {\n      class: 'w-full',\n    },\n  },\n  showCollapseButton: true,\n  submitButtonOptions: {\n    content: computed(() => $t('common.search')),\n  },\n  // enter提交\n  submitOnEnter: true,\n  wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3',\n});\n\nconst showTableTitle = computed(() => {\n  return !!slots[TABLE_TITLE]?.() || tableTitle.value;\n});\n\nconst showToolbar = computed(() => {\n  return (\n    !!slots[TOOLBAR_ACTIONS]?.() ||\n    !!slots[TOOLBAR_TOOLS]?.() ||\n    showTableTitle.value\n  );\n});\n\nconst toolbarOptions = computed(() => {\n  const slotActions = slots[TOOLBAR_ACTIONS]?.();\n  const slotTools = slots[TOOLBAR_TOOLS]?.();\n  const searchBtn: VxeToolbarPropTypes.ToolConfig = {\n    code: 'search',\n    icon: 'vxe-icon-search',\n    circle: true,\n    status: showSearchForm.value ? 'primary' : undefined,\n    title: showSearchForm.value\n      ? $t('common.hideSearchPanel')\n      : $t('common.showSearchPanel'),\n  };\n  // 将搜索按钮合并到用户配置的toolbarConfig.tools中\n  const toolbarConfig: VxeGridPropTypes.ToolbarConfig = {\n    tools: (gridOptions.value?.toolbarConfig?.tools ??\n      []) as VxeToolbarPropTypes.ToolConfig[],\n  };\n  if (gridOptions.value?.toolbarConfig?.search && !!formOptions.value) {\n    toolbarConfig.tools = Array.isArray(toolbarConfig.tools)\n      ? [...toolbarConfig.tools, searchBtn]\n      : [searchBtn];\n  }\n\n  if (!showToolbar.value) {\n    return { toolbarConfig };\n  }\n\n  // 强制使用固定的toolbar配置，不允许用户自定义\n  // 减少配置的复杂度，以及后续维护的成本\n  toolbarConfig.slots = {\n    ...(slotActions || showTableTitle.value\n      ? { buttons: TOOLBAR_ACTIONS }\n      : {}),\n    ...(slotTools ? { tools: TOOLBAR_TOOLS } : {}),\n  };\n  return { toolbarConfig };\n});\n\nconst options = computed(() => {\n  const globalGridConfig = VxeUI?.getConfig()?.grid ?? {};\n\n  const mergedOptions: VxeTableGridProps = cloneDeep(\n    mergeWithArrayOverride(\n      {},\n      toRaw(toolbarOptions.value),\n      toRaw(gridOptions.value),\n      globalGridConfig,\n    ),\n  );\n\n  if (mergedOptions.proxyConfig) {\n    const { ajax } = mergedOptions.proxyConfig;\n    mergedOptions.proxyConfig.enabled = !!ajax;\n    // 不自动加载数据, 由组件控制\n    mergedOptions.proxyConfig.autoLoad = false;\n  }\n\n  if (mergedOptions.pagerConfig) {\n    const mobileLayouts = [\n      'PrevJump',\n      'PrevPage',\n      'Number',\n      'NextPage',\n      'NextJump',\n    ] as any;\n    const layouts = [\n      'Total',\n      'Sizes',\n      'Home',\n      ...mobileLayouts,\n      'End',\n    ] as readonly string[];\n    mergedOptions.pagerConfig = mergeWithArrayOverride(\n      {},\n      mergedOptions.pagerConfig,\n      {\n        pageSize: 20,\n        background: true,\n        pageSizes: [10, 20, 30, 50, 100, 200],\n        className: 'mt-2 w-full',\n        layouts: isMobile.value ? mobileLayouts : layouts,\n        size: 'mini' as const,\n      },\n    );\n  }\n  if (mergedOptions.formConfig) {\n    mergedOptions.formConfig.enabled = false;\n  }\n  return mergedOptions;\n});\n\nfunction onToolbarToolClick(event: VxeGridDefines.ToolbarToolClickEventParams) {\n  if (event.code === 'search') {\n    onSearchBtnClick();\n  }\n  (\n    gridEvents.value?.toolbarToolClick as VxeGridListeners['toolbarToolClick']\n  )?.(event);\n}\n\nfunction onSearchBtnClick() {\n  props.api?.toggleSearchForm?.();\n}\n\nconst events = computed(() => {\n  return {\n    ...gridEvents.value,\n    toolbarToolClick: onToolbarToolClick,\n  };\n});\n\nconst delegatedSlots = computed(() => {\n  const resultSlots: string[] = [];\n\n  for (const key of Object.keys(slots)) {\n    if (\n      !['empty', 'form', 'loading', TOOLBAR_ACTIONS, TOOLBAR_TOOLS].includes(\n        key,\n      )\n    ) {\n      resultSlots.push(key);\n    }\n  }\n  return resultSlots;\n});\n\nconst delegatedFormSlots = computed(() => {\n  const resultSlots: string[] = [];\n\n  for (const key of Object.keys(slots)) {\n    if (key.startsWith(FORM_SLOT_PREFIX)) {\n      resultSlots.push(key);\n    }\n  }\n  return resultSlots.map((key) => key.replace(FORM_SLOT_PREFIX, ''));\n});\n\nconst showDefaultEmpty = computed(() => {\n  // 检查是否有原生的 VXE Table 空状态配置\n  const hasEmptyText = options.value.emptyText !== undefined;\n  const hasEmptyRender = options.value.emptyRender !== undefined;\n\n  // 如果有原生配置，就不显示默认的空状态\n  return !hasEmptyText && !hasEmptyRender;\n});\n\nasync function init() {\n  await nextTick();\n  const globalGridConfig = VxeUI?.getConfig()?.grid ?? {};\n  const defaultGridOptions: VxeTableGridProps = mergeWithArrayOverride(\n    {},\n    toRaw(gridOptions.value),\n    toRaw(globalGridConfig),\n  );\n  // 内部主动加载数据，防止form的默认值影响\n  const autoLoad = defaultGridOptions.proxyConfig?.autoLoad;\n  const enableProxyConfig = options.value.proxyConfig?.enabled;\n  if (enableProxyConfig && autoLoad) {\n    // 第一次拿到的是readonly的数据 如果需要修改 需要cloneDeep\n    props.api.grid.commitProxy?.(\n      'query',\n      cloneDeep(formOptions.value)\n        ? (cloneDeep(await formApi.getValues()) ?? {})\n        : {},\n    );\n    // props.api.reload(formApi.form?.values ?? {});\n  }\n\n  // form 由 vben-form代替，所以不适配formConfig，这里给出警告\n  const formConfig = gridOptions.value?.formConfig;\n  // 处理某个页面加载多个Table时，第2个之后的Table初始化报出警告\n  // 因为第一次初始化之后会把defaultGridOptions和gridOptions合并后缓存进State\n  if (formConfig && formConfig.enabled) {\n    console.warn(\n      '[Vben Vxe Table]: The formConfig in the grid is not supported, please use the `formOptions` props',\n    );\n  }\n  props.api?.setState?.({ gridOptions: defaultGridOptions });\n  // form 由 vben-form 代替，所以需要保证query相关事件可以拿到参数\n  extendProxyOptions(props.api, defaultGridOptions, () =>\n    formApi.getLatestSubmissionValues(),\n  );\n}\n\n// formOptions支持响应式\nwatch(\n  formOptions,\n  () => {\n    formApi.setState((prev) => {\n      const finalFormOptions: VbenFormProps = mergeWithArrayOverride(\n        {},\n        formOptions.value,\n        prev,\n      );\n      return {\n        ...finalFormOptions,\n        collapseTriggerResize: !!finalFormOptions.showCollapseButton,\n      };\n    });\n  },\n  {\n    immediate: true,\n  },\n);\n\nconst isCompactForm = computed(() => {\n  return formApi.getState()?.compact;\n});\n\nonMounted(() => {\n  props.api?.mount?.(gridRef.value, formApi);\n  init();\n});\n\nonUnmounted(() => {\n  formApi?.unmount?.();\n  props.api?.unmount?.();\n});\n</script>\n\n<template>\n  <div :class=\"cn('bg-card h-full rounded-md', className)\">\n    <VxeGrid\n      ref=\"gridRef\"\n      :class=\"\n        cn(\n          'p-2',\n          {\n            'pt-0': showToolbar && !formOptions,\n          },\n          gridClass,\n        )\n      \"\n      v-bind=\"options\"\n      v-on=\"events\"\n    >\n      <!-- 左侧操作区域或者title -->\n      <template v-if=\"showToolbar\" #toolbar-actions=\"slotProps\">\n        <slot v-if=\"showTableTitle\" name=\"table-title\">\n          <div class=\"mr-1 pl-1 text-[1rem]\">\n            {{ tableTitle }}\n            <VbenHelpTooltip v-if=\"tableTitleHelp\" trigger-class=\"pb-1\">\n              {{ tableTitleHelp }}\n            </VbenHelpTooltip>\n          </div>\n        </slot>\n        <slot name=\"toolbar-actions\" v-bind=\"slotProps\"> </slot>\n      </template>\n\n      <!-- 继承默认的slot -->\n      <template\n        v-for=\"slotName in delegatedSlots\"\n        :key=\"slotName\"\n        #[slotName]=\"slotProps\"\n      >\n        <slot :name=\"slotName\" v-bind=\"slotProps\"></slot>\n      </template>\n      <template #toolbar-tools=\"slotProps\">\n        <slot name=\"toolbar-tools\" v-bind=\"slotProps\"></slot>\n        <VxeButton\n          icon=\"vxe-icon-search\"\n          circle\n          class=\"ml-2\"\n          v-if=\"gridOptions?.toolbarConfig?.search && !!formOptions\"\n          :status=\"showSearchForm ? 'primary' : undefined\"\n          :title=\"$t('common.search')\"\n          @click=\"onSearchBtnClick\"\n        />\n      </template>\n\n      <!-- form表单 -->\n      <template #form>\n        <div\n          v-if=\"formOptions\"\n          v-show=\"showSearchForm !== false\"\n          :class=\"\n            cn(\n              'relative rounded py-3',\n              isCompactForm\n                ? isSeparator\n                  ? 'pb-8'\n                  : 'pb-4'\n                : isSeparator\n                  ? 'pb-4'\n                  : 'pb-0',\n            )\n          \"\n        >\n          <slot name=\"form\">\n            <Form>\n              <template\n                v-for=\"slotName in delegatedFormSlots\"\n                :key=\"slotName\"\n                #[slotName]=\"slotProps\"\n              >\n                <slot\n                  :name=\"`${FORM_SLOT_PREFIX}${slotName}`\"\n                  v-bind=\"slotProps\"\n                ></slot>\n              </template>\n              <template #reset-before=\"slotProps\">\n                <slot name=\"reset-before\" v-bind=\"slotProps\"></slot>\n              </template>\n              <template #submit-before=\"slotProps\">\n                <slot name=\"submit-before\" v-bind=\"slotProps\"></slot>\n              </template>\n              <template #expand-before=\"slotProps\">\n                <slot name=\"expand-before\" v-bind=\"slotProps\"></slot>\n              </template>\n              <template #expand-after=\"slotProps\">\n                <slot name=\"expand-after\" v-bind=\"slotProps\"></slot>\n              </template>\n            </Form>\n          </slot>\n          <div\n            v-if=\"isSeparator\"\n            :style=\"{\n              ...(separatorBg ? { backgroundColor: separatorBg } : undefined),\n            }\"\n            class=\"bg-background-deep z-100 absolute -left-2 bottom-1 h-2 w-[calc(100%+1rem)] overflow-hidden md:bottom-2 md:h-3\"\n          ></div>\n        </div>\n      </template>\n      <!-- loading -->\n      <template #loading>\n        <slot name=\"loading\">\n          <VbenLoading :spinning=\"true\" />\n        </slot>\n      </template>\n      <!-- 统一控状态 -->\n      <template v-if=\"showDefaultEmpty\" #empty>\n        <slot name=\"empty\">\n          <EmptyIcon class=\"mx-auto\" />\n          <div class=\"mt-2\">{{ $t('common.noData') }}</div>\n        </slot>\n      </template>\n    </VxeGrid>\n  </div>\n</template>\n"
  },
  {
    "path": "packages/effects/plugins/tsconfig.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/tsconfig\",\n  \"extends\": \"@vben/tsconfig/web.json\",\n  \"include\": [\"src\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "packages/effects/request/package.json",
    "content": "{\n  \"name\": \"@vben/request\",\n  \"version\": \"5.5.9\",\n  \"homepage\": \"https://github.com/vbenjs/vue-vben-admin\",\n  \"bugs\": \"https://github.com/vbenjs/vue-vben-admin/issues\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/vbenjs/vue-vben-admin.git\",\n    \"directory\": \"packages/effects/request\"\n  },\n  \"license\": \"MIT\",\n  \"type\": \"module\",\n  \"sideEffects\": [\n    \"**/*.css\"\n  ],\n  \"exports\": {\n    \".\": {\n      \"types\": \"./src/index.ts\",\n      \"default\": \"./src/index.ts\"\n    }\n  },\n  \"dependencies\": {\n    \"@vben/locales\": \"workspace:*\",\n    \"@vben/utils\": \"workspace:*\",\n    \"axios\": \"catalog:\",\n    \"qs\": \"catalog:\"\n  },\n  \"devDependencies\": {\n    \"@types/qs\": \"catalog:\",\n    \"axios-mock-adapter\": \"catalog:\"\n  }\n}\n"
  },
  {
    "path": "packages/effects/request/src/index.ts",
    "content": "export * from './request-client';\nexport * from 'axios';\nexport { stringify } from 'qs';\n"
  },
  {
    "path": "packages/effects/request/src/request-client/index.ts",
    "content": "export * from './preset-interceptors';\nexport * from './request-client';\nexport type * from './types';\n"
  },
  {
    "path": "packages/effects/request/src/request-client/modules/downloader.test.ts",
    "content": "import type { AxiosRequestConfig } from 'axios';\n\nimport { beforeEach, describe, expect, it, vi } from 'vitest';\n\nimport { FileDownloader } from './downloader';\n\ndescribe('fileDownloader', () => {\n  let fileDownloader: FileDownloader;\n  const mockAxiosInstance = {\n    get: vi.fn(),\n  } as any;\n\n  beforeEach(() => {\n    fileDownloader = new FileDownloader(mockAxiosInstance);\n  });\n\n  it('should create an instance of FileDownloader', () => {\n    expect(fileDownloader).toBeInstanceOf(FileDownloader);\n  });\n\n  it('should download a file and return a Blob', async () => {\n    const url = 'https://example.com/file';\n    const mockBlob = new Blob(['file content'], { type: 'text/plain' });\n    const mockResponse: Blob = mockBlob;\n\n    mockAxiosInstance.get.mockResolvedValueOnce(mockResponse);\n\n    const result = await fileDownloader.download(url);\n\n    expect(result).toBeInstanceOf(Blob);\n    expect(result).toEqual(mockBlob);\n    expect(mockAxiosInstance.get).toHaveBeenCalledWith(url, {\n      responseType: 'blob',\n      responseReturn: 'body',\n    });\n  });\n\n  it('should merge provided config with default config', async () => {\n    const url = 'https://example.com/file';\n    const mockBlob = new Blob(['file content'], { type: 'text/plain' });\n    const mockResponse: Blob = mockBlob;\n\n    mockAxiosInstance.get.mockResolvedValueOnce(mockResponse);\n\n    const customConfig: AxiosRequestConfig = {\n      headers: { 'Custom-Header': 'value' },\n    };\n\n    const result = await fileDownloader.download(url, customConfig);\n    expect(result).toBeInstanceOf(Blob);\n    expect(result).toEqual(mockBlob);\n    expect(mockAxiosInstance.get).toHaveBeenCalledWith(url, {\n      ...customConfig,\n      responseType: 'blob',\n      responseReturn: 'body',\n    });\n  });\n\n  it('should handle errors gracefully', async () => {\n    const url = 'https://example.com/file';\n    mockAxiosInstance.get.mockRejectedValueOnce(new Error('Network Error'));\n    await expect(fileDownloader.download(url)).rejects.toThrow('Network Error');\n  });\n\n  it('should handle empty URL gracefully', async () => {\n    const url = '';\n    mockAxiosInstance.get.mockRejectedValueOnce(\n      new Error('Request failed with status code 404'),\n    );\n\n    await expect(fileDownloader.download(url)).rejects.toThrow(\n      'Request failed with status code 404',\n    );\n  });\n\n  it('should handle null URL gracefully', async () => {\n    const url = null as unknown as string;\n    mockAxiosInstance.get.mockRejectedValueOnce(\n      new Error('Request failed with status code 404'),\n    );\n\n    await expect(fileDownloader.download(url)).rejects.toThrow(\n      'Request failed with status code 404',\n    );\n  });\n});\n"
  },
  {
    "path": "packages/effects/request/src/request-client/modules/downloader.ts",
    "content": "import type { RequestClient } from '../request-client';\nimport type { RequestClientConfig } from '../types';\n\ntype DownloadRequestConfig = {\n  /**\n   * 定义期望获得的数据类型。\n   * raw: 原始的AxiosResponse，包括headers、status等。\n   * body: 只返回响应数据的BODY部分(Blob)\n   */\n  responseReturn?: 'body' | 'raw';\n} & Omit<RequestClientConfig, 'responseReturn'>;\n\nclass FileDownloader {\n  private client: RequestClient;\n\n  constructor(client: RequestClient) {\n    this.client = client;\n  }\n  /**\n   * 下载文件\n   * @param url 文件的完整链接\n   * @param config 配置信息，可选。\n   * @returns 如果config.responseReturn为'body'，则返回Blob(默认)，否则返回RequestResponse<Blob>\n   */\n  public async download<T = Blob>(\n    url: string,\n    config?: DownloadRequestConfig,\n  ): Promise<T> {\n    const finalConfig: DownloadRequestConfig = {\n      responseReturn: 'body',\n      ...config,\n      responseType: 'blob',\n    };\n\n    const response = await this.client.get<T>(url, finalConfig);\n\n    return response;\n  }\n}\n\nexport { FileDownloader };\n"
  },
  {
    "path": "packages/effects/request/src/request-client/modules/interceptor.ts",
    "content": "import type { AxiosInstance, AxiosResponse } from 'axios';\n\nimport type {\n  RequestInterceptorConfig,\n  ResponseInterceptorConfig,\n} from '../types';\n\nconst defaultRequestInterceptorConfig: RequestInterceptorConfig = {\n  fulfilled: (response) => response,\n  rejected: (error) => Promise.reject(error),\n};\n\nconst defaultResponseInterceptorConfig: ResponseInterceptorConfig = {\n  fulfilled: (response: AxiosResponse) => response,\n  rejected: (error) => Promise.reject(error),\n};\n\nclass InterceptorManager {\n  private axiosInstance: AxiosInstance;\n\n  constructor(instance: AxiosInstance) {\n    this.axiosInstance = instance;\n  }\n\n  addRequestInterceptor({\n    fulfilled,\n    rejected,\n  }: RequestInterceptorConfig = defaultRequestInterceptorConfig) {\n    this.axiosInstance.interceptors.request.use(fulfilled, rejected);\n  }\n\n  addResponseInterceptor<T = any>({\n    fulfilled,\n    rejected,\n  }: ResponseInterceptorConfig<T> = defaultResponseInterceptorConfig) {\n    this.axiosInstance.interceptors.response.use(fulfilled, rejected);\n  }\n}\n\nexport { InterceptorManager };\n"
  },
  {
    "path": "packages/effects/request/src/request-client/modules/sse.test.ts",
    "content": "import type { RequestClient } from '../request-client';\n\nimport { beforeEach, describe, expect, it, vi } from 'vitest';\n\nimport { SSE } from './sse';\n\n// 模拟 TextDecoder\nconst OriginalTextDecoder = globalThis.TextDecoder;\n\nbeforeEach(() => {\n  vi.stubGlobal(\n    'TextDecoder',\n    class {\n      private decoder = new OriginalTextDecoder();\n      decode(value: Uint8Array, opts?: any) {\n        return this.decoder.decode(value, opts);\n      }\n    },\n  );\n});\n\n// 创建 fetch mock\nconst createFetchMock = (chunks: string[], ok = true) => {\n  const encoder = new TextEncoder();\n  let index = 0;\n  return vi.fn().mockResolvedValue({\n    ok,\n    status: ok ? 200 : 500,\n    body: {\n      getReader: () => ({\n        read: async () => {\n          if (index < chunks.length) {\n            return { done: false, value: encoder.encode(chunks[index++]) };\n          }\n          return { done: true, value: undefined };\n        },\n      }),\n    },\n  });\n};\n\ndescribe('sSE', () => {\n  let client: RequestClient;\n  let sse: SSE;\n\n  beforeEach(() => {\n    vi.restoreAllMocks();\n    client = {\n      getBaseUrl: () => 'http://localhost',\n      instance: {\n        interceptors: {\n          request: {\n            handlers: [],\n          },\n        },\n      },\n    } as unknown as RequestClient;\n    sse = new SSE(client);\n  });\n\n  it('should call requestSSE when postSSE is used', async () => {\n    const spy = vi.spyOn(sse, 'requestSSE').mockResolvedValue(undefined);\n    await sse.postSSE('/test', { foo: 'bar' }, { headers: { a: '1' } });\n    expect(spy).toHaveBeenCalledWith(\n      '/test',\n      { foo: 'bar' },\n      {\n        headers: { a: '1' },\n        method: 'POST',\n      },\n    );\n  });\n\n  it('should throw error if fetch response not ok', async () => {\n    vi.stubGlobal('fetch', createFetchMock([], false));\n    await expect(sse.requestSSE('/bad')).rejects.toThrow(\n      'HTTP error! status: 500',\n    );\n  });\n\n  it('should trigger onMessage and onEnd callbacks', async () => {\n    const messages: string[] = [];\n    const onMessage = vi.fn((msg: string) => messages.push(msg));\n    const onEnd = vi.fn();\n\n    vi.stubGlobal('fetch', createFetchMock(['hello', ' world']));\n\n    await sse.requestSSE('/sse', undefined, { onMessage, onEnd });\n\n    expect(onMessage).toHaveBeenCalledTimes(2);\n    expect(messages.join('')).toBe('hello world');\n    // onEnd 不再带参数\n    expect(onEnd).toHaveBeenCalled();\n  });\n\n  it('should apply request interceptors', async () => {\n    const interceptor = vi.fn(async (config) => {\n      config.headers['x-test'] = 'intercepted';\n      return config;\n    });\n    (client.instance.interceptors.request as any).handlers.push({\n      fulfilled: interceptor,\n    });\n\n    // 创建 fetch mock，并挂到全局\n    const fetchMock = createFetchMock(['data']);\n    vi.stubGlobal('fetch', fetchMock);\n\n    await sse.requestSSE('/sse', undefined, {});\n\n    expect(interceptor).toHaveBeenCalled();\n    expect(fetchMock).toHaveBeenCalledWith(\n      'http://localhost/sse',\n      expect.objectContaining({\n        headers: expect.any(Headers),\n      }),\n    );\n\n    const calls = fetchMock.mock?.calls;\n    expect(calls).toBeDefined();\n    expect(calls?.length).toBeGreaterThan(0);\n\n    const init = calls?.[0]?.[1] as RequestInit;\n    expect(init).toBeDefined();\n\n    const headers = init?.headers as Headers;\n    expect(headers?.get('x-test')).toBe('intercepted');\n    expect(headers?.get('accept')).toBe('text/event-stream');\n  });\n\n  it('should throw error when no reader', async () => {\n    vi.stubGlobal(\n      'fetch',\n      vi.fn().mockResolvedValue({\n        ok: true,\n        status: 200,\n        body: null,\n      }),\n    );\n    await expect(sse.requestSSE('/sse')).rejects.toThrow('No reader');\n  });\n});\n"
  },
  {
    "path": "packages/effects/request/src/request-client/modules/sse.ts",
    "content": "import type { AxiosRequestHeaders, InternalAxiosRequestConfig } from 'axios';\n\nimport type { RequestClient } from '../request-client';\nimport type { SseRequestOptions } from '../types';\n\n/**\n * SSE模块\n */\nclass SSE {\n  private client: RequestClient;\n\n  constructor(client: RequestClient) {\n    this.client = client;\n  }\n\n  public async postSSE(\n    url: string,\n    data?: any,\n    requestOptions?: SseRequestOptions,\n  ) {\n    return this.requestSSE(url, data, {\n      ...requestOptions,\n      method: 'POST',\n    });\n  }\n\n  /**\n   * SSE请求方法\n   * @param url - 请求URL\n   * @param data - 请求数据\n   * @param requestOptions - SSE请求选项\n   */\n  public async requestSSE(\n    url: string,\n    data?: any,\n    requestOptions?: SseRequestOptions,\n  ) {\n    const baseUrl = this.client.getBaseUrl() || '';\n\n    let axiosConfig: InternalAxiosRequestConfig<any> = {\n      url,\n      method: (requestOptions?.method as any) ?? 'GET',\n      headers: {} as AxiosRequestHeaders,\n    };\n    const requestInterceptors = this.client.instance.interceptors\n      .request as any;\n    if (\n      requestInterceptors.handlers &&\n      requestInterceptors.handlers.length > 0\n    ) {\n      for (const handler of requestInterceptors.handlers) {\n        if (typeof handler?.fulfilled === 'function') {\n          const next = await handler.fulfilled(axiosConfig as any);\n          if (next) axiosConfig = next as InternalAxiosRequestConfig<any>;\n        }\n      }\n    }\n\n    const merged = new Headers();\n    Object.entries(\n      (axiosConfig.headers ?? {}) as Record<string, string>,\n    ).forEach(([k, v]) => merged.set(k, String(v)));\n    if (requestOptions?.headers) {\n      new Headers(requestOptions.headers).forEach((v, k) => merged.set(k, v));\n    }\n    if (!merged.has('accept')) {\n      merged.set('accept', 'text/event-stream');\n    }\n\n    let bodyInit = requestOptions?.body ?? data;\n    const ct = (merged.get('content-type') || '').toLowerCase();\n    if (\n      bodyInit &&\n      typeof bodyInit === 'object' &&\n      !ArrayBuffer.isView(bodyInit as any) &&\n      !(bodyInit instanceof ArrayBuffer) &&\n      !(bodyInit instanceof Blob) &&\n      !(bodyInit instanceof FormData) &&\n      ct.includes('application/json')\n    ) {\n      bodyInit = JSON.stringify(bodyInit);\n    }\n    const requestInit: RequestInit = {\n      ...requestOptions,\n      method: axiosConfig.method,\n      headers: merged,\n      body: bodyInit,\n    };\n\n    const response = await fetch(safeJoinUrl(baseUrl, url), requestInit);\n    if (!response.ok) {\n      throw new Error(`HTTP error! status: ${response.status}`);\n    }\n\n    const reader = response.body?.getReader();\n    const decoder = new TextDecoder();\n\n    if (!reader) {\n      throw new Error('No reader');\n    }\n    let isEnd = false;\n    while (!isEnd) {\n      const { done, value } = await reader.read();\n      if (done) {\n        isEnd = true;\n        decoder.decode(new Uint8Array(0), { stream: false });\n        requestOptions?.onEnd?.();\n        reader.releaseLock?.();\n        break;\n      }\n      const content = decoder.decode(value, { stream: true });\n      requestOptions?.onMessage?.(content);\n    }\n  }\n}\n\nfunction safeJoinUrl(baseUrl: string | undefined, url: string): string {\n  if (!baseUrl) {\n    return url; // 没有 baseUrl，直接返回 url\n  }\n\n  // 如果 url 本身就是绝对地址，直接返回\n  if (/^https?:\\/\\//i.test(url)) {\n    return url;\n  }\n\n  // 如果 baseUrl 是完整 URL，就用 new URL\n  if (/^https?:\\/\\//i.test(baseUrl)) {\n    return new URL(url, baseUrl).toString();\n  }\n\n  // 否则，当作路径拼接\n  return `${baseUrl.replace(/\\/+$/, '')}/${url.replace(/^\\/+/, '')}`;\n}\n\nexport { SSE };\n"
  },
  {
    "path": "packages/effects/request/src/request-client/modules/uploader.test.ts",
    "content": "import type { AxiosRequestConfig, AxiosResponse } from 'axios';\n\nimport { beforeEach, describe, expect, it, vi } from 'vitest';\n\nimport { FileUploader } from './uploader';\n\ndescribe('fileUploader', () => {\n  let fileUploader: FileUploader;\n  // Mock the AxiosInstance\n  const mockAxiosInstance = {\n    post: vi.fn(),\n  } as any;\n\n  beforeEach(() => {\n    fileUploader = new FileUploader(mockAxiosInstance);\n  });\n\n  it('should create an instance of FileUploader', () => {\n    expect(fileUploader).toBeInstanceOf(FileUploader);\n  });\n\n  it('should upload a file and return the response', async () => {\n    const url = 'https://example.com/upload';\n    const file = new File(['file content'], 'test.txt', { type: 'text/plain' });\n    const mockResponse: AxiosResponse = {\n      config: {} as any,\n      data: { success: true },\n      headers: {},\n      status: 200,\n      statusText: 'OK',\n    };\n\n    (\n      mockAxiosInstance.post as unknown as ReturnType<typeof vi.fn>\n    ).mockResolvedValueOnce(mockResponse);\n\n    const result = await fileUploader.upload(url, { file });\n    expect(result).toEqual(mockResponse);\n    expect(mockAxiosInstance.post).toHaveBeenCalledWith(\n      url,\n      expect.any(FormData),\n      {\n        headers: {\n          'Content-Type': 'multipart/form-data',\n        },\n      },\n    );\n  });\n\n  it('should merge provided config with default config', async () => {\n    const url = 'https://example.com/upload';\n    const file = new File(['file content'], 'test.txt', { type: 'text/plain' });\n    const mockResponse: AxiosResponse = {\n      config: {} as any,\n      data: { success: true },\n      headers: {},\n      status: 200,\n      statusText: 'OK',\n    };\n\n    (\n      mockAxiosInstance.post as unknown as ReturnType<typeof vi.fn>\n    ).mockResolvedValueOnce(mockResponse);\n\n    const customConfig: AxiosRequestConfig = {\n      headers: { 'Custom-Header': 'value' },\n    };\n\n    const result = await fileUploader.upload(url, { file }, customConfig);\n    expect(result).toEqual(mockResponse);\n    expect(mockAxiosInstance.post).toHaveBeenCalledWith(\n      url,\n      expect.any(FormData),\n      {\n        headers: {\n          'Content-Type': 'multipart/form-data',\n          'Custom-Header': 'value',\n        },\n      },\n    );\n  });\n\n  it('should handle errors gracefully', async () => {\n    const url = 'https://example.com/upload';\n    const file = new File(['file content'], 'test.txt', { type: 'text/plain' });\n    (\n      mockAxiosInstance.post as unknown as ReturnType<typeof vi.fn>\n    ).mockRejectedValueOnce(new Error('Network Error'));\n\n    await expect(fileUploader.upload(url, { file })).rejects.toThrow(\n      'Network Error',\n    );\n  });\n\n  it('should handle empty URL gracefully', async () => {\n    const url = '';\n    const file = new File(['file content'], 'test.txt', { type: 'text/plain' });\n    (\n      mockAxiosInstance.post as unknown as ReturnType<typeof vi.fn>\n    ).mockRejectedValueOnce(new Error('Request failed with status code 404'));\n\n    await expect(fileUploader.upload(url, { file })).rejects.toThrow(\n      'Request failed with status code 404',\n    );\n  });\n\n  it('should handle null URL gracefully', async () => {\n    const url = null as unknown as string;\n    const file = new File(['file content'], 'test.txt', { type: 'text/plain' });\n    (\n      mockAxiosInstance.post as unknown as ReturnType<typeof vi.fn>\n    ).mockRejectedValueOnce(new Error('Request failed with status code 404'));\n\n    await expect(fileUploader.upload(url, { file })).rejects.toThrow(\n      'Request failed with status code 404',\n    );\n  });\n});\n"
  },
  {
    "path": "packages/effects/request/src/request-client/modules/uploader.ts",
    "content": "import type { RequestClient } from '../request-client';\nimport type { RequestClientConfig } from '../types';\n\nimport { isUndefined } from '@vben/utils';\n\nclass FileUploader {\n  private client: RequestClient;\n\n  constructor(client: RequestClient) {\n    this.client = client;\n  }\n\n  public async upload<T = any>(\n    url: string,\n    data: Record<string, any> & { file: Blob | File },\n    config?: RequestClientConfig,\n  ): Promise<T> {\n    const formData = new FormData();\n\n    Object.entries(data).forEach(([key, value]) => {\n      if (Array.isArray(value)) {\n        value.forEach((item, index) => {\n          !isUndefined(item) && formData.append(`${key}[${index}]`, item);\n        });\n      } else {\n        !isUndefined(value) && formData.append(key, value);\n      }\n    });\n\n    const finalConfig: RequestClientConfig = {\n      ...config,\n      headers: {\n        'Content-Type': 'multipart/form-data',\n        ...config?.headers,\n      },\n    };\n\n    return this.client.post(url, formData, finalConfig);\n  }\n}\n\nexport { FileUploader };\n"
  },
  {
    "path": "packages/effects/request/src/request-client/preset-interceptors.ts",
    "content": "import type { RequestClient } from './request-client';\nimport type { MakeErrorMessageFn, ResponseInterceptorConfig } from './types';\n\nimport { $t } from '@vben/locales';\nimport { isFunction } from '@vben/utils';\n\nimport axios from 'axios';\n\nexport const defaultResponseInterceptor = ({\n  codeField = 'code',\n  dataField = 'data',\n  successCode = 0,\n}: {\n  /** 响应数据中代表访问结果的字段名 */\n  codeField: string;\n  /** 响应数据中装载实际数据的字段名，或者提供一个函数从响应数据中解析需要返回的数据 */\n  dataField: ((response: any) => any) | string;\n  /** 当codeField所指定的字段值与successCode相同时，代表接口访问成功。如果提供一个函数，则返回true代表接口访问成功 */\n  successCode: ((code: any) => boolean) | number | string;\n}): ResponseInterceptorConfig => {\n  return {\n    fulfilled: (response) => {\n      const { config, data: responseData, status } = response;\n\n      if (config.responseReturn === 'raw') {\n        return response;\n      }\n\n      if (status >= 200 && status < 400) {\n        if (config.responseReturn === 'body') {\n          return responseData;\n        } else if (\n          isFunction(successCode)\n            ? successCode(responseData[codeField])\n            : responseData[codeField] === successCode\n        ) {\n          return isFunction(dataField)\n            ? dataField(responseData)\n            : responseData[dataField];\n        }\n      }\n      throw Object.assign({}, response, { response });\n    },\n  };\n};\n\nexport const authenticateResponseInterceptor = ({\n  client,\n  doReAuthenticate,\n  doRefreshToken,\n  enableRefreshToken,\n  formatToken,\n}: {\n  client: RequestClient;\n  doReAuthenticate: () => Promise<void>;\n  doRefreshToken: () => Promise<string>;\n  enableRefreshToken: boolean;\n  formatToken: (token: string) => null | string;\n}): ResponseInterceptorConfig => {\n  return {\n    rejected: async (error) => {\n      const { config, response } = error;\n      // 如果不是 401 错误，直接抛出异常\n      if (response?.status !== 401) {\n        throw error;\n      }\n      // 判断是否启用了 refreshToken 功能\n      // 如果没有启用或者已经是重试请求了，直接跳转到重新登录\n      if (!enableRefreshToken || config.__isRetryRequest) {\n        await doReAuthenticate();\n        throw error;\n      }\n      // 如果正在刷新 token，则将请求加入队列，等待刷新完成\n      if (client.isRefreshing) {\n        return new Promise((resolve) => {\n          client.refreshTokenQueue.push((newToken: string) => {\n            config.headers.Authorization = formatToken(newToken);\n            resolve(client.request(config.url, { ...config }));\n          });\n        });\n      }\n\n      // 标记开始刷新 token\n      client.isRefreshing = true;\n      // 标记当前请求为重试请求，避免无限循环\n      config.__isRetryRequest = true;\n\n      try {\n        const newToken = await doRefreshToken();\n\n        // 处理队列中的请求\n        client.refreshTokenQueue.forEach((callback) => callback(newToken));\n        // 清空队列\n        client.refreshTokenQueue = [];\n\n        return client.request(error.config.url, { ...error.config });\n      } catch (refreshError) {\n        // 如果刷新 token 失败，处理错误（如强制登出或跳转登录页面）\n        client.refreshTokenQueue.forEach((callback) => callback(''));\n        client.refreshTokenQueue = [];\n        console.error('Refresh token failed, please login again.');\n        await doReAuthenticate();\n\n        throw refreshError;\n      } finally {\n        client.isRefreshing = false;\n      }\n    },\n  };\n};\n\nexport const errorMessageResponseInterceptor = (\n  makeErrorMessage?: MakeErrorMessageFn,\n): ResponseInterceptorConfig => {\n  return {\n    rejected: (error: any) => {\n      if (axios.isCancel(error)) {\n        return Promise.reject(error);\n      }\n\n      const err: string = error?.toString?.() ?? '';\n      let errMsg = '';\n      if (err?.includes('Network Error')) {\n        errMsg = $t('ui.fallback.http.networkError');\n      } else if (error?.message?.includes?.('timeout')) {\n        errMsg = $t('ui.fallback.http.requestTimeout');\n      }\n      if (errMsg) {\n        makeErrorMessage?.(errMsg, error);\n        return Promise.reject(error);\n      }\n\n      let errorMessage = '';\n      const status = error?.response?.status;\n\n      switch (status) {\n        case 400: {\n          errorMessage = $t('ui.fallback.http.badRequest');\n          break;\n        }\n        case 401: {\n          errorMessage = $t('ui.fallback.http.unauthorized');\n          break;\n        }\n        case 403: {\n          errorMessage = $t('ui.fallback.http.forbidden');\n          break;\n        }\n        case 404: {\n          errorMessage = $t('ui.fallback.http.notFound');\n          break;\n        }\n        case 408: {\n          errorMessage = $t('ui.fallback.http.requestTimeout');\n          break;\n        }\n        default: {\n          errorMessage = $t('ui.fallback.http.internalServerError');\n        }\n      }\n      makeErrorMessage?.(errorMessage, error);\n      return Promise.reject(error);\n    },\n  };\n};\n"
  },
  {
    "path": "packages/effects/request/src/request-client/request-client.test.ts",
    "content": "import axios from 'axios';\nimport MockAdapter from 'axios-mock-adapter';\nimport { afterEach, beforeEach, describe, expect, it } from 'vitest';\n\nimport { RequestClient } from './request-client';\n\ndescribe('requestClient', () => {\n  let mock: MockAdapter;\n  let requestClient: RequestClient;\n\n  beforeEach(() => {\n    mock = new MockAdapter(axios);\n    requestClient = new RequestClient();\n  });\n\n  afterEach(() => {\n    mock.reset();\n  });\n\n  it('should successfully make a GET request', async () => {\n    mock.onGet('test/url').reply(200, { data: 'response' });\n\n    const response = await requestClient.get('test/url');\n\n    expect(response.data).toEqual({ data: 'response' });\n  });\n\n  it('should successfully make a POST request', async () => {\n    const postData = { key: 'value' };\n    const mockData = { data: 'response' };\n    mock.onPost('/test/post', postData).reply(200, mockData);\n    const response = await requestClient.post('/test/post', postData);\n    expect(response.data).toEqual(mockData);\n  });\n\n  it('should successfully make a PUT request', async () => {\n    const putData = { key: 'updatedValue' };\n    const mockData = { data: 'updated response' };\n    mock.onPut('/test/put', putData).reply(200, mockData);\n    const response = await requestClient.put('/test/put', putData);\n    expect(response.data).toEqual(mockData);\n  });\n\n  it('should successfully make a DELETE request', async () => {\n    const mockData = { data: 'delete response' };\n    mock.onDelete('/test/delete').reply(200, mockData);\n    const response = await requestClient.delete('/test/delete');\n    expect(response.data).toEqual(mockData);\n  });\n\n  it('should handle network errors', async () => {\n    mock.onGet('/test/error').networkError();\n    try {\n      await requestClient.get('/test/error');\n      expect(true).toBe(false);\n    } catch (error: any) {\n      expect(error.isAxiosError).toBe(true);\n      expect(error.message).toBe('Network Error');\n    }\n  });\n\n  it('should handle timeout', async () => {\n    mock.onGet('/test/timeout').timeout();\n    try {\n      await requestClient.get('/test/timeout');\n      expect(true).toBe(false);\n    } catch (error: any) {\n      expect(error.isAxiosError).toBe(true);\n      expect(error.code).toBe('ECONNABORTED');\n    }\n  });\n\n  it('should successfully upload a file', async () => {\n    const fileData = new Blob(['file contents'], { type: 'text/plain' });\n\n    mock.onPost('/test/upload').reply((config) => {\n      return config.data instanceof FormData && config.data.has('file')\n        ? [200, { data: 'file uploaded' }]\n        : [400, { error: 'Bad Request' }];\n    });\n\n    const response = await requestClient.upload('/test/upload', {\n      file: fileData,\n    });\n    expect(response.data).toEqual({ data: 'file uploaded' });\n  });\n\n  it('should successfully download a file as a blob', async () => {\n    const mockFileContent = new Blob(['mock file content'], {\n      type: 'text/plain',\n    });\n\n    mock.onGet('/test/download').reply(200, mockFileContent);\n\n    const res = await requestClient.download('/test/download');\n\n    expect(res.data).toBeInstanceOf(Blob);\n  });\n});\n"
  },
  {
    "path": "packages/effects/request/src/request-client/request-client.ts",
    "content": "import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';\n\nimport type { RequestClientConfig, RequestClientOptions } from './types';\n\nimport { bindMethods, isString, merge } from '@vben/utils';\n\nimport axios from 'axios';\nimport qs from 'qs';\n\nimport { FileDownloader } from './modules/downloader';\nimport { InterceptorManager } from './modules/interceptor';\nimport { SSE } from './modules/sse';\nimport { FileUploader } from './modules/uploader';\n\nfunction getParamsSerializer(\n  paramsSerializer: RequestClientOptions['paramsSerializer'],\n) {\n  if (isString(paramsSerializer)) {\n    switch (paramsSerializer) {\n      case 'brackets': {\n        return (params: any) =>\n          qs.stringify(params, { arrayFormat: 'brackets' });\n      }\n      case 'comma': {\n        return (params: any) => qs.stringify(params, { arrayFormat: 'comma' });\n      }\n      case 'indices': {\n        return (params: any) =>\n          qs.stringify(params, { arrayFormat: 'indices' });\n      }\n      case 'repeat': {\n        return (params: any) => qs.stringify(params, { arrayFormat: 'repeat' });\n      }\n    }\n  }\n  return paramsSerializer;\n}\n\nclass RequestClient {\n  public addRequestInterceptor: InterceptorManager['addRequestInterceptor'];\n\n  public addResponseInterceptor: InterceptorManager['addResponseInterceptor'];\n  public download: FileDownloader['download'];\n\n  public readonly instance: AxiosInstance;\n  // 是否正在刷新token\n  public isRefreshing = false;\n  public postSSE: SSE['postSSE'];\n  // 刷新token队列\n  public refreshTokenQueue: ((token: string) => void)[] = [];\n  public requestSSE: SSE['requestSSE'];\n  public upload: FileUploader['upload'];\n\n  /**\n   * 构造函数，用于创建Axios实例\n   * @param options - Axios请求配置，可选\n   */\n  constructor(options: RequestClientOptions = {}) {\n    // 合并默认配置和传入的配置\n    const defaultConfig: RequestClientOptions = {\n      headers: {\n        'Content-Type': 'application/json;charset=utf-8',\n      },\n      responseReturn: 'raw',\n      // 默认超时时间\n      timeout: 10_000,\n    };\n    const { ...axiosConfig } = options;\n    const requestConfig = merge(axiosConfig, defaultConfig);\n    requestConfig.paramsSerializer = getParamsSerializer(\n      requestConfig.paramsSerializer,\n    );\n    this.instance = axios.create(requestConfig);\n\n    bindMethods(this);\n\n    // 实例化拦截器管理器\n    const interceptorManager = new InterceptorManager(this.instance);\n    this.addRequestInterceptor =\n      interceptorManager.addRequestInterceptor.bind(interceptorManager);\n    this.addResponseInterceptor =\n      interceptorManager.addResponseInterceptor.bind(interceptorManager);\n\n    // 实例化文件上传器\n    const fileUploader = new FileUploader(this);\n    this.upload = fileUploader.upload.bind(fileUploader);\n    // 实例化文件下载器\n    const fileDownloader = new FileDownloader(this);\n    this.download = fileDownloader.download.bind(fileDownloader);\n    // 实例化SSE模块\n    const sse = new SSE(this);\n    this.postSSE = sse.postSSE.bind(sse);\n    this.requestSSE = sse.requestSSE.bind(sse);\n  }\n\n  /**\n   * DELETE请求方法\n   */\n  public delete<T = any>(\n    url: string,\n    config?: RequestClientConfig,\n  ): Promise<T> {\n    return this.request<T>(url, { ...config, method: 'DELETE' });\n  }\n\n  /**\n   * DELETE请求方法 成功会弹出msg\n   */\n  public deleteWithMsg<T = any>(\n    url: string,\n    config?: AxiosRequestConfig,\n  ): Promise<T> {\n    return this.request<T>(url, {\n      ...config,\n      method: 'DELETE',\n      successMessageMode: 'message',\n    });\n  }\n\n  /**\n   * GET请求方法\n   */\n  public get<T = any>(url: string, config?: RequestClientConfig): Promise<T> {\n    return this.request<T>(url, { ...config, method: 'GET' });\n  }\n\n  /**\n   * 获取基础URL\n   */\n  public getBaseUrl() {\n    return this.instance.defaults.baseURL;\n  }\n\n  /**\n   * POST请求方法\n   */\n  public post<T = any>(\n    url: string,\n    data?: any,\n    config?: RequestClientConfig,\n  ): Promise<T> {\n    return this.request<T>(url, { ...config, data, method: 'POST' });\n  }\n\n  /**\n   * POST请求方法 成功会弹出msg\n   */\n  public postWithMsg<T = any>(\n    url: string,\n    data?: any,\n    config?: AxiosRequestConfig,\n  ): Promise<T> {\n    return this.request<T>(url, {\n      ...config,\n      data,\n      method: 'POST',\n      successMessageMode: 'message',\n    });\n  }\n\n  /**\n   * PUT请求方法\n   */\n  public put<T = any>(\n    url: string,\n    data?: any,\n    config?: RequestClientConfig,\n  ): Promise<T> {\n    return this.request<T>(url, { ...config, data, method: 'PUT' });\n  }\n\n  /**\n   * PUT请求方法 成功会弹出msg\n   */\n  public putWithMsg<T = any>(\n    url: string,\n    data?: any,\n    config?: AxiosRequestConfig,\n  ): Promise<T> {\n    return this.request<T>(url, {\n      ...config,\n      data,\n      method: 'PUT',\n      successMessageMode: 'message',\n    });\n  }\n\n  /**\n   * 通用的请求方法\n   */\n  public async request<T>(\n    url: string,\n    config: RequestClientConfig,\n  ): Promise<T> {\n    try {\n      const response: AxiosResponse<T> = await this.instance({\n        url,\n        ...config,\n        ...(config.paramsSerializer\n          ? { paramsSerializer: getParamsSerializer(config.paramsSerializer) }\n          : {}),\n      });\n      return response as T;\n    } catch (error: any) {\n      throw error.response ? error.response.data : error;\n    }\n  }\n}\n\nexport { RequestClient };\n"
  },
  {
    "path": "packages/effects/request/src/request-client/types.ts",
    "content": "import type {\n  AxiosRequestConfig,\n  AxiosResponse,\n  CreateAxiosDefaults,\n  InternalAxiosRequestConfig,\n} from 'axios';\n\ntype ExtendOptions<T = any> = {\n  /**\n   * 参数序列化方式。预置的有\n   * - brackets: ids[]=1&ids[]=2&ids[]=3\n   * - comma: ids=1,2,3\n   * - indices: ids[0]=1&ids[1]=2&ids[2]=3\n   * - repeat: ids=1&ids=2&ids=3\n   */\n  paramsSerializer?:\n    | 'brackets'\n    | 'comma'\n    | 'indices'\n    | 'repeat'\n    | AxiosRequestConfig<T>['paramsSerializer'];\n  /**\n   * 响应数据的返回方式。\n   * - raw: 原始的AxiosResponse，包括headers、status等，不做是否成功请求的检查。\n   * - body: 返回响应数据的BODY部分（只会根据status检查请求是否成功，忽略对code的判断，这种情况下应由调用方检查请求是否成功）。\n   * - data: 解构响应的BODY数据，只返回其中的data节点数据（会检查status和code是否为成功状态）。\n   */\n  responseReturn?: 'body' | 'data' | 'raw';\n};\ntype RequestClientConfig<T = any> = AxiosRequestConfig<T> & ExtendOptions<T>;\n\ntype RequestResponse<T = any> = AxiosResponse<T> & {\n  config: RequestClientConfig<T>;\n};\n\ntype RequestContentType =\n  | 'application/json;charset=utf-8'\n  | 'application/octet-stream;charset=utf-8'\n  | 'application/x-www-form-urlencoded;charset=utf-8'\n  | 'multipart/form-data;charset=utf-8';\n\ntype RequestClientOptions = CreateAxiosDefaults & ExtendOptions;\n\n/**\n * SSE 请求选项\n */\ninterface SseRequestOptions extends RequestInit {\n  onMessage?: (message: string) => void;\n  onEnd?: () => void;\n}\n\ninterface RequestInterceptorConfig {\n  fulfilled?: (\n    config: ExtendOptions & InternalAxiosRequestConfig,\n  ) =>\n    | (ExtendOptions & InternalAxiosRequestConfig<any>)\n    | Promise<ExtendOptions & InternalAxiosRequestConfig<any>>;\n  rejected?: (error: any) => any;\n}\n\ninterface ResponseInterceptorConfig<T = any> {\n  fulfilled?: (\n    response: RequestResponse<T>,\n  ) => Promise<RequestResponse> | RequestResponse;\n  rejected?: (error: any) => any;\n}\n\ntype MakeErrorMessageFn = (message: string, error: any) => void;\n\ninterface HttpResponse<T = any> {\n  code: number;\n  data: T;\n  msg: string;\n}\n\nexport type {\n  HttpResponse,\n  MakeErrorMessageFn,\n  RequestClientConfig,\n  RequestClientOptions,\n  RequestContentType,\n  RequestInterceptorConfig,\n  RequestResponse,\n  ResponseInterceptorConfig,\n  SseRequestOptions,\n};\n\nexport type ErrorMessageMode = 'message' | 'modal' | 'none' | undefined;\nexport type SuccessMessageMode = ErrorMessageMode;\n\n/**\n * 拓展axios的请求配置\n */\ndeclare module 'axios' {\n  interface AxiosRequestConfig {\n    /** 是否加密请求参数  */\n    encrypt?: boolean;\n    /**\n     * 错误弹窗类型\n     */\n    errorMessageMode?: ErrorMessageMode;\n    /**\n     * 是否返回原生axios响应\n     */\n    isReturnNativeResponse?: boolean;\n    /**\n     * 是否需要转换响应 即只获取{code, msg, data}中的data\n     */\n    isTransformResponse?: boolean;\n    /**\n     * 成功弹窗类型\n     */\n    successMessageMode?: SuccessMessageMode;\n  }\n}\n"
  },
  {
    "path": "packages/effects/request/tsconfig.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/tsconfig\",\n  \"extends\": \"@vben/tsconfig/web.json\",\n  \"include\": [\"src\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "packages/icons/README.md",
    "content": "# @vben/icons\n\n用于多个 `app` 公用的图标文件，继承了 `@vben-core/icons` 的所有能力。业务上有通用图标可以放在这里。\n\n## 用法\n\n### 添加依赖\n\n```bash\n# 进入目标应用目录，例如 apps/xxxx-app\n# cd apps/xxxx-app\npnpm add @vben/icons\n```\n\n### 使用\n\n```ts\nimport { X } from '@vben/icons';\n```\n"
  },
  {
    "path": "packages/icons/package.json",
    "content": "{\n  \"name\": \"@vben/icons\",\n  \"version\": \"5.5.9\",\n  \"homepage\": \"https://github.com/vbenjs/vue-vben-admin\",\n  \"bugs\": \"https://github.com/vbenjs/vue-vben-admin/issues\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/vbenjs/vue-vben-admin.git\",\n    \"directory\": \"packages/icons\"\n  },\n  \"license\": \"MIT\",\n  \"type\": \"module\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./src/index.ts\",\n      \"default\": \"./src/index.ts\"\n    }\n  },\n  \"dependencies\": {\n    \"@vben-core/icons\": \"workspace:*\",\n    \"@vben-core/shadcn-ui\": \"workspace:^\"\n  },\n  \"devDependencies\": {\n    \"@iconify/icons-akar-icons\": \"^1.2.19\",\n    \"@iconify/icons-ant-design\": \"^1.2.7\",\n    \"@iconify/icons-arcticons\": \"^1.2.77\",\n    \"@iconify/icons-bi\": \"^1.2.19\",\n    \"@iconify/icons-bx\": \"^1.2.6\",\n    \"@iconify/icons-carbon\": \"^1.2.20\",\n    \"@iconify/icons-devicon\": \"^1.2.17\",\n    \"@iconify/icons-emojione\": \"^1.2.6\",\n    \"@iconify/icons-eos-icons\": \"^1.2.6\",\n    \"@iconify/icons-fa-brands\": \"^1.2.4\",\n    \"@iconify/icons-fe\": \"^1.2.5\",\n    \"@iconify/icons-flat-color-icons\": \"^1.2.5\",\n    \"@iconify/icons-fluent\": \"^1.2.38\",\n    \"@iconify/icons-fluent-mdl2\": \"^1.2.1\",\n    \"@iconify/icons-ic\": \"^1.2.13\",\n    \"@iconify/icons-icon-park-outline\": \"^1.2.11\",\n    \"@iconify/icons-icon-park-twotone\": \"^1.2.8\",\n    \"@iconify/icons-la\": \"^1.2.3\",\n    \"@iconify/icons-logos\": \"^1.2.36\",\n    \"@iconify/icons-lucide\": \"^1.2.135\",\n    \"@iconify/icons-majesticons\": \"^1.2.6\",\n    \"@iconify/icons-material-symbols\": \"^1.2.58\",\n    \"@iconify/icons-mdi\": \"^1.2.48\",\n    \"@iconify/icons-mingcute\": \"^1.2.9\",\n    \"@iconify/icons-noto\": \"^1.2.10\",\n    \"@iconify/icons-ph\": \"^1.2.5\",\n    \"@iconify/icons-ri\": \"^1.2.10\",\n    \"@iconify/icons-simple-icons\": \"^1.2.74\",\n    \"@iconify/icons-skill-icons\": \"^1.2.1\",\n    \"@iconify/icons-solar\": \"^1.2.3\",\n    \"@iconify/icons-streamline\": \"^1.2.3\",\n    \"@iconify/icons-tabler\": \"^1.2.95\",\n    \"@iconify/icons-uiw\": \"^1.2.6\",\n    \"@iconify/icons-vscode-icons\": \"^1.2.29\",\n    \"@iconify/icons-wpf\": \"^1.2.3\"\n  }\n}\n"
  },
  {
    "path": "packages/icons/src/iconify/index.ts",
    "content": "import { createIconifyIcon } from '@vben-core/icons';\n\nexport * from '@vben-core/icons';\n\nexport const MdiKeyboardEsc = createIconifyIcon('mdi:keyboard-esc');\n\nexport const MdiWechat = createIconifyIcon('mdi:wechat');\n\nexport const MdiGithub = createIconifyIcon('mdi:github');\n\nexport const MdiGoogle = createIconifyIcon('mdi:google');\n\nexport const MdiQqchat = createIconifyIcon('mdi:qqchat');\n\nexport const EosSystem = createIconifyIcon('eos-icons:system-group');\n\n// 个人中心\nexport const ProfileIcon = createIconifyIcon('mingcute:profile-line');\nexport const RiDingding = createIconifyIcon('ri:dingding-fill');\n"
  },
  {
    "path": "packages/icons/src/iconify-offline/index.ts",
    "content": "import { createIconifyOfflineIcon } from '@vben-core/icons';\n\nimport githubOutlined from '@iconify/icons-ant-design/github-outlined';\nimport inboxIcon from '@iconify/icons-ant-design/inbox-outlined';\nimport userOutlined from '@iconify/icons-ant-design/user-outlined';\nimport ucIcon from '@iconify/icons-arcticons/uc-browser';\nimport defaultFileIcon from '@iconify/icons-bx/file';\nimport sqlIcon from '@iconify/icons-carbon/sql';\nimport linuxIcon from '@iconify/icons-devicon/linux';\nimport windowsIcon from '@iconify/icons-devicon/windows8';\nimport alipayIcon from '@iconify/icons-fa-brands/alipay';\nimport androidIcon from '@iconify/icons-flat-color-icons/android-os';\nimport comandLine from '@iconify/icons-flat-color-icons/command-line';\nimport folderIcon from '@iconify/icons-flat-color-icons/folder';\nimport defaultOsIcon from '@iconify/icons-ic/outline-computer';\nimport memoryIcon from '@iconify/icons-la/memory';\nimport chromeIcon from '@iconify/icons-logos/chrome';\nimport firefoxIcon from '@iconify/icons-logos/firefox';\nimport edgeIcon from '@iconify/icons-logos/microsoft-edge';\nimport operaIcon from '@iconify/icons-logos/opera';\nimport quarkIcon from '@iconify/icons-logos/quarkus-icon';\nimport redisIcon from '@iconify/icons-logos/redis';\nimport safariIcon from '@iconify/icons-logos/safari';\nimport vueIcon from '@iconify/icons-logos/vue';\nimport iphoneIcon from '@iconify/icons-majesticons/iphone-x-apps-line';\nimport menuIcon from '@iconify/icons-material-symbols/menu';\nimport okButtonIcon from '@iconify/icons-mdi/button-pointer';\nimport micromessengerIcon from '@iconify/icons-mdi/wechat';\nimport defaultBrowserIcon from '@iconify/icons-ph/browser-duotone';\nimport baiduIcon from '@iconify/icons-ri/baidu-fill';\nimport dingdingFill from '@iconify/icons-ri/dingding-fill';\nimport dingtalkIcon from '@iconify/icons-ri/dingding-line';\nimport taobaoIconFill from '@iconify/icons-ri/taobao-fill';\nimport giteeIcon from '@iconify/icons-simple-icons/gitee';\nimport qqIcon from '@iconify/icons-simple-icons/tencentqq';\nimport javaIcon from '@iconify/icons-skill-icons/java-light';\nimport tsIcon from '@iconify/icons-skill-icons/typescript';\nimport xmlIcon from '@iconify/icons-tabler/file-type-xml';\nimport githubOAuthIcon from '@iconify/icons-uiw/github';\nimport excelIcon from '@iconify/icons-vscode-icons/file-type-excel';\nimport osxIcon from '@iconify/icons-wpf/macos';\n\nimport './menu-icons';\n\n// 用户 下拉菜单\nexport const GitHubOutlined = createIconifyOfflineIcon(\n  'ant-design:github-outlined',\n  githubOutlined,\n);\n\nexport const UserOutlined = createIconifyOfflineIcon(\n  'ant-design:user-outlined',\n  userOutlined,\n);\n\n// 缓存监控使用\nexport const RedisIcon = createIconifyOfflineIcon('logos:redis', redisIcon);\nexport const CommandLineIcon = createIconifyOfflineIcon(\n  'flat-color-icons:command-line',\n  comandLine,\n);\nexport const MemoryIcon = createIconifyOfflineIcon('la:memory', memoryIcon);\n\n// 用户管理 导入\n// Excel图标\nexport const ExcelIcon = createIconifyOfflineIcon(\n  'vscode-icons:file-type-excel',\n  excelIcon,\n);\n// 拖拽上传图标\nexport const InBoxIcon = createIconifyOfflineIcon(\n  'ant-design:inbox-outlined',\n  inboxIcon,\n);\n\n// 第三方登录相关图标\nexport const TaobaoIcon = createIconifyOfflineIcon(\n  'ri:taobao-fill',\n  taobaoIconFill,\n);\nexport const AlipayIcon = createIconifyOfflineIcon(\n  'fa-brands:alipay',\n  alipayIcon,\n);\nexport const DingdingIcon = createIconifyOfflineIcon(\n  'ri:dingding-fill',\n  dingdingFill,\n);\nexport const GiteeIcon = createIconifyOfflineIcon(\n  'simple-icons:gitee',\n  giteeIcon,\n);\nexport const GithubOAuthIcon = createIconifyOfflineIcon(\n  'uiw:github',\n  githubOAuthIcon,\n);\n\n// 系统相关图标\nexport const WindowsIcon = createIconifyOfflineIcon(\n  'devicon:windows8',\n  windowsIcon,\n);\nexport const LinuxIcon = createIconifyOfflineIcon('devicon:linux', linuxIcon);\nexport const OSXIcon = createIconifyOfflineIcon('wpf:macos', osxIcon);\nexport const AndroidIcon = createIconifyOfflineIcon(\n  'flat-color-icons:android-os',\n  androidIcon,\n);\nexport const IPhoneIcon = createIconifyOfflineIcon(\n  'majesticons:iphone-x-apps-line',\n  iphoneIcon,\n);\n// 上面图标没找到 默认图标\nexport const DefaultOsIcon = createIconifyOfflineIcon(\n  'ic:outline-computer',\n  defaultOsIcon,\n);\n\n// 浏览器相关图标\nexport const ChromeIcon = createIconifyOfflineIcon('logos:chrome', chromeIcon);\nexport const EdgeIcon = createIconifyOfflineIcon(\n  'logos:microsoft-edge',\n  edgeIcon,\n);\nexport const FirefoxIcon = createIconifyOfflineIcon(\n  'logos:firefox',\n  firefoxIcon,\n);\nexport const OperaIcon = createIconifyOfflineIcon('logos:opera', operaIcon);\nexport const SafariIcon = createIconifyOfflineIcon('logos:safari', safariIcon);\nexport const MicromessengerIcon = createIconifyOfflineIcon(\n  'mdi:wechat',\n  micromessengerIcon,\n);\nexport const QuarkIcon = createIconifyOfflineIcon(\n  'logos:quarkus-icon',\n  quarkIcon,\n);\nexport const QQIcon = createIconifyOfflineIcon(\n  'simple-icons:tencentqq',\n  qqIcon,\n);\nexport const DingtalkIcon = createIconifyOfflineIcon(\n  'ri:dingding-line',\n  dingtalkIcon,\n);\nexport const UcIcon = createIconifyOfflineIcon('arcticons:uc-browser', ucIcon);\nexport const BaiduIcon = createIconifyOfflineIcon('ri:baidu-fill', baiduIcon);\n// 未知浏览器图标\nexport const DefaultBrowserIcon = createIconifyOfflineIcon(\n  'ph:browser-duotone',\n  defaultBrowserIcon,\n);\n\n// 菜单类型 目录/按钮/菜单\nexport const FolderIcon = createIconifyOfflineIcon(\n  'flat-color-icons:folder',\n  folderIcon,\n);\nexport const OkButtonIcon = createIconifyOfflineIcon(\n  'mdi:button-pointer',\n  okButtonIcon,\n);\nexport const MenuIcon = createIconifyOfflineIcon(\n  'material-symbols:menu',\n  menuIcon,\n);\n\nexport const JavaIcon = createIconifyOfflineIcon(\n  'skill-icons:java-light',\n  javaIcon,\n);\nexport const XmlIcon = createIconifyOfflineIcon(\n  'tabler:file-type-xml',\n  xmlIcon,\n);\nexport const SqlIcon = createIconifyOfflineIcon('carbon:sql', sqlIcon);\nexport const TsIcon = createIconifyOfflineIcon(\n  'skill-icons:typescript',\n  tsIcon,\n);\nexport const VueIcon = createIconifyOfflineIcon('logos:vue', vueIcon);\nexport const DefaultFileIcon = createIconifyOfflineIcon(\n  'flat-color-icons:folder',\n  defaultFileIcon,\n);\n"
  },
  {
    "path": "packages/icons/src/iconify-offline/menu-icons.ts",
    "content": "import { addIcon } from '@vben-core/icons';\n\nimport schedule from '@iconify/icons-akar-icons/schedule';\nimport settingOutline from '@iconify/icons-ant-design/setting-outlined';\nimport antdTool from '@iconify/icons-ant-design/tool-outlined';\nimport UserAntd from '@iconify/icons-ant-design/user-outlined';\nimport Operation from '@iconify/icons-arcticons/one-hand-operation';\nimport BaseLineHousesFill from '@iconify/icons-bi/houses-fill';\nimport BxPackage from '@iconify/icons-bx/package';\nimport modelAlt from '@iconify/icons-carbon/model-alt';\nimport taskApproved from '@iconify/icons-carbon/task-approved';\nimport redisWordmark from '@iconify/icons-devicon/redis-wordmark';\nimport springWordmark from '@iconify/icons-devicon/spring-wordmark';\nimport vscode from '@iconify/icons-devicon/vscode';\nimport evergreenTree from '@iconify/icons-emojione/evergreen-tree';\nimport RoleBindingOutlined from '@iconify/icons-eos-icons/role-binding-outlined';\nimport SystemGroup from '@iconify/icons-eos-icons/system-group';\nimport NoticePush from '@iconify/icons-fe/notice-push';\nimport leave from '@iconify/icons-flat-color-icons/leave';\nimport plus from '@iconify/icons-flat-color-icons/plus';\nimport builDefinition from '@iconify/icons-fluent-mdl2/build-definition';\nimport Dictionary from '@iconify/icons-fluent-mdl2/dictionary';\nimport flow from '@iconify/icons-fluent-mdl2/flow';\nimport leaveUser from '@iconify/icons-fluent-mdl2/leave-user';\nimport from24 from '@iconify/icons-fluent/form-24-regular';\nimport BaseLineHouse from '@iconify/icons-ic/baseline-house';\nimport monitor from '@iconify/icons-ic/baseline-monitor';\nimport roundLaunch from '@iconify/icons-ic/round-launch';\nimport MenuSharp from '@iconify/icons-ic/sharp-menu';\nimport Appointment from '@iconify/icons-icon-park-outline/appointment';\nimport SettingTwo from '@iconify/icons-icon-park-twotone/setting-two';\nimport boolOpenText from '@iconify/icons-lucide/book-open-text';\nimport copyright from '@iconify/icons-lucide/copyright';\nimport table from '@iconify/icons-lucide/table';\nimport cloudDoneOutlineRounded from '@iconify/icons-material-symbols/cloud-done-outline-rounded';\nimport generatingTokensOutline from '@iconify/icons-material-symbols/generating-tokens-outline';\nimport LogoDevOutline from '@iconify/icons-material-symbols/logo-dev-outline';\nimport expressionIcon from '@iconify/icons-material-symbols/regular-expression-rounded';\nimport ccOutline from '@iconify/icons-mdi/cc-outline';\nimport tools from '@iconify/icons-mdi/tools';\nimport workflowOutline from '@iconify/icons-mdi/workflow-outline';\nimport DepartmentLine from '@iconify/icons-mingcute/department-line';\nimport profileLine from '@iconify/icons-mingcute/profile-line';\nimport UserDuotone from '@iconify/icons-ph/user-duotone';\nimport userList from '@iconify/icons-ph/user-list';\nimport users from '@iconify/icons-ph/users-light';\nimport insatnceLine from '@iconify/icons-ri/instance-line';\nimport todoLine from '@iconify/icons-ri/todo-line';\nimport Authy from '@iconify/icons-simple-icons/authy';\nimport FolderWithFilesOutline from '@iconify/icons-solar/folder-with-files-outline';\nimport monitorBoldDuotone from '@iconify/icons-solar/monitor-bold-duotone';\nimport monitorCameraOutlined from '@iconify/icons-solar/monitor-camera-outline';\nimport monitorPhoneOutlined from '@iconify/icons-solar/monitor-smartphone-outline';\nimport InterfaceLoginDialPadFingerPasswordDialPadDotFinger from '@iconify/icons-streamline/interface-login-dial-pad-finger-password-dial-pad-dot-finger';\nimport categoryPlus from '@iconify/icons-tabler/category-plus';\nimport code from '@iconify/icons-tabler/code';\n\n/**\n * 这里添加菜单图标\n */\naddIcon('eos-icons:system-group', SystemGroup);\naddIcon('ph:user-duotone', UserDuotone);\naddIcon('ant-design:user-outlined', UserAntd);\naddIcon('eos-icons:role-binding-outlined', RoleBindingOutlined);\naddIcon('ic:sharp-menu', MenuSharp);\naddIcon('mingcute:department-line', DepartmentLine);\naddIcon('icon-park-outline:appointment', Appointment);\naddIcon('fluent-mdl2:dictionary', Dictionary);\naddIcon('icon-park-twotone:setting-two', SettingTwo);\naddIcon('fe:notice-push', NoticePush);\naddIcon('material-symbols:logo-dev-outline', LogoDevOutline);\naddIcon('arcticons:one-hand-operation', Operation);\naddIcon(\n  'streamline:interface-login-dial-pad-finger-password-dial-pad-dot-finger',\n  InterfaceLoginDialPadFingerPasswordDialPadDotFinger,\n);\naddIcon('solar:folder-with-files-outline', FolderWithFilesOutline);\naddIcon('simple-icons:authy', Authy);\naddIcon('solar:monitor-smartphone-outline', monitorPhoneOutlined);\naddIcon('ic:baseline-house', BaseLineHouse);\naddIcon('ph:users-light', users);\naddIcon('bi:houses-fill', BaseLineHousesFill);\naddIcon('ph:user-list', userList);\naddIcon('bx:package', BxPackage);\naddIcon('solar:monitor-bold-duotone', monitorBoldDuotone);\naddIcon('solar:monitor-camera-outline', monitorCameraOutlined);\naddIcon('material-symbols:generating-tokens-outline', generatingTokensOutline);\naddIcon('devicon:redis-wordmark', redisWordmark);\naddIcon('devicon:spring-wordmark', springWordmark);\naddIcon('akar-icons:schedule', schedule);\naddIcon('mdi:tools', tools);\naddIcon('ant-design:tool-outlined', antdTool);\naddIcon('tabler:code', code);\naddIcon('flat-color-icons:plus', plus);\naddIcon('devicon:vscode', vscode);\naddIcon('lucide:table', table);\naddIcon('emojione:evergreen-tree', evergreenTree);\naddIcon('fluent-mdl2:leave-user', leaveUser);\naddIcon('mdi:workflow-outline', workflowOutline);\naddIcon('tabler:category-plus', categoryPlus);\naddIcon('carbon:model-alt', modelAlt);\naddIcon('fluent-mdl2:build-definition', builDefinition);\naddIcon('fluent-mdl2:build-definition', builDefinition);\naddIcon('icon-park-outline:monitor', monitor);\naddIcon('ri:instance-line', insatnceLine);\naddIcon('ri:todo-line', todoLine);\naddIcon('fluent:form-24-regular', from24);\naddIcon('carbon:task-approved', taskApproved);\naddIcon('ic:round-launch', roundLaunch);\naddIcon('material-symbols:cloud-done-outline-rounded', cloudDoneOutlineRounded);\naddIcon('mdi:cc-outline', ccOutline);\naddIcon('lucide:book-open-text', boolOpenText);\naddIcon('lucide:copyright', copyright);\n// 个人中心\naddIcon('mingcute:profile-line', profileLine);\n// oss配置\naddIcon('ant-design:setting-outlined', settingOutline);\n// 请假\naddIcon('flat-color-icons:leave', leave);\n// flow\naddIcon('fluent-mdl2:flow', flow);\n// 流程表达式\naddIcon('material-symbols:regular-expression-rounded', expressionIcon);\n"
  },
  {
    "path": "packages/icons/src/icons/empty-icon.vue",
    "content": "<template>\n  <svg\n    height=\"41\"\n    viewBox=\"0 0 64 41\"\n    width=\"64\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n  >\n    <g fill=\"none\" fill-rule=\"evenodd\" transform=\"translate(0 1)\">\n      <ellipse\n        cx=\"32\"\n        cy=\"33\"\n        fill=\"hsl(var(--background-deep))\"\n        rx=\"32\"\n        ry=\"7\"\n      />\n      <g fill-rule=\"nonzero\" stroke=\"hsl(var(--heavy))\">\n        <path\n          d=\"M55 12.76L44.854 1.258C44.367.474 43.656 0 42.907 0H21.093c-.749 0-1.46.474-1.947 1.257L9 12.761V22h46v-9.24z\"\n        />\n        <path\n          d=\"M41.613 15.931c0-1.605.994-2.93 2.227-2.931H55v18.137C55 33.26 53.68 35 52.05 35h-40.1C10.32 35 9 33.259 9 31.137V13h11.16c1.233 0 2.227 1.323 2.227 2.928v.022c0 1.605 1.005 2.901 2.237 2.901h14.752c1.232 0 2.237-1.308 2.237-2.913v-.007z\"\n          fill=\"hsl(var(--accent))\"\n        />\n      </g>\n    </g>\n  </svg>\n</template>\n"
  },
  {
    "path": "packages/icons/src/index.ts",
    "content": "export * from './iconify';\nexport * from './iconify-offline';\nexport { default as EmptyIcon } from './icons/empty-icon.vue';\nexport * from './svg';\nexport { VbenIcon } from '@vben-core/shadcn-ui';\n"
  },
  {
    "path": "packages/icons/src/svg/index.ts",
    "content": "import { createIconifyIcon } from '@vben-core/icons';\n\nimport './load.js';\n\nconst SvgAvatar1Icon = createIconifyIcon('svg:avatar-1');\nconst SvgAvatar2Icon = createIconifyIcon('svg:avatar-2');\nconst SvgAvatar3Icon = createIconifyIcon('svg:avatar-3');\nconst SvgAvatar4Icon = createIconifyIcon('svg:avatar-4');\nconst SvgDownloadIcon = createIconifyIcon('svg:download');\nconst SvgCardIcon = createIconifyIcon('svg:card');\nconst SvgBellIcon = createIconifyIcon('svg:bell');\nconst SvgCakeIcon = createIconifyIcon('svg:cake');\nconst SvgAntdvLogoIcon = createIconifyIcon('svg:antdv-logo');\nconst SvgMaxKeyIcon = createIconifyIcon('svg:max-key');\nconst SvgTopiamIcon = createIconifyIcon('svg:topiam');\nconst SvgWechatIcon = createIconifyIcon('svg:wechat');\nconst SvgQQIcon = createIconifyIcon('svg:qq');\nconst SvgSnailJobIcon = createIconifyIcon('svg:snail-job');\n\nexport {\n  SvgAntdvLogoIcon,\n  SvgAvatar1Icon,\n  SvgAvatar2Icon,\n  SvgAvatar3Icon,\n  SvgAvatar4Icon,\n  SvgBellIcon,\n  SvgCakeIcon,\n  SvgCardIcon,\n  SvgDownloadIcon,\n  SvgMaxKeyIcon,\n  SvgQQIcon,\n  SvgSnailJobIcon,\n  SvgTopiamIcon,\n  SvgWechatIcon,\n};\n\nexport { default as SvgMessageUrl } from './icons/message.svg';\n"
  },
  {
    "path": "packages/icons/src/svg/load.ts",
    "content": "import type { IconifyIconStructure } from '@vben-core/icons';\n\nimport { addIcon } from '@vben-core/icons';\n\nlet loaded = false;\nif (!loaded) {\n  loadSvgIcons();\n  loaded = true;\n}\n\nfunction parseSvg(svgData: string): IconifyIconStructure {\n  const parser = new DOMParser();\n  const xmlDoc = parser.parseFromString(svgData, 'image/svg+xml');\n  const svgElement = xmlDoc.documentElement;\n\n  const svgContent = [...svgElement.childNodes]\n    .filter((node) => node.nodeType === Node.ELEMENT_NODE)\n    .map((node) => new XMLSerializer().serializeToString(node))\n    .join('');\n\n  const viewBoxValue = svgElement.getAttribute('viewBox') || '';\n  const [left, top, width, height] = viewBoxValue.split(' ').map((val) => {\n    const num = Number(val);\n    return Number.isNaN(num) ? undefined : num;\n  });\n\n  return {\n    body: svgContent,\n    height,\n    left,\n    top,\n    width,\n  };\n}\n\n/**\n * 自定义的svg图片转化为组件\n * @example ./svg/avatar.svg\n * <Icon icon=\"svg:avatar\"></Icon>\n */\nasync function loadSvgIcons() {\n  const svgEagers = import.meta.glob('./icons/**', {\n    eager: true,\n    query: '?raw',\n  });\n\n  await Promise.all(\n    Object.entries(svgEagers).map((svg) => {\n      const [key, body] = svg as [string, { default: string } | string];\n\n      // ./icons/xxxx.svg => xxxxxx\n      const start = key.lastIndexOf('/') + 1;\n      const end = key.lastIndexOf('.');\n      const iconName = key.slice(start, end);\n\n      return addIcon(`svg:${iconName}`, {\n        ...parseSvg(typeof body === 'object' ? body.default : body),\n      });\n    }),\n  );\n}\n"
  },
  {
    "path": "packages/icons/tsconfig.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/tsconfig\",\n  \"extends\": \"@vben/tsconfig/web.json\",\n  \"include\": [\"src\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "packages/locales/package.json",
    "content": "{\n  \"name\": \"@vben/locales\",\n  \"version\": \"5.5.9\",\n  \"homepage\": \"https://github.com/vbenjs/vue-vben-admin\",\n  \"bugs\": \"https://github.com/vbenjs/vue-vben-admin/issues\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/vbenjs/vue-vben-admin.git\",\n    \"directory\": \"packages/locales\"\n  },\n  \"license\": \"MIT\",\n  \"type\": \"module\",\n  \"sideEffects\": [\n    \"**/*.css\"\n  ],\n  \"exports\": {\n    \".\": {\n      \"types\": \"./src/index.ts\",\n      \"default\": \"./src/index.ts\"\n    }\n  },\n  \"dependencies\": {\n    \"@intlify/core-base\": \"catalog:\",\n    \"@vben-core/composables\": \"workspace:*\",\n    \"vue\": \"catalog:\",\n    \"vue-i18n\": \"catalog:\"\n  }\n}\n"
  },
  {
    "path": "packages/locales/src/i18n.ts",
    "content": "import type { App } from 'vue';\nimport type { Locale } from 'vue-i18n';\n\nimport type {\n  ImportLocaleFn,\n  LoadMessageFn,\n  LocaleSetupOptions,\n  SupportedLanguagesType,\n} from './typing';\n\nimport { useSimpleLocale } from '@vben-core/composables';\nimport { unref } from 'vue';\nimport { createI18n } from 'vue-i18n';\n\nconst i18n = createI18n({\n  globalInjection: true,\n  legacy: false,\n  locale: '',\n  messages: {},\n});\n\nconst modules = import.meta.glob('./langs/**/*.json');\n\nconst { setSimpleLocale } = useSimpleLocale();\n\nconst localesMap = loadLocalesMapFromDir(\n  /\\.\\/langs\\/([^/]+)\\/(.*)\\.json$/,\n  modules,\n);\nlet loadMessages: LoadMessageFn;\n\n/**\n * Load locale modules\n * @param modules\n */\nfunction loadLocalesMap(modules: Record<string, () => Promise<unknown>>) {\n  const localesMap: Record<Locale, ImportLocaleFn> = {};\n\n  for (const [path, loadLocale] of Object.entries(modules)) {\n    const key = path.match(/([\\w-]*)\\.(json)/)?.[1];\n    if (key) {\n      localesMap[key] = loadLocale as ImportLocaleFn;\n    }\n  }\n  return localesMap;\n}\n\n/**\n * Load locale modules with directory structure\n * @param regexp - Regular expression to match language and file names\n * @param modules - The modules object containing paths and import functions\n * @returns A map of locales to their corresponding import functions\n */\nfunction loadLocalesMapFromDir(\n  regexp: RegExp,\n  modules: Record<string, () => Promise<unknown>>,\n): Record<Locale, ImportLocaleFn> {\n  const localesRaw: Record<Locale, Record<string, () => Promise<unknown>>> = {};\n  const localesMap: Record<Locale, ImportLocaleFn> = {};\n\n  // Iterate over the modules to extract language and file names\n  for (const path in modules) {\n    const match = path.match(regexp);\n    if (match) {\n      const [_, locale, fileName] = match;\n      if (locale && fileName) {\n        if (!localesRaw[locale]) {\n          localesRaw[locale] = {};\n        }\n        if (modules[path]) {\n          localesRaw[locale][fileName] = modules[path];\n        }\n      }\n    }\n  }\n\n  // Convert raw locale data into async import functions\n  for (const [locale, files] of Object.entries(localesRaw)) {\n    localesMap[locale] = async () => {\n      const messages: Record<string, any> = {};\n      for (const [fileName, importFn] of Object.entries(files)) {\n        messages[fileName] = ((await importFn()) as any)?.default;\n      }\n      return { default: messages };\n    };\n  }\n\n  return localesMap;\n}\n\n/**\n * Set i18n language\n * @param locale\n */\nfunction setI18nLanguage(locale: Locale) {\n  i18n.global.locale.value = locale;\n\n  document?.querySelector('html')?.setAttribute('lang', locale);\n}\n\nasync function setupI18n(app: App, options: LocaleSetupOptions = {}) {\n  const { defaultLocale = 'zh-CN' } = options;\n  // app可以自行扩展一些第三方库和组件库的国际化\n  loadMessages = options.loadMessages || (async () => ({}));\n  app.use(i18n);\n  await loadLocaleMessages(defaultLocale);\n\n  // 在控制台打印警告\n  i18n.global.setMissingHandler((locale, key) => {\n    if (options.missingWarn && key.includes('.')) {\n      console.warn(\n        `[intlify] Not found '${key}' key in '${locale}' locale messages.`,\n      );\n    }\n  });\n}\n\n/**\n * Load locale messages\n * @param lang\n */\nasync function loadLocaleMessages(lang: SupportedLanguagesType) {\n  if (unref(i18n.global.locale) === lang) {\n    return setI18nLanguage(lang);\n  }\n  setSimpleLocale(lang);\n\n  const message = await localesMap[lang]?.();\n\n  if (message?.default) {\n    i18n.global.setLocaleMessage(lang, message.default);\n  }\n\n  const mergeMessage = await loadMessages(lang);\n  i18n.global.mergeLocaleMessage(lang, mergeMessage);\n\n  return setI18nLanguage(lang);\n}\n\nexport {\n  i18n,\n  loadLocaleMessages,\n  loadLocalesMap,\n  loadLocalesMapFromDir,\n  setupI18n,\n};\n"
  },
  {
    "path": "packages/locales/src/index.ts",
    "content": "import {\n  i18n,\n  loadLocaleMessages,\n  loadLocalesMap,\n  loadLocalesMapFromDir,\n  setupI18n,\n} from './i18n';\n\nconst $t = i18n.global.t;\nconst $te = i18n.global.te;\n\nexport {\n  $t,\n  $te,\n  i18n,\n  loadLocaleMessages,\n  loadLocalesMap,\n  loadLocalesMapFromDir,\n  setupI18n,\n};\nexport {\n  type ImportLocaleFn,\n  type LocaleSetupOptions,\n  type SupportedLanguagesType,\n} from './typing';\nexport type { CompileError } from '@intlify/core-base';\n\nexport { I18nT, useI18n } from 'vue-i18n';\n\nexport type { Locale } from 'vue-i18n';\n"
  },
  {
    "path": "packages/locales/src/langs/en-US/authentication.json",
    "content": "{\n  \"welcomeBack\": \"Welcome Back\",\n  \"pageTitle\": \"Plug-and-play Admin system\",\n  \"pageDesc\": \"Efficient, versatile frontend template\",\n  \"loginSuccess\": \"Login Successful\",\n  \"loginSuccessDesc\": \"Welcome Back\",\n  \"loginSubtitle\": \"Enter your account details to manage your projects\",\n  \"selectAccount\": \"Quick Select Account\",\n  \"username\": \"Username\",\n  \"password\": \"Password\",\n  \"usernameTip\": \"Please enter username\",\n  \"passwordErrorTip\": \"Password is incorrect\",\n  \"passwordTip\": \"Please enter password\",\n  \"verifyRequiredTip\": \"Please complete the verification first\",\n  \"rememberMe\": \"Remember Me\",\n  \"createAnAccount\": \"Create an Account\",\n  \"createAccount\": \"Create Account\",\n  \"alreadyHaveAccount\": \"Already have an account?\",\n  \"accountTip\": \"Don't have an account?\",\n  \"signUp\": \"Sign Up\",\n  \"signUpSubtitle\": \"Make managing your applications simple and fun\",\n  \"confirmPassword\": \"Confirm Password\",\n  \"confirmPasswordTip\": \"The passwords do not match\",\n  \"agree\": \"I agree to\",\n  \"privacyPolicy\": \"Privacy-policy\",\n  \"terms\": \"Terms\",\n  \"agreeTip\": \"Please agree to the Privacy Policy and Terms\",\n  \"goToLogin\": \"Login instead\",\n  \"passwordStrength\": \"Use 8 or more characters with a mix of letters, numbers & symbols\",\n  \"forgetPassword\": \"Forget Password?\",\n  \"forgetPasswordSubtitle\": \"Enter your email and we'll send you instructions to reset your password\",\n  \"emailTip\": \"Please enter email\",\n  \"emailValidErrorTip\": \"The email format you entered is incorrect\",\n  \"sendResetLink\": \"Send Reset Link\",\n  \"email\": \"Email\",\n  \"qrcodeSubtitle\": \"Scan the QR code with your phone to login\",\n  \"qrcodePrompt\": \"Click 'Confirm' after scanning to complete login\",\n  \"qrcodeLogin\": \"QR Code Login\",\n  \"wechatLogin\": \"Wechat Login\",\n  \"qqLogin\": \"QQ Login\",\n  \"githubLogin\": \"Github Login\",\n  \"googleLogin\": \"Google Login\",\n  \"dingdingLogin\": \"Dingding Login\",\n  \"codeSubtitle\": \"Enter your phone number to start managing your project\",\n  \"code\": \"Security code\",\n  \"codeTip\": \"Security code required {0} characters\",\n  \"mobile\": \"Mobile\",\n  \"mobileLogin\": \"Mobile Login\",\n  \"mobileTip\": \"Please enter mobile number\",\n  \"mobileErrortip\": \"The phone number format is incorrect\",\n  \"sendCode\": \"Get Security code\",\n  \"sendText\": \"Resend in {0}s\",\n  \"thirdPartyLogin\": \"Or continue with\",\n  \"weChat\": \"WeChat\",\n  \"qq\": \"QQ\",\n  \"gitHub\": \"GitHub\",\n  \"google\": \"Google\",\n  \"loginAgainTitle\": \"Please Log In Again\",\n  \"loginAgainSubTitle\": \"Your login session has expired. Please log in again to continue.\",\n  \"layout\": {\n    \"center\": \"Align Center\",\n    \"alignLeft\": \"Align Left\",\n    \"alignRight\": \"Align Right\"\n  }\n}\n"
  },
  {
    "path": "packages/locales/src/langs/en-US/common.json",
    "content": "{\n  \"back\": \"Back\",\n  \"backToHome\": \"Back To Home\",\n  \"login\": \"Login\",\n  \"logout\": \"Logout\",\n  \"prompt\": \"Prompt\",\n  \"cancel\": \"Cancel\",\n  \"confirm\": \"Confirm\",\n  \"reset\": \"Reset\",\n  \"noData\": \"No Data\",\n  \"refresh\": \"Refresh\",\n  \"loadingMenu\": \"Loading Menu\",\n  \"query\": \"Search\",\n  \"search\": \"Search\",\n  \"enabled\": \"Enabled\",\n  \"disabled\": \"Disabled\",\n  \"edit\": \"Edit\",\n  \"delete\": \"Delete\",\n  \"create\": \"Create\",\n  \"yes\": \"Yes\",\n  \"no\": \"No\",\n  \"showSearchPanel\": \"Show search panel\",\n  \"hideSearchPanel\": \"Hide search panel\"\n}\n"
  },
  {
    "path": "packages/locales/src/langs/en-US/preferences.json",
    "content": "{\n  \"title\": \"Preferences\",\n  \"subtitle\": \"Customize Preferences & Preview in Real Time\",\n  \"enableStickyPreferencesNavigationBar\": \"Enable sticky preferences navigation bar\",\n  \"disableStickyPreferencesNavigationBar\": \"Disable sticky preferences navigation bar\",\n  \"resetTip\": \"Data has changed, click to reset\",\n  \"resetTitle\": \"Reset Preferences\",\n  \"resetSuccess\": \"Preferences reset successfully\",\n  \"appearance\": \"Appearance\",\n  \"layout\": \"Layout\",\n  \"content\": \"Content\",\n  \"other\": \"Other\",\n  \"wide\": \"Wide\",\n  \"compact\": \"Fixed\",\n  \"followSystem\": \"Follow System\",\n  \"vertical\": \"Vertical\",\n  \"verticalTip\": \"Side vertical menu mode\",\n  \"horizontal\": \"Horizontal\",\n  \"horizontalTip\": \"Horizontal menu mode, all menus displayed at the top\",\n  \"twoColumn\": \"Two Column\",\n  \"twoColumnTip\": \"Vertical Two Column Menu Mode\",\n  \"headerSidebarNav\": \"Header Vertical\",\n  \"headerSidebarNavTip\": \"Header Full Width, Sidebar Navigation Mode\",\n  \"headerTwoColumn\": \"Header Two Column\",\n  \"headerTwoColumnTip\": \"Header Navigation & Sidebar Two Column co-exists\",\n  \"mixedMenu\": \"Mixed Menu\",\n  \"mixedMenuTip\": \"Vertical & Horizontal Menu Co-exists\",\n  \"fullContent\": \"Full Content\",\n  \"fullContentTip\": \"Only display content body, hide all menus\",\n  \"normal\": \"Normal\",\n  \"plain\": \"Plain\",\n  \"rounded\": \"Rounded\",\n  \"copyPreferences\": \"Copy Preferences\",\n  \"copyPreferencesSuccessTitle\": \"Copy successful\",\n  \"copyPreferencesSuccess\": \"Copy successful, please override in `src/preferences.ts` under app\",\n  \"clearAndLogout\": \"Clear Cache & Logout\",\n  \"mode\": \"Mode\",\n  \"general\": \"General\",\n  \"language\": \"Language\",\n  \"dynamicTitle\": \"Dynamic Title\",\n  \"watermark\": \"Watermark\",\n  \"watermarkContent\": \"Please input Watermark content\",\n  \"checkUpdates\": \"Periodic update check\",\n  \"position\": {\n    \"title\": \"Preferences Postion\",\n    \"header\": \"Header\",\n    \"auto\": \"Auto\",\n    \"fixed\": \"Fixed\"\n  },\n  \"sidebar\": {\n    \"buttons\": \"Show Buttons\",\n    \"buttonFixed\": \"Fixed\",\n    \"buttonCollapsed\": \"Collapsed\",\n    \"title\": \"Sidebar\",\n    \"width\": \"Width\",\n    \"visible\": \"Show Sidebar\",\n    \"collapsed\": \"Collpase Menu\",\n    \"collapsedShowTitle\": \"Show Menu Title\",\n    \"autoActivateChild\": \"Auto Activate SubMenu\",\n    \"autoActivateChildTip\": \"`Enabled` to automatically activate the submenu while click menu.\",\n    \"expandOnHover\": \"Expand On Hover\",\n    \"expandOnHoverTip\": \"When the mouse hovers over menu, \\n `Enabled` to expand children menus \\n `Disabled` to expand whole sidebar.\"\n  },\n  \"tabbar\": {\n    \"title\": \"Tabbar\",\n    \"enable\": \"Enable Tab Bar\",\n    \"icon\": \"Show Tabbar Icon\",\n    \"showMore\": \"Show More Button\",\n    \"showMaximize\": \"Show Maximize Button\",\n    \"persist\": \"Persist Tabs\",\n    \"maxCount\": \"Max Count of Tabs\",\n    \"maxCountTip\": \"When the number of tabs exceeds the maximum,\\nthe oldest tab will be closed.\\n Set to 0 to disable count checking.\",\n    \"draggable\": \"Enable Draggable Sort\",\n    \"wheelable\": \"Support Mouse Wheel\",\n    \"middleClickClose\": \"Close Tab when Mouse Middle Button Click\",\n    \"wheelableTip\": \"When enabled, the Tabbar area responds to vertical scrolling events of the scroll wheel.\",\n    \"styleType\": {\n      \"title\": \"Tabs Style\",\n      \"chrome\": \"Chrome\",\n      \"card\": \"Card\",\n      \"plain\": \"Plain\",\n      \"brisk\": \"Brisk\"\n    },\n    \"contextMenu\": {\n      \"reload\": \"Reload\",\n      \"close\": \"Close\",\n      \"pin\": \"Pin\",\n      \"unpin\": \"Unpin\",\n      \"closeLeft\": \"Close Left Tabs\",\n      \"closeRight\": \"Close Right Tabs\",\n      \"closeOther\": \"Close Other Tabs\",\n      \"closeAll\": \"Close All Tabs\",\n      \"openInNewWindow\": \"Open in New Window\",\n      \"maximize\": \"Maximize\",\n      \"restoreMaximize\": \"Restore\"\n    }\n  },\n  \"navigationMenu\": {\n    \"title\": \"Navigation Menu\",\n    \"style\": \"Navigation Menu Style\",\n    \"accordion\": \"Sidebar Accordion Menu\",\n    \"split\": \"Navigation Menu Separation\",\n    \"splitTip\": \"When enabled, the sidebar displays the top bar's submenu\"\n  },\n  \"breadcrumb\": {\n    \"title\": \"Breadcrumb\",\n    \"home\": \"Show Home Button\",\n    \"enable\": \"Enable Breadcrumb\",\n    \"icon\": \"Show Breadcrumb Icon\",\n    \"background\": \"background\",\n    \"style\": \"Breadcrumb Style\",\n    \"hideOnlyOne\": \"Hidden when only one\"\n  },\n  \"animation\": {\n    \"title\": \"Animation\",\n    \"loading\": \"Page Loading\",\n    \"transition\": \"Page Transition\",\n    \"progress\": \"Page Progress\"\n  },\n  \"theme\": {\n    \"title\": \"Theme\",\n    \"radius\": \"Radius\",\n    \"light\": \"Light\",\n    \"dark\": \"Dark\",\n    \"darkSidebar\": \"Semi Dark Sidebar\",\n    \"darkHeader\": \"Semi Dark Header\",\n    \"weakMode\": \"Weak Mode\",\n    \"grayMode\": \"Gray Mode\",\n    \"builtin\": {\n      \"title\": \"Built-in\",\n      \"default\": \"Default\",\n      \"violet\": \"Violet\",\n      \"pink\": \"Pink\",\n      \"rose\": \"Rose\",\n      \"skyBlue\": \"Sky Blue\",\n      \"deepBlue\": \"Deep Blue\",\n      \"green\": \"Green\",\n      \"deepGreen\": \"Deep Green\",\n      \"orange\": \"Orange\",\n      \"yellow\": \"Yellow\",\n      \"zinc\": \"Zinc\",\n      \"neutral\": \"Neutral\",\n      \"slate\": \"Slate\",\n      \"gray\": \"Gray\",\n      \"custom\": \"Custom\"\n    }\n  },\n  \"header\": {\n    \"title\": \"Header\",\n    \"visible\": \"Show Header\",\n    \"modeStatic\": \"Static\",\n    \"modeFixed\": \"Fixed\",\n    \"modeAuto\": \"Auto hide & Show\",\n    \"modeAutoScroll\": \"Scroll to Hide & Show\",\n    \"menuAlign\": \"Menu Align\",\n    \"menuAlignStart\": \"Start\",\n    \"menuAlignEnd\": \"End\",\n    \"menuAlignCenter\": \"Center\"\n  },\n  \"footer\": {\n    \"title\": \"Footer\",\n    \"visible\": \"Show Footer\",\n    \"fixed\": \"Fixed at Bottom\"\n  },\n  \"copyright\": {\n    \"title\": \"Copyright\",\n    \"enable\": \"Enable Copyright\",\n    \"companyName\": \"Company Name\",\n    \"companySiteLink\": \"Company Site Link\",\n    \"date\": \"Date\",\n    \"icp\": \"ICP License Number\",\n    \"icpLink\": \"ICP Site Link\"\n  },\n  \"shortcutKeys\": {\n    \"title\": \"Shortcut Keys\",\n    \"global\": \"Global\",\n    \"search\": \"Global Search\",\n    \"logout\": \"Logout\",\n    \"preferences\": \"Preferences\"\n  },\n  \"widget\": {\n    \"title\": \"Widget\",\n    \"globalSearch\": \"Enable Global Search\",\n    \"fullscreen\": \"Enable Fullscreen\",\n    \"themeToggle\": \"Enable Theme Toggle\",\n    \"languageToggle\": \"Enable Language Toggle\",\n    \"notification\": \"Enable Notification\",\n    \"sidebarToggle\": \"Enable Sidebar Toggle\",\n    \"lockScreen\": \"Enable Lock Screen\",\n    \"refresh\": \"Enable Refresh\"\n  }\n}\n"
  },
  {
    "path": "packages/locales/src/langs/en-US/ui.json",
    "content": "{\n  \"formRules\": {\n    \"required\": \"Please enter {0}\",\n    \"selectRequired\": \"Please select {0}\",\n    \"minLength\": \"{0} must be at least {1} characters\",\n    \"maxLength\": \"{0} can be at most {1} characters\",\n    \"length\": \"{0} must be {1} characters long\",\n    \"alreadyExists\": \"{0} `{1}` already exists\",\n    \"startWith\": \"{0} must start with `{1}`\",\n    \"invalidURL\": \"Please input a valid URL\"\n  },\n  \"actionTitle\": {\n    \"edit\": \"Modify {0}\",\n    \"create\": \"Create {0}\",\n    \"delete\": \"Delete {0}\",\n    \"view\": \"View {0}\"\n  },\n  \"actionMessage\": {\n    \"deleteConfirm\": \"Are you sure to delete {0}?\",\n    \"deleting\": \"Deleting {0} ...\",\n    \"deleteSuccess\": \"{0} deleted successfully\",\n    \"operationSuccess\": \"Operation succeeded\",\n    \"operationFailed\": \"Operation failed\"\n  },\n  \"placeholder\": {\n    \"input\": \"Please enter\",\n    \"select\": \"Please select\"\n  },\n  \"captcha\": {\n    \"title\": \"Please complete the security verification\",\n    \"sliderSuccessText\": \"Passed\",\n    \"sliderDefaultText\": \"Slider and drag\",\n    \"alt\": \"Supports img tag src attribute value\",\n    \"sliderRotateDefaultTip\": \"Click picture to refresh\",\n    \"sliderTranslateDefaultTip\": \"Click picture to refresh\",\n    \"sliderRotateFailTip\": \"Validation failed\",\n    \"sliderRotateSuccessTip\": \"Validation successful, time {0} seconds\",\n    \"sliderTranslateFailTip\": \"Validation failed\",\n    \"sliderTranslateSuccessTip\": \"Validation successful, time {0} seconds\",\n    \"refreshAriaLabel\": \"Refresh captcha\",\n    \"confirmAriaLabel\": \"Confirm selection\",\n    \"confirm\": \"Confirm\",\n    \"pointAriaLabel\": \"Click point\",\n    \"clickInOrder\": \"Please click in order\"\n  },\n  \"iconPicker\": {\n    \"placeholder\": \"Select an icon\",\n    \"search\": \"Search icon...\"\n  },\n  \"jsonViewer\": {\n    \"copy\": \"Copy\",\n    \"copied\": \"Copied\"\n  },\n  \"fallback\": {\n    \"pageNotFound\": \"Oops! Page Not Found\",\n    \"pageNotFoundDesc\": \"Sorry, we couldn't find the page you were looking for.\",\n    \"forbidden\": \"Oops! Access Denied\",\n    \"forbiddenDesc\": \"Sorry, but you don't have permission to access this page.\",\n    \"internalError\": \"Oops! Something Went Wrong\",\n    \"internalErrorDesc\": \"Sorry, but the server encountered an error.\",\n    \"offline\": \"Offline Page\",\n    \"offlineError\": \"Oops! Network Error\",\n    \"offlineErrorDesc\": \"Sorry, can't connect to the internet. Check your connection.\",\n    \"comingSoon\": \"Coming Soon\",\n    \"http\": {\n      \"requestTimeout\": \"The request timed out. Please try again later.\",\n      \"networkError\": \"A network error occurred. Please check your internet connection and try again.\",\n      \"badRequest\": \"Bad Request. Please check your input and try again.\",\n      \"unauthorized\": \"Unauthorized. Please log in to continue.\",\n      \"forbidden\": \"Forbidden. You do not have permission to access this resource.\",\n      \"notFound\": \"Not Found. The requested resource could not be found.\",\n      \"internalServerError\": \"Internal Server Error. Something went wrong on our end. Please try again later.\"\n    }\n  },\n  \"widgets\": {\n    \"document\": \"Document\",\n    \"profile\": \"Profile\",\n    \"qa\": \"Q&A\",\n    \"setting\": \"Settings\",\n    \"logoutTip\": \"Do you want to logout?\",\n    \"viewAll\": \"View All Messages\",\n    \"notifications\": \"Notifications\",\n    \"markAllAsRead\": \"Make All as Read\",\n    \"clearNotifications\": \"Clear\",\n    \"checkUpdatesTitle\": \"New Version Available\",\n    \"checkUpdatesDescription\": \"Click to refresh and get the latest version\",\n    \"search\": {\n      \"title\": \"Search\",\n      \"searchNavigate\": \"Search Navigation\",\n      \"select\": \"Select\",\n      \"navigate\": \"Navigate\",\n      \"close\": \"Close\",\n      \"noResults\": \"No Search Results Found\",\n      \"noRecent\": \"No Search History\",\n      \"recent\": \"Search History\"\n    },\n    \"lockScreen\": {\n      \"title\": \"Lock Screen\",\n      \"screenButton\": \"Locking\",\n      \"password\": \"Password\",\n      \"placeholder\": \"Please enter password\",\n      \"unlock\": \"Click to unlock\",\n      \"errorPasswordTip\": \"Password error, please re-enter\",\n      \"backToLogin\": \"Back to login\",\n      \"entry\": \"Enter the system\"\n    }\n  }\n}\n"
  },
  {
    "path": "packages/locales/src/langs/zh-CN/authentication.json",
    "content": "{\n  \"welcomeBack\": \"欢迎回来\",\n  \"pageTitle\": \"开箱即用的大型中后台管理系统\",\n  \"pageDesc\": \"工程化、高性能、跨组件库的前端模版\",\n  \"loginSuccess\": \"登录成功\",\n  \"loginSuccessDesc\": \"欢迎回来\",\n  \"loginSubtitle\": \"请输入您的帐户信息以开始管理您的项目\",\n  \"selectAccount\": \"快速选择账号\",\n  \"username\": \"账号\",\n  \"password\": \"密码\",\n  \"usernameTip\": \"请输入用户名\",\n  \"passwordTip\": \"请输入密码\",\n  \"verifyRequiredTip\": \"请先完成验证\",\n  \"passwordErrorTip\": \"密码错误\",\n  \"rememberMe\": \"记住账号\",\n  \"createAnAccount\": \"创建一个账号\",\n  \"createAccount\": \"创建账号\",\n  \"alreadyHaveAccount\": \"已经有账号了?\",\n  \"accountTip\": \"还没有账号?\",\n  \"signUp\": \"注册\",\n  \"signUpSubtitle\": \"让您的应用程序管理变得简单而有趣\",\n  \"confirmPassword\": \"确认密码\",\n  \"confirmPasswordTip\": \"两次输入的密码不一致\",\n  \"agree\": \"我同意\",\n  \"privacyPolicy\": \"隐私政策\",\n  \"terms\": \"条款\",\n  \"agreeTip\": \"请同意隐私政策和条款\",\n  \"goToLogin\": \"去登录\",\n  \"passwordStrength\": \"使用 8 个或更多字符，混合字母、数字和符号\",\n  \"forgetPassword\": \"忘记密码?\",\n  \"forgetPasswordSubtitle\": \"输入您的电子邮件，我们将向您发送重置密码的连接\",\n  \"emailTip\": \"请输入邮箱\",\n  \"emailValidErrorTip\": \"你输入的邮箱格式不正确\",\n  \"sendResetLink\": \"发送重置链接\",\n  \"email\": \"邮箱\",\n  \"qrcodeSubtitle\": \"请用手机扫描二维码登录\",\n  \"qrcodePrompt\": \"扫码后点击 '确认'，即可完成登录\",\n  \"qrcodeLogin\": \"扫码登录\",\n  \"wechatLogin\": \"微信登录\",\n  \"qqLogin\": \"QQ登录\",\n  \"githubLogin\": \"Github登录\",\n  \"googleLogin\": \"Google登录\",\n  \"dingdingLogin\": \"钉钉登录\",\n  \"codeSubtitle\": \"请输入您的手机号码以开始管理您的项目\",\n  \"code\": \"验证码\",\n  \"codeTip\": \"请输入{0}位验证码\",\n  \"mobile\": \"手机号码\",\n  \"mobileTip\": \"请输入手机号\",\n  \"mobileErrortip\": \"手机号码格式错误\",\n  \"mobileLogin\": \"手机号登录\",\n  \"sendCode\": \"获取验证码\",\n  \"sendText\": \"{0}秒后重新获取\",\n  \"thirdPartyLogin\": \"其他登录方式\",\n  \"weChat\": \"微信\",\n  \"qq\": \"QQ\",\n  \"gitHub\": \"GitHub\",\n  \"google\": \"Google\",\n  \"loginAgainTitle\": \"重新登录\",\n  \"loginAgainSubTitle\": \"您的登录状态已过期，请重新登录以继续。\",\n  \"layout\": {\n    \"center\": \"居中\",\n    \"alignLeft\": \"居左\",\n    \"alignRight\": \"居右\"\n  }\n}\n"
  },
  {
    "path": "packages/locales/src/langs/zh-CN/common.json",
    "content": "{\n  \"back\": \"返回\",\n  \"backToHome\": \"返回首页\",\n  \"login\": \"登录\",\n  \"logout\": \"退出登录\",\n  \"prompt\": \"提示\",\n  \"cancel\": \"取消\",\n  \"confirm\": \"确认\",\n  \"reset\": \"重置\",\n  \"noData\": \"暂无数据\",\n  \"refresh\": \"刷新\",\n  \"loadingMenu\": \"加载菜单中\",\n  \"query\": \"查询\",\n  \"search\": \"搜索\",\n  \"enabled\": \"已启用\",\n  \"disabled\": \"已禁用\",\n  \"edit\": \"修改\",\n  \"delete\": \"删除\",\n  \"create\": \"新增\",\n  \"yes\": \"是\",\n  \"no\": \"否\",\n  \"showSearchPanel\": \"显示搜索面板\",\n  \"hideSearchPanel\": \"隐藏搜索面板\"\n}\n"
  },
  {
    "path": "packages/locales/src/langs/zh-CN/preferences.json",
    "content": "{\n  \"title\": \"偏好设置\",\n  \"subtitle\": \"自定义偏好设置 & 实时预览\",\n  \"enableStickyPreferencesNavigationBar\": \"开启首选项导航栏吸顶效果\",\n  \"disableStickyPreferencesNavigationBar\": \"关闭首选项导航栏吸顶效果\",\n  \"resetTitle\": \"重置偏好设置\",\n  \"resetTip\": \"数据有变化，点击可进行重置\",\n  \"resetSuccess\": \"重置偏好设置成功\",\n  \"appearance\": \"外观\",\n  \"layout\": \"布局\",\n  \"content\": \"内容\",\n  \"other\": \"其它\",\n  \"wide\": \"流式\",\n  \"compact\": \"定宽\",\n  \"followSystem\": \"跟随系统\",\n  \"vertical\": \"垂直\",\n  \"verticalTip\": \"侧边垂直菜单模式\",\n  \"horizontal\": \"水平\",\n  \"horizontalTip\": \"水平菜单模式，菜单全部显示在顶部\",\n  \"twoColumn\": \"双列菜单\",\n  \"twoColumnTip\": \"垂直双列菜单模式\",\n  \"headerSidebarNav\": \"侧边导航\",\n  \"headerSidebarNavTip\": \"顶部通栏，侧边导航模式\",\n  \"headerTwoColumn\": \"混合双列\",\n  \"headerTwoColumnTip\": \"双列、水平菜单共存模式\",\n  \"mixedMenu\": \"混合垂直\",\n  \"mixedMenuTip\": \"垂直水平菜单共存\",\n  \"fullContent\": \"内容全屏\",\n  \"fullContentTip\": \"不显示任何菜单，只显示内容主体\",\n  \"normal\": \"常规\",\n  \"plain\": \"朴素\",\n  \"rounded\": \"圆润\",\n  \"copyPreferences\": \"复制偏好设置\",\n  \"copyPreferencesSuccessTitle\": \"复制成功\",\n  \"copyPreferencesSuccess\": \"复制成功，请在 app 下的 `src/preferences.ts`内进行覆盖\",\n  \"clearAndLogout\": \"清空缓存 & 退出登录\",\n  \"mode\": \"模式\",\n  \"general\": \"通用\",\n  \"language\": \"语言\",\n  \"dynamicTitle\": \"动态标题\",\n  \"watermark\": \"水印\",\n  \"watermarkContent\": \"请输入水印文案\",\n  \"checkUpdates\": \"定时检查更新\",\n  \"position\": {\n    \"title\": \"偏好设置位置\",\n    \"header\": \"顶栏\",\n    \"auto\": \"自动\",\n    \"fixed\": \"固定\"\n  },\n  \"sidebar\": {\n    \"buttons\": \"显示按钮\",\n    \"buttonFixed\": \"固定按钮\",\n    \"buttonCollapsed\": \"折叠按钮\",\n    \"title\": \"侧边栏\",\n    \"width\": \"宽度\",\n    \"visible\": \"显示侧边栏\",\n    \"collapsed\": \"折叠菜单\",\n    \"collapsedShowTitle\": \"折叠显示菜单名\",\n    \"autoActivateChild\": \"自动激活子菜单\",\n    \"autoActivateChildTip\": \"点击顶层菜单时,自动激活第一个子菜单或者上一次激活的子菜单\",\n    \"expandOnHover\": \"鼠标悬停展开\",\n    \"expandOnHoverTip\": \"鼠标在折叠区域悬浮时，`启用`则展开当前子菜单，`禁用`则展开整个侧边栏\"\n  },\n  \"tabbar\": {\n    \"title\": \"标签栏\",\n    \"enable\": \"启用标签栏\",\n    \"icon\": \"显示标签栏图标\",\n    \"showMore\": \"显示更多按钮\",\n    \"showMaximize\": \"显示最大化按钮\",\n    \"persist\": \"持久化标签页\",\n    \"maxCount\": \"最大标签数\",\n    \"maxCountTip\": \"每次打开新的标签时如果超过最大标签数，\\n会自动关闭一个最先打开的标签\\n设置为 0 则不限制\",\n    \"draggable\": \"启动拖拽排序\",\n    \"wheelable\": \"启用纵向滚轮响应\",\n    \"middleClickClose\": \"点击鼠标中键关闭标签页\",\n    \"wheelableTip\": \"开启后，标签栏区域可以响应滚轮的纵向滚动事件。\\n关闭时，只能响应系统的横向滚动事件（需要按下Shift再滚动滚轮）\",\n    \"styleType\": {\n      \"title\": \"标签页风格\",\n      \"chrome\": \"谷歌\",\n      \"card\": \"卡片\",\n      \"plain\": \"朴素\",\n      \"brisk\": \"轻快\"\n    },\n    \"contextMenu\": {\n      \"reload\": \"重新加载\",\n      \"close\": \"关闭\",\n      \"pin\": \"固定\",\n      \"unpin\": \"取消固定\",\n      \"closeLeft\": \"关闭左侧标签页\",\n      \"closeRight\": \"关闭右侧标签页\",\n      \"closeOther\": \"关闭其它标签页\",\n      \"closeAll\": \"关闭全部标签页\",\n      \"openInNewWindow\": \"在新窗口打开\",\n      \"maximize\": \"最大化\",\n      \"restoreMaximize\": \"还原\"\n    }\n  },\n  \"navigationMenu\": {\n    \"title\": \"导航菜单\",\n    \"style\": \"导航菜单风格\",\n    \"accordion\": \"侧边导航菜单手风琴模式\",\n    \"split\": \"导航菜单分离\",\n    \"splitTip\": \"开启时，侧边栏显示顶栏对应菜单的子菜单\"\n  },\n  \"breadcrumb\": {\n    \"title\": \"面包屑导航\",\n    \"enable\": \"开启面包屑导航\",\n    \"icon\": \"显示面包屑图标\",\n    \"home\": \"显示首页按钮\",\n    \"style\": \"面包屑风格\",\n    \"hideOnlyOne\": \"仅有一个时隐藏\",\n    \"background\": \"背景\"\n  },\n  \"animation\": {\n    \"title\": \"动画\",\n    \"loading\": \"页面切换 Loading\",\n    \"transition\": \"页面切换动画\",\n    \"progress\": \"页面切换进度条\"\n  },\n  \"theme\": {\n    \"title\": \"主题\",\n    \"radius\": \"圆角\",\n    \"light\": \"浅色\",\n    \"dark\": \"深色\",\n    \"darkSidebar\": \"深色侧边栏\",\n    \"darkHeader\": \"深色顶栏\",\n    \"weakMode\": \"色弱模式\",\n    \"grayMode\": \"灰色模式\",\n    \"builtin\": {\n      \"title\": \"内置主题\",\n      \"default\": \"默认\",\n      \"violet\": \"紫罗兰\",\n      \"pink\": \"樱花粉\",\n      \"rose\": \"玫瑰红\",\n      \"skyBlue\": \"天蓝色\",\n      \"deepBlue\": \"深蓝色\",\n      \"green\": \"浅绿色\",\n      \"deepGreen\": \"深绿色\",\n      \"orange\": \"橙黄色\",\n      \"yellow\": \"柠檬黄\",\n      \"zinc\": \"锌色灰\",\n      \"neutral\": \"中性色\",\n      \"slate\": \"石板灰\",\n      \"gray\": \"中灰色\",\n      \"custom\": \"自定义\"\n    }\n  },\n  \"header\": {\n    \"title\": \"顶栏\",\n    \"modeStatic\": \"静止\",\n    \"modeFixed\": \"固定\",\n    \"modeAuto\": \"自动隐藏和显示\",\n    \"modeAutoScroll\": \"滚动隐藏和显示\",\n    \"visible\": \"显示顶栏\",\n    \"menuAlign\": \"菜单位置\",\n    \"menuAlignStart\": \"左侧\",\n    \"menuAlignEnd\": \"右侧\",\n    \"menuAlignCenter\": \"居中\"\n  },\n  \"footer\": {\n    \"title\": \"底栏\",\n    \"visible\": \"显示底栏\",\n    \"fixed\": \"固定在底部\"\n  },\n  \"copyright\": {\n    \"title\": \"版权\",\n    \"enable\": \"启用版权\",\n    \"companyName\": \"公司名\",\n    \"companySiteLink\": \"公司主页\",\n    \"date\": \"日期\",\n    \"icp\": \"ICP 备案号\",\n    \"icpLink\": \"ICP 网站链接\"\n  },\n  \"shortcutKeys\": {\n    \"title\": \"快捷键\",\n    \"global\": \"全局\",\n    \"search\": \"全局搜索\",\n    \"logout\": \"退出登录\",\n    \"preferences\": \"偏好设置\"\n  },\n  \"widget\": {\n    \"title\": \"小部件\",\n    \"globalSearch\": \"启用全局搜索\",\n    \"fullscreen\": \"启用全屏\",\n    \"themeToggle\": \"启用主题切换\",\n    \"languageToggle\": \"启用语言切换\",\n    \"notification\": \"启用通知\",\n    \"sidebarToggle\": \"启用侧边栏切换\",\n    \"lockScreen\": \"启用锁屏\",\n    \"refresh\": \"启用刷新\"\n  }\n}\n"
  },
  {
    "path": "packages/locales/src/langs/zh-CN/ui.json",
    "content": "{\n  \"formRules\": {\n    \"required\": \"请输入{0}\",\n    \"selectRequired\": \"请选择{0}\",\n    \"minLength\": \"{0}至少{1}个字符\",\n    \"maxLength\": \"{0}最多{1}个字符\",\n    \"length\": \"{0}长度必须为{1}个字符\",\n    \"alreadyExists\": \"{0} `{1}` 已存在\",\n    \"startWith\": \"{0}必须以 {1} 开头\",\n    \"invalidURL\": \"请输入有效的链接\"\n  },\n  \"actionTitle\": {\n    \"edit\": \"修改{0}\",\n    \"create\": \"新增{0}\",\n    \"delete\": \"删除{0}\",\n    \"view\": \"查看{0}\"\n  },\n  \"actionMessage\": {\n    \"deleteConfirm\": \"确定删除 {0} 吗？\",\n    \"deleting\": \"正在删除 {0} ...\",\n    \"deleteSuccess\": \"{0} 删除成功\",\n    \"operationSuccess\": \"操作成功\",\n    \"operationFailed\": \"操作失败\"\n  },\n  \"placeholder\": {\n    \"input\": \"请输入\",\n    \"select\": \"请选择\"\n  },\n  \"captcha\": {\n    \"title\": \"请完成安全验证\",\n    \"sliderSuccessText\": \"验证通过\",\n    \"sliderDefaultText\": \"请按住滑块拖动\",\n    \"sliderRotateDefaultTip\": \"点击图片可刷新\",\n    \"sliderTranslateDefaultTip\": \"点击图片可刷新\",\n    \"sliderRotateFailTip\": \"验证失败\",\n    \"sliderRotateSuccessTip\": \"验证成功，耗时{0}秒\",\n    \"sliderTranslateFailTip\": \"验证失败\",\n    \"sliderTranslateSuccessTip\": \"验证成功，耗时{0}秒\",\n    \"alt\": \"支持img标签src属性值\",\n    \"refreshAriaLabel\": \"刷新验证码\",\n    \"confirmAriaLabel\": \"确认选择\",\n    \"confirm\": \"确认\",\n    \"pointAriaLabel\": \"点击点\",\n    \"clickInOrder\": \"请依次点击\"\n  },\n  \"iconPicker\": {\n    \"placeholder\": \"选择一个图标\",\n    \"search\": \"搜索图标...\"\n  },\n  \"jsonViewer\": {\n    \"copy\": \"复制\",\n    \"copied\": \"已复制\"\n  },\n  \"fallback\": {\n    \"pageNotFound\": \"哎呀！未找到页面\",\n    \"pageNotFoundDesc\": \"抱歉，我们无法找到您要找的页面。\",\n    \"forbidden\": \"哎呀！访问被拒绝\",\n    \"forbiddenDesc\": \"抱歉，您没有权限访问此页面。\",\n    \"internalError\": \"哎呀！出错了\",\n    \"internalErrorDesc\": \"抱歉，服务器遇到错误。\",\n    \"offline\": \"离线页面\",\n    \"offlineError\": \"哎呀！网络错误\",\n    \"offlineErrorDesc\": \"抱歉，无法连接到互联网，请检查您的网络连接并重试。\",\n    \"comingSoon\": \"即将推出\",\n    \"http\": {\n      \"requestTimeout\": \"请求超时，请稍后再试。\",\n      \"networkError\": \"网络异常，请检查您的网络连接后重试。\",\n      \"badRequest\": \"请求错误。请检查您的输入并重试。\",\n      \"unauthorized\": \"登录认证过期，请重新登录后继续。\",\n      \"forbidden\": \"禁止访问, 您没有权限访问此资源。\",\n      \"notFound\": \"未找到, 请求的资源不存在。\",\n      \"internalServerError\": \"内部服务器错误，请稍后再试。\"\n    }\n  },\n  \"widgets\": {\n    \"document\": \"文档\",\n    \"profile\": \"个人中心\",\n    \"qa\": \"问题 & 帮助\",\n    \"setting\": \"设置\",\n    \"logoutTip\": \"是否退出登录？\",\n    \"viewAll\": \"查看所有消息\",\n    \"notifications\": \"通知\",\n    \"markAllAsRead\": \"全部标记为已读\",\n    \"clearNotifications\": \"清空\",\n    \"checkUpdatesTitle\": \"新版本可用\",\n    \"checkUpdatesDescription\": \"点击刷新以获取最新版本\",\n    \"search\": {\n      \"title\": \"搜索\",\n      \"searchNavigate\": \"搜索导航菜单\",\n      \"select\": \"选择\",\n      \"navigate\": \"导航\",\n      \"close\": \"关闭\",\n      \"noResults\": \"未找到搜索结果\",\n      \"noRecent\": \"没有搜索历史\",\n      \"recent\": \"搜索历史\"\n    },\n    \"lockScreen\": {\n      \"title\": \"锁定屏幕\",\n      \"screenButton\": \"锁定\",\n      \"password\": \"密码\",\n      \"placeholder\": \"请输入锁屏密码\",\n      \"unlock\": \"点击解锁\",\n      \"errorPasswordTip\": \"密码错误，请重新输入\",\n      \"backToLogin\": \"返回登录\",\n      \"entry\": \"进入系统\"\n    }\n  }\n}\n"
  },
  {
    "path": "packages/locales/src/typing.ts",
    "content": "export type SupportedLanguagesType = 'en-US' | 'zh-CN';\n\nexport type ImportLocaleFn = () => Promise<{ default: Record<string, string> }>;\n\nexport type LoadMessageFn = (\n  lang: SupportedLanguagesType,\n) => Promise<Record<string, string> | undefined>;\n\nexport interface LocaleSetupOptions {\n  /**\n   * Default language\n   * @default zh-CN\n   */\n  defaultLocale?: SupportedLanguagesType;\n  /**\n   * Load message function\n   * @param lang\n   * @returns\n   */\n  loadMessages?: LoadMessageFn;\n  /**\n   * Whether to warn when the key is not found\n   */\n  missingWarn?: boolean;\n}\n"
  },
  {
    "path": "packages/locales/tsconfig.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/tsconfig\",\n  \"extends\": \"@vben/tsconfig/web.json\",\n  \"include\": [\"src\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "packages/preferences/package.json",
    "content": "{\n  \"name\": \"@vben/preferences\",\n  \"version\": \"5.5.9\",\n  \"homepage\": \"https://github.com/vbenjs/vue-vben-admin\",\n  \"bugs\": \"https://github.com/vbenjs/vue-vben-admin/issues\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/vbenjs/vue-vben-admin.git\",\n    \"directory\": \"packages/preferences\"\n  },\n  \"license\": \"MIT\",\n  \"type\": \"module\",\n  \"sideEffects\": [\n    \"**/*.css\"\n  ],\n  \"exports\": {\n    \".\": {\n      \"types\": \"./src/index.ts\",\n      \"default\": \"./src/index.ts\"\n    }\n  },\n  \"dependencies\": {\n    \"@vben-core/preferences\": \"workspace:*\",\n    \"@vben-core/typings\": \"workspace:*\"\n  }\n}\n"
  },
  {
    "path": "packages/preferences/src/index.ts",
    "content": "import type { Preferences } from '@vben-core/preferences';\nimport type { DeepPartial } from '@vben-core/typings';\n\n/**\n * 如果你想所有的app都使用相同的默认偏好设置，你可以在这里定义\n * 而不是去修改 @vben-core/preferences 中的默认偏好设置\n * @param preferences\n * @returns\n */\n\nfunction defineOverridesPreferences(preferences: DeepPartial<Preferences>) {\n  return preferences;\n}\n\nexport { defineOverridesPreferences };\n\nexport * from '@vben-core/preferences';\n"
  },
  {
    "path": "packages/preferences/tsconfig.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/tsconfig\",\n  \"extends\": \"@vben/tsconfig/web.json\",\n  \"include\": [\"src\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "packages/stores/package.json",
    "content": "{\n  \"name\": \"@vben/stores\",\n  \"version\": \"5.5.9\",\n  \"homepage\": \"https://github.com/vbenjs/vue-vben-admin\",\n  \"bugs\": \"https://github.com/vbenjs/vue-vben-admin/issues\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/vbenjs/vue-vben-admin.git\",\n    \"directory\": \"packages/stores\"\n  },\n  \"license\": \"MIT\",\n  \"type\": \"module\",\n  \"sideEffects\": [\n    \"**/*.css\"\n  ],\n  \"exports\": {\n    \".\": {\n      \"types\": \"./src/index.ts\",\n      \"default\": \"./src/index.ts\"\n    }\n  },\n  \"dependencies\": {\n    \"@vben-core/preferences\": \"workspace:*\",\n    \"@vben-core/shared\": \"workspace:*\",\n    \"@vben-core/typings\": \"workspace:*\",\n    \"pinia\": \"catalog:\",\n    \"pinia-plugin-persistedstate\": \"catalog:\",\n    \"secure-ls\": \"catalog:\",\n    \"vue\": \"catalog:\",\n    \"vue-router\": \"catalog:\"\n  }\n}\n"
  },
  {
    "path": "packages/stores/shim-pinia.d.ts",
    "content": "// https://github.com/vuejs/pinia/issues/2098\ndeclare module 'pinia' {\n  export function acceptHMRUpdate(\n    initialUseStore: any | StoreDefinition,\n    hot: any,\n  ): (newModule: any) => any;\n}\n\nexport {};\n"
  },
  {
    "path": "packages/stores/src/index.ts",
    "content": "export * from './modules';\nexport * from './setup';\nexport { defineStore, storeToRefs } from 'pinia';\n"
  },
  {
    "path": "packages/stores/src/modules/access.test.ts",
    "content": "import { createPinia, setActivePinia } from 'pinia';\nimport { beforeEach, describe, expect, it } from 'vitest';\n\nimport { useAccessStore } from './access';\n\ndescribe('useAccessStore', () => {\n  beforeEach(() => {\n    setActivePinia(createPinia());\n  });\n\n  it('updates accessMenus state', () => {\n    const store = useAccessStore();\n    expect(store.accessMenus).toEqual([]);\n    store.setAccessMenus([{ name: 'Dashboard', path: '/dashboard' }]);\n    expect(store.accessMenus).toEqual([\n      { name: 'Dashboard', path: '/dashboard' },\n    ]);\n  });\n\n  it('updates accessToken state correctly', () => {\n    const store = useAccessStore();\n    expect(store.accessToken).toBeNull(); // 初始状态\n    store.setAccessToken('abc123');\n    expect(store.accessToken).toBe('abc123');\n  });\n\n  it('returns the correct accessToken', () => {\n    const store = useAccessStore();\n    store.setAccessToken('xyz789');\n    expect(store.accessToken).toBe('xyz789');\n  });\n\n  // 测试设置空的访问菜单列表\n  it('handles empty accessMenus correctly', () => {\n    const store = useAccessStore();\n    store.setAccessMenus([]);\n    expect(store.accessMenus).toEqual([]);\n  });\n\n  // 测试设置空的访问路由列表\n  it('handles empty accessRoutes correctly', () => {\n    const store = useAccessStore();\n    store.setAccessRoutes([]);\n    expect(store.accessRoutes).toEqual([]);\n  });\n});\n"
  },
  {
    "path": "packages/stores/src/modules/access.ts",
    "content": "import type { RouteRecordRaw } from 'vue-router';\n\nimport type { MenuRecordRaw } from '@vben-core/typings';\n\nimport { acceptHMRUpdate, defineStore } from 'pinia';\n\ntype AccessToken = null | string;\n\ninterface AccessState {\n  /**\n   * 权限码\n   */\n  accessCodes: string[];\n  /**\n   * 可访问的菜单列表\n   */\n  accessMenus: MenuRecordRaw[];\n  /**\n   * 可访问的路由列表\n   */\n  accessRoutes: RouteRecordRaw[];\n  /**\n   * 登录 accessToken\n   */\n  accessToken: AccessToken;\n  /**\n   * 是否已经检查过权限\n   */\n  isAccessChecked: boolean;\n  /**\n   * 是否锁屏状态\n   */\n  isLockScreen: boolean;\n  /**\n   * 锁屏密码\n   */\n  lockScreenPassword?: string;\n  /**\n   * 登录是否过期\n   */\n  loginExpired: boolean;\n  /**\n   * 登录 accessToken\n   */\n  refreshToken: AccessToken;\n}\n\n/**\n * @zh_CN 访问权限相关\n */\nexport const useAccessStore = defineStore('core-access', {\n  actions: {\n    getMenuByPath(path: string) {\n      function findMenu(\n        menus: MenuRecordRaw[],\n        path: string,\n      ): MenuRecordRaw | undefined {\n        for (const menu of menus) {\n          if (menu.path === path) {\n            return menu;\n          }\n          if (menu.children) {\n            const matched = findMenu(menu.children, path);\n            if (matched) {\n              return matched;\n            }\n          }\n        }\n      }\n      return findMenu(this.accessMenus, path);\n    },\n    lockScreen(password: string) {\n      this.isLockScreen = true;\n      this.lockScreenPassword = password;\n    },\n    setAccessCodes(codes: string[]) {\n      this.accessCodes = codes;\n    },\n    setAccessMenus(menus: MenuRecordRaw[]) {\n      this.accessMenus = menus;\n    },\n    setAccessRoutes(routes: RouteRecordRaw[]) {\n      this.accessRoutes = routes;\n    },\n    setAccessToken(token: AccessToken) {\n      this.accessToken = token;\n    },\n    setIsAccessChecked(isAccessChecked: boolean) {\n      this.isAccessChecked = isAccessChecked;\n    },\n    setLoginExpired(loginExpired: boolean) {\n      this.loginExpired = loginExpired;\n    },\n    setRefreshToken(token: AccessToken) {\n      this.refreshToken = token;\n    },\n    unlockScreen() {\n      this.isLockScreen = false;\n      this.lockScreenPassword = undefined;\n    },\n  },\n  persist: {\n    // 持久化\n    pick: [\n      'accessToken',\n      'refreshToken',\n      'accessCodes',\n      'isLockScreen',\n      'lockScreenPassword',\n    ],\n  },\n  state: (): AccessState => ({\n    accessCodes: [],\n    accessMenus: [],\n    accessRoutes: [],\n    accessToken: null,\n    isAccessChecked: false,\n    isLockScreen: false,\n    lockScreenPassword: undefined,\n    loginExpired: false,\n    refreshToken: null,\n  }),\n});\n\n// 解决热更新问题\nconst hot = import.meta.hot;\nif (hot) {\n  hot.accept(acceptHMRUpdate(useAccessStore, hot));\n}\n"
  },
  {
    "path": "packages/stores/src/modules/index.ts",
    "content": "export * from './access';\nexport * from './tabbar';\nexport * from './user';\n"
  },
  {
    "path": "packages/stores/src/modules/tabbar.test.ts",
    "content": "import { createRouter, createWebHistory } from 'vue-router';\n\nimport { createPinia, setActivePinia } from 'pinia';\nimport { beforeEach, describe, expect, it, vi } from 'vitest';\n\nimport { useTabbarStore } from './tabbar';\n\ndescribe('useAccessStore', () => {\n  const router = createRouter({\n    history: createWebHistory(),\n    routes: [],\n  });\n  router.push = vi.fn();\n  router.replace = vi.fn();\n  beforeEach(() => {\n    setActivePinia(createPinia());\n    vi.clearAllMocks();\n  });\n\n  it('adds a new tab', () => {\n    const store = useTabbarStore();\n    const tab: any = {\n      fullPath: '/home',\n      meta: {},\n      key: '/home',\n      name: 'Home',\n      path: '/home',\n    };\n    const addNewTab = store.addTab(tab);\n    expect(store.tabs.length).toBe(1);\n    expect(store.tabs[0]).toEqual(addNewTab);\n  });\n\n  it('adds a new tab if it does not exist', () => {\n    const store = useTabbarStore();\n    const newTab: any = {\n      fullPath: '/new',\n      meta: {},\n      name: 'New',\n      path: '/new',\n    };\n    const addNewTab = store.addTab(newTab);\n    expect(store.tabs).toContainEqual(addNewTab);\n  });\n\n  it('updates an existing tab instead of adding a new one', () => {\n    const store = useTabbarStore();\n    const initialTab: any = {\n      fullPath: '/existing',\n      meta: {\n        fullPathKey: false,\n      },\n      name: 'Existing',\n      path: '/existing',\n      query: {},\n    };\n    store.addTab(initialTab);\n    const updatedTab = { ...initialTab, query: { id: '1' } };\n    store.addTab(updatedTab);\n    expect(store.tabs.length).toBe(1);\n    expect(store.tabs[0]?.query).toEqual({ id: '1' });\n  });\n\n  it('closes all tabs', async () => {\n    const store = useTabbarStore();\n    store.addTab({\n      fullPath: '/home',\n      meta: {},\n      name: 'Home',\n      path: '/home',\n    } as any);\n    router.replace = vi.fn();\n\n    await store.closeAllTabs(router);\n\n    expect(store.tabs.length).toBe(1);\n  });\n\n  it('closes a non-affix tab', () => {\n    const store = useTabbarStore();\n    const tab: any = {\n      fullPath: '/closable',\n      meta: {},\n      name: 'Closable',\n      path: '/closable',\n    };\n    store.tabs.push(tab);\n    store._close(tab);\n    expect(store.tabs.length).toBe(0);\n  });\n\n  it('does not close an affix tab', () => {\n    const store = useTabbarStore();\n    const affixTab: any = {\n      fullPath: '/affix',\n      meta: { affixTab: true },\n      name: 'Affix',\n      path: '/affix',\n    };\n    store.tabs.push(affixTab);\n    store._close(affixTab);\n    expect(store.tabs.length).toBe(1); // Affix tab should not be closed\n  });\n\n  it('returns all cache tabs', () => {\n    const store = useTabbarStore();\n    store.cachedTabs.add('Home');\n    store.cachedTabs.add('About');\n    expect(store.getCachedTabs).toEqual(['Home', 'About']);\n  });\n\n  it('returns all tabs, including affix tabs', () => {\n    const store = useTabbarStore();\n    const normalTab: any = {\n      fullPath: '/normal',\n      meta: {},\n      name: 'Normal',\n      path: '/normal',\n    };\n    const affixTab: any = {\n      fullPath: '/affix',\n      meta: { affixTab: true },\n      name: 'Affix',\n      path: '/affix',\n    };\n    store.tabs.push(normalTab);\n    store.affixTabs.push(affixTab);\n    expect(store.getTabs).toContainEqual(normalTab);\n    expect(store.affixTabs).toContainEqual(affixTab);\n  });\n\n  it('navigates to a specific tab', async () => {\n    const store = useTabbarStore();\n    const tab: any = { meta: {}, name: 'Dashboard', path: '/dashboard' };\n\n    await store._goToTab(tab, router);\n\n    expect(router.replace).toHaveBeenCalledWith({\n      params: {},\n      path: '/dashboard',\n      query: {},\n    });\n  });\n\n  it('closes multiple tabs by paths', async () => {\n    const store = useTabbarStore();\n    store.addTab({\n      fullPath: '/home',\n      meta: {},\n      name: 'Home',\n      path: '/home',\n    } as any);\n    store.addTab({\n      fullPath: '/about',\n      meta: {},\n      name: 'About',\n      path: '/about',\n    } as any);\n    store.addTab({\n      fullPath: '/contact',\n      meta: {},\n      name: 'Contact',\n      path: '/contact',\n    } as any);\n\n    await store._bulkCloseByKeys(['/home', '/contact']);\n\n    expect(store.tabs).toHaveLength(1);\n    expect(store.tabs[0]?.name).toBe('About');\n  });\n\n  it('closes all tabs to the left of the specified tab', async () => {\n    const store = useTabbarStore();\n    store.addTab({\n      fullPath: '/home',\n      meta: {},\n      name: 'Home',\n      path: '/home',\n    } as any);\n    store.addTab({\n      fullPath: '/about',\n      meta: {},\n      name: 'About',\n      path: '/about',\n    } as any);\n    const targetTab: any = {\n      fullPath: '/contact',\n      meta: {},\n      name: 'Contact',\n      path: '/contact',\n    };\n    const addTargetTab = store.addTab(targetTab);\n    await store.closeLeftTabs(addTargetTab);\n\n    expect(store.tabs).toHaveLength(1);\n    expect(store.tabs[0]?.name).toBe('Contact');\n  });\n\n  it('closes all tabs except the specified tab', async () => {\n    const store = useTabbarStore();\n    store.addTab({\n      fullPath: '/home',\n      meta: {},\n      name: 'Home',\n      path: '/home',\n    } as any);\n    const targetTab: any = {\n      fullPath: '/about',\n      meta: {},\n      name: 'About',\n      path: '/about',\n    };\n    const addTargetTab = store.addTab(targetTab);\n    store.addTab({\n      fullPath: '/contact',\n      meta: {},\n      name: 'Contact',\n      path: '/contact',\n    } as any);\n\n    await store.closeOtherTabs(addTargetTab);\n\n    expect(store.tabs).toHaveLength(1);\n    expect(store.tabs[0]?.name).toBe('About');\n  });\n\n  it('closes all tabs to the right of the specified tab', async () => {\n    const store = useTabbarStore();\n    const targetTab: any = {\n      fullPath: '/home',\n      meta: {},\n      name: 'Home',\n      path: '/home',\n    };\n    const addTargetTab = store.addTab(targetTab);\n    store.addTab({\n      fullPath: '/about',\n      meta: {},\n      name: 'About',\n      path: '/about',\n    } as any);\n    store.addTab({\n      fullPath: '/contact',\n      meta: {},\n      name: 'Contact',\n      path: '/contact',\n    } as any);\n\n    await store.closeRightTabs(addTargetTab);\n\n    expect(store.tabs).toHaveLength(1);\n    expect(store.tabs[0]?.name).toBe('Home');\n  });\n\n  it('closes the tab with the specified key', async () => {\n    const store = useTabbarStore();\n    const keyToClose = '/about';\n    store.addTab({\n      fullPath: '/home',\n      meta: {},\n      name: 'Home',\n      path: '/home',\n    } as any);\n    store.addTab({\n      fullPath: keyToClose,\n      meta: {},\n      name: 'About',\n      path: '/about',\n    } as any);\n    store.addTab({\n      fullPath: '/contact',\n      meta: {},\n      name: 'Contact',\n      path: '/contact',\n    } as any);\n\n    await store.closeTabByKey(keyToClose, router);\n\n    expect(store.tabs).toHaveLength(2);\n    expect(\n      store.tabs.find((tab) => tab.fullPath === keyToClose),\n    ).toBeUndefined();\n  });\n\n  it('refreshes the current tab', async () => {\n    const store = useTabbarStore();\n    const currentTab: any = {\n      fullPath: '/dashboard',\n      meta: { name: 'Dashboard' },\n      name: 'Dashboard',\n      path: '/dashboard',\n    };\n    router.currentRoute.value = currentTab;\n\n    await store.refresh(router);\n\n    expect(store.excludeCachedTabs.has('Dashboard')).toBe(false);\n    expect(store.renderRouteView).toBe(true);\n  });\n});\n"
  },
  {
    "path": "packages/stores/src/modules/tabbar.ts",
    "content": "import type { ComputedRef } from 'vue';\nimport type {\n  RouteLocationNormalized,\n  Router,\n  RouteRecordNormalized,\n} from 'vue-router';\n\nimport type { TabDefinition } from '@vben-core/typings';\n\nimport { toRaw } from 'vue';\n\nimport { preferences } from '@vben-core/preferences';\nimport {\n  openRouteInNewWindow,\n  startProgress,\n  stopProgress,\n} from '@vben-core/shared/utils';\n\nimport { acceptHMRUpdate, defineStore } from 'pinia';\n\ninterface TabbarState {\n  /**\n   * @zh_CN 当前打开的标签页列表缓存\n   */\n  cachedTabs: Set<string>;\n  /**\n   * @zh_CN 拖拽结束的索引\n   */\n  dragEndIndex: number;\n  /**\n   * @zh_CN 需要排除缓存的标签页\n   */\n  excludeCachedTabs: Set<string>;\n  /**\n   * @zh_CN 标签右键菜单列表\n   */\n  menuList: string[];\n  /**\n   * @zh_CN 是否刷新\n   */\n  renderRouteView?: boolean;\n  /**\n   * @zh_CN 当前打开的标签页列表\n   */\n  tabs: TabDefinition[];\n  /**\n   * @zh_CN 更新时间，用于一些更新场景，使用watch深度监听的话，会损耗性能\n   */\n  updateTime?: number;\n}\n\n/**\n * @zh_CN 访问权限相关\n */\nexport const useTabbarStore = defineStore('core-tabbar', {\n  actions: {\n    /**\n     * Close tabs in bulk\n     */\n    async _bulkCloseByKeys(keys: string[]) {\n      const keySet = new Set(keys);\n      this.tabs = this.tabs.filter(\n        (item) => !keySet.has(getTabKeyFromTab(item)),\n      );\n\n      await this.updateCacheTabs();\n    },\n    /**\n     * @zh_CN 关闭标签页\n     * @param tab\n     */\n    _close(tab: TabDefinition) {\n      if (isAffixTab(tab)) {\n        return;\n      }\n      const index = this.tabs.findIndex((item) => equalTab(item, tab));\n      index !== -1 && this.tabs.splice(index, 1);\n    },\n    /**\n     * @zh_CN 跳转到默认标签页\n     */\n    async _goToDefaultTab(router: Router) {\n      if (this.getTabs.length <= 0) {\n        return;\n      }\n      const firstTab = this.getTabs[0];\n      if (firstTab) {\n        await this._goToTab(firstTab, router);\n      }\n    },\n    /**\n     * @zh_CN 跳转到标签页\n     * @param tab\n     * @param router\n     */\n    async _goToTab(tab: TabDefinition, router: Router) {\n      const { params, path, query } = tab;\n      const toParams = {\n        params: params || {},\n        path,\n        query: query || {},\n      };\n      await router.replace(toParams);\n    },\n    /**\n     * @zh_CN 添加标签页\n     * @param routeTab\n     */\n    addTab(routeTab: TabDefinition): TabDefinition {\n      let tab = cloneTab(routeTab);\n      if (!tab.key) {\n        tab.key = getTabKey(routeTab);\n      }\n      if (!isTabShown(tab)) {\n        return tab;\n      }\n\n      const tabIndex = this.tabs.findIndex((item) => {\n        return equalTab(item, tab);\n      });\n\n      if (tabIndex === -1) {\n        const maxCount = preferences.tabbar.maxCount;\n        // 获取动态路由打开数，超过 0 即代表需要控制打开数\n        const maxNumOfOpenTab = (routeTab?.meta?.maxNumOfOpenTab ??\n          -1) as number;\n        // 如果动态路由层级大于 0 了，那么就要限制该路由的打开数限制了\n        // 获取到已经打开的动态路由数, 判断是否大于某一个值\n        if (\n          maxNumOfOpenTab > 0 &&\n          this.tabs.filter((tab) => tab.name === routeTab.name).length >=\n            maxNumOfOpenTab\n        ) {\n          // 关闭第一个\n          const index = this.tabs.findIndex(\n            (item) => item.name === routeTab.name,\n          );\n          index !== -1 && this.tabs.splice(index, 1);\n        } else if (maxCount > 0 && this.tabs.length >= maxCount) {\n          // 关闭第一个\n          const index = this.tabs.findIndex(\n            (item) =>\n              !Reflect.has(item.meta, 'affixTab') || !item.meta.affixTab,\n          );\n          index !== -1 && this.tabs.splice(index, 1);\n        }\n        this.tabs.push(tab);\n      } else {\n        // 页面已经存在，不重复添加选项卡，只更新选项卡参数\n        const currentTab = toRaw(this.tabs)[tabIndex];\n        const mergedTab = {\n          ...currentTab,\n          ...tab,\n          meta: { ...currentTab?.meta, ...tab.meta },\n        };\n        if (currentTab) {\n          const curMeta = currentTab.meta;\n          if (Reflect.has(curMeta, 'affixTab')) {\n            mergedTab.meta.affixTab = curMeta.affixTab;\n          }\n          if (Reflect.has(curMeta, 'newTabTitle')) {\n            mergedTab.meta.newTabTitle = curMeta.newTabTitle;\n          }\n        }\n        tab = mergedTab;\n        this.tabs.splice(tabIndex, 1, mergedTab);\n      }\n      this.updateCacheTabs();\n      return tab;\n    },\n    /**\n     * @zh_CN 关闭所有标签页\n     */\n    async closeAllTabs(router: Router) {\n      const newTabs = this.tabs.filter((tab) => isAffixTab(tab));\n      this.tabs = newTabs.length > 0 ? newTabs : [...this.tabs].splice(0, 1);\n      await this._goToDefaultTab(router);\n      this.updateCacheTabs();\n    },\n    /**\n     * @zh_CN 关闭左侧标签页\n     * @param tab\n     */\n    async closeLeftTabs(tab: TabDefinition) {\n      const index = this.tabs.findIndex((item) => equalTab(item, tab));\n\n      if (index < 1) {\n        return;\n      }\n\n      const leftTabs = this.tabs.slice(0, index);\n      const keys: string[] = [];\n\n      for (const item of leftTabs) {\n        if (!isAffixTab(item)) {\n          keys.push(item.key as string);\n        }\n      }\n      await this._bulkCloseByKeys(keys);\n    },\n    /**\n     * @zh_CN 关闭其他标签页\n     * @param tab\n     */\n    async closeOtherTabs(tab: TabDefinition) {\n      const closeKeys = this.tabs.map((item) => getTabKeyFromTab(item));\n\n      const keys: string[] = [];\n\n      for (const key of closeKeys) {\n        if (key !== getTabKeyFromTab(tab)) {\n          const closeTab = this.tabs.find(\n            (item) => getTabKeyFromTab(item) === key,\n          );\n          if (!closeTab) {\n            continue;\n          }\n          if (!isAffixTab(closeTab)) {\n            keys.push(closeTab.key as string);\n          }\n        }\n      }\n      await this._bulkCloseByKeys(keys);\n    },\n    /**\n     * @zh_CN 关闭右侧标签页\n     * @param tab\n     */\n    async closeRightTabs(tab: TabDefinition) {\n      const index = this.tabs.findIndex((item) => equalTab(item, tab));\n\n      if (index !== -1 && index < this.tabs.length - 1) {\n        const rightTabs = this.tabs.slice(index + 1);\n\n        const keys: string[] = [];\n        for (const item of rightTabs) {\n          if (!isAffixTab(item)) {\n            keys.push(item.key as string);\n          }\n        }\n        await this._bulkCloseByKeys(keys);\n      }\n    },\n\n    /**\n     * @zh_CN 关闭标签页\n     * @param tab\n     * @param router\n     */\n    async closeTab(tab: TabDefinition, router: Router) {\n      const { currentRoute } = router;\n      // 关闭不是激活选项卡\n      if (getTabKey(currentRoute.value) !== getTabKeyFromTab(tab)) {\n        this._close(tab);\n        this.updateCacheTabs();\n        return;\n      }\n      const index = this.getTabs.findIndex(\n        (item) => getTabKeyFromTab(item) === getTabKey(currentRoute.value),\n      );\n\n      const before = this.getTabs[index - 1];\n      const after = this.getTabs[index + 1];\n\n      // 下一个tab存在，跳转到下一个\n      if (after) {\n        this._close(tab);\n        await this._goToTab(after, router);\n        // 上一个tab存在，跳转到上一个\n      } else if (before) {\n        this._close(tab);\n        await this._goToTab(before, router);\n      } else {\n        console.error('Failed to close the tab; only one tab remains open.');\n      }\n    },\n\n    /**\n     * @zh_CN 通过key关闭标签页\n     * @param key\n     * @param router\n     */\n    async closeTabByKey(key: string, router: Router) {\n      const originKey = decodeURIComponent(key);\n      const index = this.tabs.findIndex(\n        (item) => getTabKeyFromTab(item) === originKey,\n      );\n      if (index === -1) {\n        return;\n      }\n\n      const tab = this.tabs[index];\n      if (tab) {\n        await this.closeTab(tab, router);\n      }\n    },\n\n    /**\n     * 根据tab的key获取tab\n     * @param key\n     */\n    getTabByKey(key: string) {\n      return this.getTabs.find(\n        (item) => getTabKeyFromTab(item) === key,\n      ) as TabDefinition;\n    },\n    /**\n     * @zh_CN 新窗口打开标签页\n     * @param tab\n     */\n    async openTabInNewWindow(tab: TabDefinition) {\n      openRouteInNewWindow(tab.fullPath || tab.path);\n    },\n\n    /**\n     * @zh_CN 固定标签页\n     * @param tab\n     */\n    async pinTab(tab: TabDefinition) {\n      const index = this.tabs.findIndex((item) => equalTab(item, tab));\n      if (index === -1) {\n        return;\n      }\n      const oldTab = this.tabs[index];\n      tab.meta.affixTab = true;\n      tab.meta.title = oldTab?.meta?.title as string;\n      // this.addTab(tab);\n      this.tabs.splice(index, 1, tab);\n      // 过滤固定tabs，后面更改affixTabOrder的值的话可能会有问题，目前行464排序affixTabs没有设置值\n      const affixTabs = this.tabs.filter((tab) => isAffixTab(tab));\n      // 获得固定tabs的index\n      const newIndex = affixTabs.findIndex((item) => equalTab(item, tab));\n      // 交换位置重新排序\n      await this.sortTabs(index, newIndex);\n    },\n\n    /**\n     * 刷新标签页\n     */\n    async refresh(router: Router | string) {\n      // 如果是Router路由，那么就根据当前路由刷新\n      // 如果是string字符串，为路由名称，则定向刷新指定标签页，不能是当前路由名称，否则不会刷新\n      if (typeof router === 'string') {\n        return await this.refreshByName(router);\n      }\n\n      const { currentRoute } = router;\n      const { name } = currentRoute.value;\n\n      this.excludeCachedTabs.add(name as string);\n      this.renderRouteView = false;\n      startProgress();\n\n      await new Promise((resolve) => setTimeout(resolve, 200));\n\n      this.excludeCachedTabs.delete(name as string);\n      this.renderRouteView = true;\n      stopProgress();\n    },\n\n    /**\n     * 根据路由名称刷新指定标签页\n     */\n    async refreshByName(name: string) {\n      this.excludeCachedTabs.add(name);\n      await new Promise((resolve) => setTimeout(resolve, 200));\n      this.excludeCachedTabs.delete(name);\n    },\n\n    /**\n     * @zh_CN 重置标签页标题\n     */\n    async resetTabTitle(tab: TabDefinition) {\n      if (tab?.meta?.newTabTitle) {\n        return;\n      }\n      const findTab = this.tabs.find((item) => equalTab(item, tab));\n      if (findTab) {\n        findTab.meta.newTabTitle = undefined;\n        await this.updateCacheTabs();\n      }\n    },\n\n    /**\n     * 设置固定标签页\n     * @param tabs\n     */\n    setAffixTabs(tabs: RouteRecordNormalized[]) {\n      for (const tab of tabs) {\n        tab.meta.affixTab = true;\n        this.addTab(routeToTab(tab));\n      }\n    },\n\n    /**\n     * @zh_CN 更新菜单列表\n     * @param list\n     */\n    setMenuList(list: string[]) {\n      this.menuList = list;\n    },\n\n    /**\n     * @zh_CN 设置标签页标题\n     *\n     * @zh_CN 支持设置静态标题字符串或计算属性作为动态标题\n     * @zh_CN 当标题为计算属性时,标题会随计算属性值变化而自动更新\n     * @zh_CN 适用于需要根据状态或多语言动态更新标题的场景\n     *\n     * @param {TabDefinition} tab - 标签页对象\n     * @param {ComputedRef<string> | string} title - 标题内容,支持静态字符串或计算属性\n     *\n     * @example\n     * // 设置静态标题\n     * setTabTitle(tab, '新标签页');\n     *\n     * @example\n     * // 设置动态标题\n     * setTabTitle(tab, computed(() => t('common.dashboard')));\n     */\n    async setTabTitle(tab: TabDefinition, title: ComputedRef<string> | string) {\n      const findTab = this.tabs.find((item) => equalTab(item, tab));\n\n      if (findTab) {\n        findTab.meta.newTabTitle = title;\n\n        await this.updateCacheTabs();\n      }\n    },\n    setUpdateTime() {\n      this.updateTime = Date.now();\n    },\n    /**\n     * @zh_CN 设置标签页顺序\n     * @param oldIndex\n     * @param newIndex\n     */\n    async sortTabs(oldIndex: number, newIndex: number) {\n      const currentTab = this.tabs[oldIndex];\n      if (!currentTab) {\n        return;\n      }\n      this.tabs.splice(oldIndex, 1);\n      this.tabs.splice(newIndex, 0, currentTab);\n      this.dragEndIndex = this.dragEndIndex + 1;\n    },\n\n    /**\n     * @zh_CN 切换固定标签页\n     * @param tab\n     */\n    async toggleTabPin(tab: TabDefinition) {\n      const affixTab = tab?.meta?.affixTab ?? false;\n\n      await (affixTab ? this.unpinTab(tab) : this.pinTab(tab));\n    },\n\n    /**\n     * @zh_CN 取消固定标签页\n     * @param tab\n     */\n    async unpinTab(tab: TabDefinition) {\n      const index = this.tabs.findIndex((item) => equalTab(item, tab));\n      if (index === -1) {\n        return;\n      }\n      const oldTab = this.tabs[index];\n      tab.meta.affixTab = false;\n      tab.meta.title = oldTab?.meta?.title as string;\n      // this.addTab(tab);\n      this.tabs.splice(index, 1, tab);\n      // 过滤固定tabs，后面更改affixTabOrder的值的话可能会有问题，目前行464排序affixTabs没有设置值\n      const affixTabs = this.tabs.filter((tab) => isAffixTab(tab));\n      // 获得固定tabs的index,使用固定tabs的下一个位置也就是活动tabs的第一个位置\n      const newIndex = affixTabs.length;\n      // 交换位置重新排序\n      await this.sortTabs(index, newIndex);\n    },\n    /**\n     * 根据当前打开的选项卡更新缓存\n     */\n    async updateCacheTabs() {\n      const cacheMap = new Set<string>();\n\n      for (const tab of this.tabs) {\n        // 跳过不需要持久化的标签页\n        const keepAlive = tab.meta?.keepAlive;\n        if (!keepAlive) {\n          continue;\n        }\n        (tab.matched || []).forEach((t, i) => {\n          if (i > 0) {\n            cacheMap.add(t.name as string);\n          }\n        });\n\n        const name = tab.name as string;\n        cacheMap.add(name);\n      }\n      this.cachedTabs = cacheMap;\n    },\n  },\n  getters: {\n    affixTabs(): TabDefinition[] {\n      const affixTabs = this.tabs.filter((tab) => isAffixTab(tab));\n\n      return affixTabs.sort((a, b) => {\n        const orderA = (a.meta?.affixTabOrder ?? 0) as number;\n        const orderB = (b.meta?.affixTabOrder ?? 0) as number;\n        return orderA - orderB;\n      });\n    },\n    getCachedTabs(): string[] {\n      return [...this.cachedTabs];\n    },\n    getExcludeCachedTabs(): string[] {\n      return [...this.excludeCachedTabs];\n    },\n    getMenuList(): string[] {\n      return this.menuList;\n    },\n    getTabs(): TabDefinition[] {\n      const normalTabs = this.tabs.filter((tab) => !isAffixTab(tab));\n      return [...this.affixTabs, ...normalTabs].filter(Boolean);\n    },\n  },\n  persist: [\n    // tabs不需要保存在localStorage\n    {\n      pick: ['tabs'],\n      storage: sessionStorage,\n    },\n  ],\n  state: (): TabbarState => ({\n    cachedTabs: new Set(),\n    dragEndIndex: 0,\n    excludeCachedTabs: new Set(),\n    menuList: [\n      'close',\n      'affix',\n      'maximize',\n      'reload',\n      'open-in-new-window',\n      'close-left',\n      'close-right',\n      'close-other',\n      'close-all',\n    ],\n    renderRouteView: true,\n    tabs: [],\n    updateTime: Date.now(),\n  }),\n});\n\n// 解决热更新问题\nconst hot = import.meta.hot;\nif (hot) {\n  hot.accept(acceptHMRUpdate(useTabbarStore, hot));\n}\n\n/**\n * @zh_CN 克隆路由,防止路由被修改\n * @param route\n */\nfunction cloneTab(route: TabDefinition): TabDefinition {\n  if (!route) {\n    return route;\n  }\n  const { matched, meta, ...opt } = route;\n  return {\n    ...opt,\n    matched: (matched\n      ? matched.map((item) => ({\n          meta: item.meta,\n          name: item.name,\n          path: item.path,\n        }))\n      : undefined) as RouteRecordNormalized[],\n    meta: {\n      ...meta,\n      newTabTitle: meta.newTabTitle,\n    },\n  };\n}\n\n/**\n * @zh_CN 是否是固定标签页\n * @param tab\n */\nfunction isAffixTab(tab: TabDefinition) {\n  return tab?.meta?.affixTab ?? false;\n}\n\n/**\n * @zh_CN 是否显示标签\n * @param tab\n */\nfunction isTabShown(tab: TabDefinition) {\n  const matched = tab?.matched ?? [];\n  return !tab.meta.hideInTab && matched.every((item) => !item.meta.hideInTab);\n}\n\n/**\n * 从route获取tab页的key\n * @param tab\n */\nfunction getTabKey(tab: RouteLocationNormalized | RouteRecordNormalized) {\n  const {\n    fullPath,\n    path,\n    meta: { fullPathKey } = {},\n    query = {},\n  } = tab as RouteLocationNormalized;\n  // pageKey可能是数组（查询参数重复时可能出现）\n  const pageKey = Array.isArray(query.pageKey)\n    ? query.pageKey[0]\n    : query.pageKey;\n  let rawKey;\n  if (pageKey) {\n    rawKey = pageKey;\n  } else {\n    rawKey = fullPathKey === false ? path : (fullPath ?? path);\n  }\n  try {\n    return decodeURIComponent(rawKey);\n  } catch {\n    return rawKey;\n  }\n}\n\n/**\n * 从tab获取tab页的key\n * 如果tab没有key,那么就从route获取key\n * @param tab\n */\nfunction getTabKeyFromTab(tab: TabDefinition): string {\n  return tab.key ?? getTabKey(tab);\n}\n\n/**\n * 比较两个tab是否相等\n * @param a\n * @param b\n */\nfunction equalTab(a: TabDefinition, b: TabDefinition) {\n  return getTabKeyFromTab(a) === getTabKeyFromTab(b);\n}\n\nfunction routeToTab(route: RouteRecordNormalized) {\n  return {\n    meta: route.meta,\n    name: route.name,\n    path: route.path,\n    key: getTabKey(route),\n  } as TabDefinition;\n}\n\nexport { getTabKey };\n"
  },
  {
    "path": "packages/stores/src/modules/user.test.ts",
    "content": "import { createPinia, setActivePinia } from 'pinia';\nimport { beforeEach, describe, expect, it } from 'vitest';\n\nimport { useUserStore } from './user';\n\ndescribe('useUserStore', () => {\n  beforeEach(() => {\n    setActivePinia(createPinia());\n  });\n\n  it('returns correct userInfo', () => {\n    const store = useUserStore();\n    const userInfo: any = { name: 'Jane Doe', roles: [{ value: 'user' }] };\n    store.setUserInfo(userInfo);\n    expect(store.userInfo).toEqual(userInfo);\n  });\n\n  // 测试重置用户信息时的行为\n  it('clears userInfo and userRoles when setting null userInfo', () => {\n    const store = useUserStore();\n    store.setUserInfo({\n      roles: [{ roleName: 'User', value: 'user' }],\n    } as any);\n    expect(store.userInfo).not.toBeNull();\n    expect(store.userRoles.length).toBeGreaterThan(0);\n\n    store.setUserInfo(null as any);\n    expect(store.userInfo).toBeNull();\n    expect(store.userRoles).toEqual([]);\n  });\n\n  // 测试在没有用户角色时返回空数组\n  it('returns an empty array for userRoles if not set', () => {\n    const store = useUserStore();\n    expect(store.userRoles).toEqual([]);\n  });\n});\n"
  },
  {
    "path": "packages/stores/src/modules/user.ts",
    "content": "import { acceptHMRUpdate, defineStore } from 'pinia';\n\ninterface BasicUserInfo {\n  [key: string]: any;\n  /**\n   * 头像\n   */\n  avatar: string;\n  /**\n   * 邮箱\n   */\n  email: string;\n  /**\n   * 用户权限\n   */\n  permissions: string[];\n  /**\n   * 用户昵称\n   */\n  realName: string;\n  /**\n   * 用户角色\n   */\n  roles: string[];\n  /**\n   * 用户id\n   */\n  userId: number | string;\n  /**\n   * 用户名\n   */\n  username: string;\n}\n\ninterface AccessState {\n  /**\n   * 用户信息\n   */\n  userInfo: BasicUserInfo | null;\n  /**\n   * 用户角色\n   */\n  userRoles: string[];\n}\n\n/**\n * @zh_CN 用户信息相关\n */\nexport const useUserStore = defineStore('core-user', {\n  actions: {\n    setUserInfo(userInfo: BasicUserInfo | null) {\n      // 设置用户信息\n      this.userInfo = userInfo;\n      // 设置角色信息\n      const roles = userInfo?.roles ?? [];\n      this.setUserRoles(roles);\n    },\n    setUserRoles(roles: string[]) {\n      this.userRoles = roles;\n    },\n  },\n  state: (): AccessState => ({\n    userInfo: null,\n    userRoles: [],\n  }),\n});\n\n// 解决热更新问题\nconst hot = import.meta.hot;\nif (hot) {\n  hot.accept(acceptHMRUpdate(useUserStore, hot));\n}\n"
  },
  {
    "path": "packages/stores/src/setup.ts",
    "content": "import type { Pinia } from 'pinia';\n\nimport type { App } from 'vue';\n\nimport { createPinia } from 'pinia';\nimport SecureLS from 'secure-ls';\n\nlet pinia: Pinia;\n\nexport interface InitStoreOptions {\n  /**\n   * @zh_CN 应用名,由于 @vben/stores 是公用的，后续可能有多个app，为了防止多个app缓存冲突，可在这里配置应用名,应用名将被用于持久化的前缀\n   */\n  namespace: string;\n}\n\n/**\n * @zh_CN 初始化pinia\n */\nexport async function initStores(app: App, options: InitStoreOptions) {\n  const { createPersistedState } = await import('pinia-plugin-persistedstate');\n  pinia = createPinia();\n  const { namespace } = options;\n  const ls = new SecureLS({\n    encodingType: 'aes',\n    encryptionSecret: import.meta.env.VITE_APP_STORE_SECURE_KEY,\n    isCompression: true,\n    // @ts-ignore secure-ls does not have a type definition for this\n    metaKey: `${namespace}-secure-meta`,\n  });\n  pinia.use(\n    createPersistedState({\n      // key $appName-$store.id\n      key: (storeKey) => `${namespace}-${storeKey}`,\n      storage: import.meta.env.DEV\n        ? localStorage\n        : {\n            getItem(key) {\n              return ls.get(key);\n            },\n            setItem(key, value) {\n              ls.set(key, value);\n            },\n          },\n    }),\n  );\n  app.use(pinia);\n  return pinia;\n}\n\nexport function resetAllStores() {\n  if (!pinia) {\n    console.error('Pinia is not installed');\n    return;\n  }\n  const allStores = (pinia as any)._s;\n  for (const [_key, store] of allStores) {\n    store.$reset();\n  }\n}\n"
  },
  {
    "path": "packages/stores/tsconfig.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/tsconfig\",\n  \"extends\": \"@vben/tsconfig/web.json\",\n  \"include\": [\"src\", \"shim-pinia.d.ts\"]\n}\n"
  },
  {
    "path": "packages/styles/README.md",
    "content": "# @vben/styles\n\n用于多个 `app` 公用的样式文件，继承了 `@vben-core/design` 的所有能力。业务上有通用的样式文件可以放在这里。\n\n## 用法\n\n### 添加依赖\n\n```bash\n# 进入目标应用目录，例如 apps/xxxx-app\n# cd apps/xxxx-app\npnpm add @vben/styles\n```\n\n### 使用\n\n```ts\nimport '@vben/styles';\n```\n"
  },
  {
    "path": "packages/styles/package.json",
    "content": "{\n  \"name\": \"@vben/styles\",\n  \"version\": \"5.5.9\",\n  \"homepage\": \"https://github.com/vbenjs/vue-vben-admin\",\n  \"bugs\": \"https://github.com/vbenjs/vue-vben-admin/issues\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/vbenjs/vue-vben-admin.git\",\n    \"directory\": \"packages/styles\"\n  },\n  \"license\": \"MIT\",\n  \"type\": \"module\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./src/index.ts\",\n      \"default\": \"./src/index.ts\"\n    },\n    \"./antd\": {\n      \"default\": \"./src/antd/index.css\"\n    },\n    \"./ele\": {\n      \"default\": \"./src/ele/index.css\"\n    },\n    \"./naive\": {\n      \"default\": \"./src/naive/index.css\"\n    },\n    \"./global\": {\n      \"default\": \"./src/global/index.scss\"\n    }\n  },\n  \"dependencies\": {\n    \"@vben-core/design\": \"workspace:*\"\n  }\n}\n"
  },
  {
    "path": "packages/styles/src/antd/index.css",
    "content": "/* ant-design-vue 组件库的一些样式重置 */\n\n.ant-app {\n  width: 100%;\n  height: 100%;\n  overscroll-behavior: none;\n  color: inherit;\n}\n\n.ant-btn {\n  .anticon {\n    display: inline-flex;\n  }\n\n  /* * 修复按钮添加图标时的位置问题 */\n  > svg {\n    display: inline-block;\n  }\n\n  > svg + span {\n    margin-inline-start: 6px;\n  }\n}\n\n.ant-tag {\n  > svg {\n    display: inline-block;\n  }\n\n  > svg + span {\n    margin-inline-start: 4px;\n  }\n}\n\n.ant-message-notice-content,\n.ant-notification-notice {\n  @apply dark:border-border/60 dark:border;\n}\n\n.form-valid-error {\n  /** select 选择器的样式 */\n\n  .ant-select:not(.valid-success) .ant-select-selector:not(.valid-success) {\n    border-color: hsl(var(--destructive)) !important;\n  }\n\n  .ant-select-focused .ant-select-selector {\n    box-shadow: 0 0 0 2px rgb(255 38 5 / 6%) !important;\n  }\n\n  /** 数字输入框样式 */\n  .ant-input-number-focused {\n    box-shadow: 0 0 0 2px rgb(255 38 5 / 6%);\n  }\n\n  /** 密码输入框样式 */\n  .ant-input-affix-wrapper:hover {\n    border-color: hsl(var(--destructive));\n    box-shadow: 0 0 0 2px rgb(255 38 5 / 6%);\n  }\n\n  .ant-input:not(.valid-success) {\n    border-color: hsl(var(--destructive)) !important;\n  }\n}\n\n/** 区间选择器下面来回切换时的样式 */\n.form-valid-error .ant-picker-active-bar {\n  background-color: hsl(var(--destructive));\n}\n\n/** 时间选择器的样式 */\n.form-valid-error .ant-picker-focused {\n  box-shadow: 0 0 0 2px rgb(255 38 5 / 6%);\n}\n\n/** 前后置小圆点样式 */\n.dot-before-common {\n  @apply before:relative before:top-[-2px] before:mr-[5px] before:inline-block before:size-[6px] before:rounded-full before:content-[''];\n}\n\n.dot-before-red {\n  @apply dot-before-common before:bg-red-500;\n}\n\n.dot-before-green {\n  @apply dot-before-common before:bg-green-500;\n}\n\n/**\nvxe表格右上角toolbar元素之间的间距\n*/\n.vxe-button + .vxe-button.type--button {\n  margin-left: 8px !important;\n}\n\n/**\nvxe默认圆角\n*/\nhtml {\n  --vxe-ui-border-radius: 8px !important;\n}\n\n/**\nvxe表格loading 只加载表格 不加载上面的表单\n*/\n.vxe-grid.is--loading::before {\n  content: none !important;\n}\n\n/**\n自定义success按钮样式\nghost按钮专用!\n*/\n.btn-success {\n  color: hsl(var(--success)) !important;\n  border-color: hsl(var(--success)) !important;\n}\n\n.btn-success:hover {\n  color: hsl(var(--success) / 50%) !important;\n  border-color: hsl(var(--success) / 50%) !important;\n}\n\nhtml.dark button[disabled].btn-success {\n  color: rgb(242 242 242 / 25%) !important;\n  border-color: hsl(240deg 3.7% 22%) !important;\n}\n\nbutton[disabled].btn-success {\n  color: rgb(50 54 57 / 25%) !important;\n  border-color: hsl(240deg 5.9% 90%) !important;\n}\n"
  },
  {
    "path": "packages/styles/src/ele/index.css",
    "content": ".el-card {\n  --el-card-border-radius: var(--radius) !important;\n}\n\n.form-valid-error {\n  /** select 选择器的样式 */\n  .el-select .el-select__wrapper {\n    box-shadow: 0 0 0 1px var(--el-color-danger) inset;\n  }\n\n  /** input 选择器的样式 */\n  .el-input .el-input__wrapper {\n    box-shadow: 0 0 0 1px var(--el-color-danger) inset;\n  }\n\n  /** radio和checkbox 选择器的样式 */\n  .el-radio .el-radio__inner,\n  .el-checkbox .el-checkbox__inner {\n    border: 1px solid var(--el-color-danger);\n  }\n\n  .el-checkbox-button .el-checkbox-button__inner,\n  .el-radio-button .el-radio-button__inner {\n    border: 1px solid var(--el-color-danger);\n  }\n\n  .el-checkbox-button:first-child .el-checkbox-button__inner,\n  .el-radio-button:first-child .el-radio-button__inner {\n    border-left: 1px solid var(--el-color-danger);\n  }\n\n  .el-checkbox-button:not(:first-child) .el-checkbox-button__inner,\n  .el-radio-button:not(:first-child) .el-radio-button__inner {\n    border-left: none;\n  }\n\n  .el-textarea .el-textarea__inner {\n    border: 1px solid var(--el-color-danger);\n  }\n}\n\nhtml .el-loading-mask {\n  z-index: 1000;\n}\n"
  },
  {
    "path": "packages/styles/src/global/index.scss",
    "content": "@use '@vben-core/design/bem' as *;\n"
  },
  {
    "path": "packages/styles/src/index.ts",
    "content": "import '@vben-core/design';\n"
  },
  {
    "path": "packages/styles/src/naive/index.css",
    "content": ".form-valid-error {\n  .n-base-selection__state-border,\n  .n-input__state-border,\n  .n-radio-group__splitor {\n    border: var(--n-border-error);\n  }\n\n  .n-radio-group .n-radio-button,\n  .n-radio-group .n-radio-group__splitor {\n    --n-button-border-color: rgb(255 56 96);\n  }\n\n  .n-radio__dot {\n    --n-box-shadow: inset 0 0 0 1px rgb(255 56 96);\n  }\n\n  .n-checkbox-box__border {\n    --n-border: 1px solid rgb(255 56 96);\n  }\n}\n"
  },
  {
    "path": "packages/styles/tsconfig.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/tsconfig\",\n  \"extends\": \"@vben/tsconfig/web.json\",\n  \"include\": [\"src\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "packages/types/README.md",
    "content": "# @vben/types\n\n用于多个 `app` 公用的工具类型，继承了 `@vben-core/typings` 的所有能力。业务上有通用的类型定义可以放在这里。\n\n## 用法\n\n### 添加依赖\n\n```bash\n# 进入目标应用目录，例如 apps/xxxx-app\n# cd apps/xxxx-app\npnpm add @vben/types\n```\n\n### 使用\n\n```ts\n// 推荐加上 type\nimport type { SelectOption } from '@vben/types';\n```\n"
  },
  {
    "path": "packages/types/global.d.ts",
    "content": "import type { RouteMeta as IRouteMeta } from '@vben-core/typings';\n\nimport 'vue-router';\n\ndeclare module 'vue-router' {\n  // eslint-disable-next-line @typescript-eslint/no-empty-object-type\n  interface RouteMeta extends IRouteMeta {}\n}\n\nexport interface VbenAdminProAppConfigRaw {\n  // 后端接口地址\n  VITE_GLOB_API_URL: string;\n  // 客户端ID\n  VITE_GLOB_APP_CLIENT_ID: string;\n  // # 全局加密开关(即开启了加解密功能才会生效 不是全部接口加密 需要和后端对应)\n  VITE_GLOB_ENABLE_ENCRYPT: string;\n  // RSA请求解密私钥\n  VITE_GLOB_RSA_PRIVATE_KEY: string;\n  // RSA请求加密公钥\n  VITE_GLOB_RSA_PUBLIC_KEY: string;\n  // 是否开启sse  注意从配置文件获取的类型为string\n  VITE_GLOB_SSE_ENABLE: string;\n  // 开启websocket  注意从配置文件获取的类型为string\n  VITE_GLOB_WEBSOCKET_ENABLE: string;\n}\n\nexport interface ApplicationConfig {\n  // 后端接口地址\n  apiURL: string;\n  // 客户端key\n  clientId: string;\n  // 全局加密开关(即开启了加解密功能才会生效 不是全部接口加密 需要和后端对应)\n  enableEncrypt: boolean;\n  // RSA响应解密私钥\n  rsaPrivateKey: string;\n  // RSA请求加密公钥\n  rsaPublicKey: string;\n  // 是否开启sse\n  sseEnable: boolean;\n  // 是否开启\n  websocketEnable: boolean;\n}\n\ndeclare global {\n  interface Window {\n    _VBEN_ADMIN_PRO_APP_CONF_: VbenAdminProAppConfigRaw;\n  }\n}\n"
  },
  {
    "path": "packages/types/package.json",
    "content": "{\n  \"name\": \"@vben/types\",\n  \"version\": \"5.5.9\",\n  \"homepage\": \"https://github.com/vbenjs/vue-vben-admin\",\n  \"bugs\": \"https://github.com/vbenjs/vue-vben-admin/issues\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/vbenjs/vue-vben-admin.git\",\n    \"directory\": \"packages/types\"\n  },\n  \"license\": \"MIT\",\n  \"type\": \"module\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./src/index.ts\",\n      \"default\": \"./src/index.ts\"\n    },\n    \"./global\": {\n      \"types\": \"./global.d.ts\"\n    }\n  },\n  \"dependencies\": {\n    \"@vben-core/typings\": \"workspace:*\",\n    \"vue\": \"catalog:\",\n    \"vue-router\": \"catalog:\"\n  }\n}\n"
  },
  {
    "path": "packages/types/src/index.ts",
    "content": "export type * from './user';\nexport type * from '@vben-core/typings';\n"
  },
  {
    "path": "packages/types/src/user.ts",
    "content": "import type { BasicUserInfo } from '@vben-core/typings';\n\n/** 用户信息 */\ninterface UserInfo extends BasicUserInfo {\n  /**\n   * 拓展使用\n   */\n  [key: string]: any;\n}\n\nexport type { UserInfo };\n"
  },
  {
    "path": "packages/types/tsconfig.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/tsconfig\",\n  \"extends\": \"@vben/tsconfig/web.json\",\n  \"include\": [\"src\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "packages/utils/README.md",
    "content": "# @vben/utils\n\n用于多个 `app` 公用的工具包，继承了 `@vben-core/shared/utils` 的所有能力。业务上有通用的工具函数可以放在这里。\n\n## 用法\n\n### 添加依赖\n\n```bash\n# 进入目标应用目录，例如 apps/xxxx-app\n# cd apps/xxxx-app\npnpm add @vben/utils\n```\n\n### 使用\n\n```ts\nimport { isString } from '@vben/utils';\n```\n"
  },
  {
    "path": "packages/utils/package.json",
    "content": "{\n  \"name\": \"@vben/utils\",\n  \"version\": \"5.5.9\",\n  \"homepage\": \"https://github.com/vbenjs/vue-vben-admin\",\n  \"bugs\": \"https://github.com/vbenjs/vue-vben-admin/issues\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/vbenjs/vue-vben-admin.git\",\n    \"directory\": \"packages/utils\"\n  },\n  \"license\": \"MIT\",\n  \"type\": \"module\",\n  \"sideEffects\": [\n    \"**/*.css\"\n  ],\n  \"exports\": {\n    \".\": {\n      \"types\": \"./src/index.ts\",\n      \"default\": \"./src/index.ts\"\n    }\n  },\n  \"dependencies\": {\n    \"@vben-core/shared\": \"workspace:*\",\n    \"@vben-core/typings\": \"workspace:*\",\n    \"crypto-js\": \"^4.2.0\",\n    \"file-type\": \"^19.5.0\",\n    \"jsencrypt\": \"^3.5.4\",\n    \"sm-crypto\": \"^0.3.13\",\n    \"vue-router\": \"catalog:\"\n  },\n  \"devDependencies\": {\n    \"@types/crypto-js\": \"^4.2.2\",\n    \"@types/sm-crypto\": \"^0.3.4\"\n  }\n}\n"
  },
  {
    "path": "packages/utils/src/encryption/base.ts",
    "content": "export interface EncryptionOptions {\n  /**\n   * 私钥\n   */\n  privateKey: string;\n\n  /**\n   * 公钥\n   */\n  publicKey: string;\n}\n\n/**\n * 非对称加解密 抽象类\n * 提供基本的加密和解密功能接口\n */\nexport abstract class BaseAsymmetricEncryption {\n  /**\n   * 私钥\n   */\n  protected privateKey: string;\n\n  /**\n   * 公钥\n   */\n  protected publicKey: string;\n\n  /**\n   * 构造函数\n   * @param options 加解密选项，包含公钥和私钥\n   */\n  constructor(options: EncryptionOptions) {\n    this.publicKey = options.publicKey;\n    this.privateKey = options.privateKey;\n  }\n\n  /**\n   * 解密方法\n   * @param encryptedData 解密后的数据\n   * @returns 解密后的原始数据\n   */\n  abstract decrypt(encryptedData: string): string;\n\n  /**\n   * 加密方法\n   * @param data 需要加密的数据\n   * @returns 加密后的数据\n   */\n  abstract encrypt(data: string): string;\n}\n\n/**\n * 对称加解密抽象类\n */\nexport abstract class BaseSymmetricEncryption {\n  /**\n   * 解密方法\n   * @param data 解密后的数据\n   * @param key 密钥\n   * @returns 解密后的原始数据\n   */\n  abstract decrypt(data: string, key: string): string;\n\n  /**\n   * 加密方法\n   * @param data 需要加密的数据\n   * @param key 密钥\n   * @returns 加密后的数据\n   */\n  abstract encrypt(data: string, key: string): string;\n}\n"
  },
  {
    "path": "packages/utils/src/encryption/crypto.ts",
    "content": "import CryptoJS from 'crypto-js';\n\n/**\n * 随机字符串\n *\n * @returns str\n */\nexport function randomStr(length = 32) {\n  const str = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';\n  let result = '';\n  for (let i = length; i > 0; --i)\n    result += str[Math.floor(Math.random() * str.length)];\n  return result;\n}\n\n/**\n * base64编码\n * @param str\n * @returns base64编码\n */\nexport function encodeBase64(str: string) {\n  return CryptoJS.enc.Base64.stringify(CryptoJS.enc.Utf8.parse(str));\n}\n\n/**\n * 解码base64\n */\nexport function decodeBase64(str: string) {\n  return CryptoJS.enc.Base64.parse(str).toString(CryptoJS.enc.Utf8);\n}\n"
  },
  {
    "path": "packages/utils/src/encryption/impl/aes.ts",
    "content": "import CryptoJS from 'crypto-js';\n\nimport { BaseSymmetricEncryption } from '../base';\n\n/**\n * AES 实现\n */\nexport class AesEncryption extends BaseSymmetricEncryption {\n  override decrypt(data: string, key: string): string {\n    // 必须格式化字符串才能正常使用\n    const aesKey = CryptoJS.enc.Utf8.parse(key);\n    const decrypted = CryptoJS.AES.decrypt(data, aesKey, {\n      mode: CryptoJS.mode.ECB,\n      padding: CryptoJS.pad.Pkcs7,\n    });\n    return decrypted.toString(CryptoJS.enc.Utf8);\n  }\n\n  override encrypt(data: string, key: string): string {\n    // 必须格式化字符串才能正常使用\n    const aesKey = CryptoJS.enc.Utf8.parse(key);\n    const encrypted = CryptoJS.AES.encrypt(data, aesKey, {\n      mode: CryptoJS.mode.ECB,\n      padding: CryptoJS.pad.Pkcs7,\n    });\n    return encrypted.toString();\n  }\n}\n"
  },
  {
    "path": "packages/utils/src/encryption/impl/rsa.ts",
    "content": "import JSEncrypt from 'jsencrypt';\n\nimport { BaseAsymmetricEncryption } from '../base';\n\n/**\n * RSA 实现\n */\nexport class RsaEncryption extends BaseAsymmetricEncryption {\n  override decrypt(str: string): string {\n    const instance = new JSEncrypt();\n    instance.setPrivateKey(this.privateKey);\n    const ret = instance.decrypt(str);\n\n    if (ret === false) {\n      throw new Error('RsaEncryption decrypt error');\n    }\n\n    return ret;\n  }\n\n  override encrypt(str: string): string {\n    const instance = new JSEncrypt();\n    instance.setPublicKey(this.publicKey);\n    const ret = instance.encrypt(str);\n    if (ret === false) {\n      throw new Error('RsaEncryption encrypt error');\n    }\n    return ret;\n  }\n}\n"
  },
  {
    "path": "packages/utils/src/encryption/impl/sm2.ts",
    "content": "/* eslint-disable prefer-template */\n/* eslint-disable no-console */\nimport { sm2 } from 'sm-crypto';\n\nimport { BaseAsymmetricEncryption } from '../base';\n\n/**\n * SM2 实现\n * 注意生成的公钥必须为04开头 或者使用下面的generateSm2KeyPair生成\n * @see https://tool.hiofd.com/sm2-key-gen/ 这里可以生成04开头的SM2密钥对\n */\nexport class Sm2Encryption extends BaseAsymmetricEncryption {\n  override decrypt(hexStr: string): string {\n    /**\n     * 后端必须使用`EncryptUtils.encryptBySm2Hex`来加密而不是base64\n     * 后端返回会固定带04前缀 需要去除\n     *\n     * @see https://github.com/JuneAndGreen/sm-crypto?tab=readme-ov-file#%E5%8A%A0%E5%AF%86%E8%A7%A3%E5%AF%86\n     * ps：密文会在解密时自动补充 04，如遇到其他工具补充的 04 需手动去除再传入。\n     */\n    if (hexStr.startsWith('04')) {\n      hexStr = hexStr.slice(2);\n    }\n    return sm2.doDecrypt(hexStr, this.privateKey);\n  }\n\n  override encrypt(str: string): string {\n    /**\n     * sm2解密有千分之几的错误，报异常java.lang.IllegalArgumentException: Invalid point coordinates\n     * @see https://github.com/chinabugotech/hutool/issues/3262\n     *\n     * 固定加上04前缀 避免出现上述问题\n     */\n    return '04' + sm2.doEncrypt(str, this.publicKey);\n  }\n}\n\nexport function generateSm2KeyPair() {\n  const { privateKey, publicKey } = sm2.generateKeyPairHex();\n  return {\n    privateKey,\n    publicKey,\n  };\n}\n\nexport function logSm2KeyPair() {\n  const { privateKey, publicKey } = generateSm2KeyPair();\n  console.log('privateKey', privateKey);\n  console.log('publicKey', publicKey);\n}\n"
  },
  {
    "path": "packages/utils/src/encryption/impl/sm4.ts",
    "content": "import CryptoJS from 'crypto-js';\nimport { sm4 } from 'sm-crypto';\n\nimport { BaseSymmetricEncryption } from '../base';\n\n/**\n * SM4 实现\n */\nexport class Sm4Encryption extends BaseSymmetricEncryption {\n  /**\n   * 解密 data必须为hex字符串 可使用后端EncryptUtils.encryptBySm4Hex来加密\n   * @param hexString 待解密数据 只接受hex类型的字符串\n   * @param key 秘钥\n   * @returns result\n   */\n  override decrypt(hexString: string, key: string): string {\n    this.checkKey(key);\n    const keyHex = CryptoJS.enc.Hex.stringify(CryptoJS.enc.Utf8.parse(key));\n    return sm4.decrypt(hexString, keyHex);\n  }\n\n  override encrypt(data: string, key: string): string {\n    this.checkKey(key);\n    /**\n     * 转hex字符串\n     * encrypt方法的key需要为`16进制字符串`而非`原始字符串`\n     * 比如字符串ab a为0x61 b为0x62  转字符串为 6162\n     */\n    const keyHex = CryptoJS.enc.Hex.stringify(CryptoJS.enc.Utf8.parse(key));\n\n    return sm4.encrypt(data, keyHex);\n  }\n\n  /**\n   * key长度只能为16位字符串\n   * @param key key\n   */\n  private checkKey(key: string) {\n    if (key.length !== 16) {\n      throw new Error('SM4 key must be 16 bytes');\n    }\n  }\n}\n"
  },
  {
    "path": "packages/utils/src/encryption/index.ts",
    "content": "export * from './base';\nexport * from './crypto';\nexport * from './impl/aes';\nexport * from './impl/rsa';\nexport * from './impl/sm2';\nexport * from './impl/sm4';\n"
  },
  {
    "path": "packages/utils/src/helpers/__tests__/enum-options.test.ts",
    "content": "import { describe, expect, it } from 'vitest';\n\nimport { optionsToEnum } from '../enum-options';\n\ndescribe('optionsToEnum Test', () => {\n  it('should return an enum object', () => {\n    const genderOptions = [\n      { label: '男', value: 1, enumName: 'GENDER_MALE' },\n      { label: '女', value: 2, enumName: 'GENDER_FEMALE' },\n    ] as const;\n\n    const enumTest = optionsToEnum(genderOptions);\n    const male = enumTest.GENDER_MALE;\n    const female = enumTest.GENDER_FEMALE;\n\n    expect(male).toBe(1);\n    expect(female).toBe(2);\n  });\n});\n"
  },
  {
    "path": "packages/utils/src/helpers/__tests__/find-menu-by-path.test.ts",
    "content": "import { describe, expect, it } from 'vitest';\n\nimport { findMenuByPath, findRootMenuByPath } from '../find-menu-by-path';\n\n// 示例菜单数据\nconst menus: any[] = [\n  { path: '/', children: [] },\n  { path: '/about', children: [] },\n  {\n    path: '/contact',\n    children: [\n      { path: '/contact/email', children: [] },\n      { path: '/contact/phone', children: [] },\n    ],\n  },\n  {\n    path: '/services',\n    children: [\n      { path: '/services/design', children: [] },\n      {\n        path: '/services/development',\n        children: [{ path: '/services/development/web', children: [] }],\n      },\n    ],\n  },\n];\n\ndescribe('menu Finder Tests', () => {\n  it('finds a top-level menu', () => {\n    const menu = findMenuByPath(menus, '/about');\n    expect(menu).toBeDefined();\n    expect(menu?.path).toBe('/about');\n  });\n\n  it('finds a nested menu', () => {\n    const menu = findMenuByPath(menus, '/services/development/web');\n    expect(menu).toBeDefined();\n    expect(menu?.path).toBe('/services/development/web');\n  });\n\n  it('returns null for a non-existent path', () => {\n    const menu = findMenuByPath(menus, '/non-existent');\n    expect(menu).toBeNull();\n  });\n\n  it('handles empty menus list', () => {\n    const menu = findMenuByPath([], '/about');\n    expect(menu).toBeNull();\n  });\n\n  it('handles menu items without children', () => {\n    const menu = findMenuByPath(\n      [{ path: '/only', children: undefined }] as any[],\n      '/only',\n    );\n    expect(menu).toBeDefined();\n    expect(menu?.path).toBe('/only');\n  });\n\n  it('finds root menu by path', () => {\n    const { findMenu, rootMenu, rootMenuPath } = findRootMenuByPath(\n      menus,\n      '/services/development/web',\n    );\n\n    expect(findMenu).toBeDefined();\n    expect(rootMenu).toBeUndefined();\n    expect(rootMenuPath).toBeUndefined();\n    expect(findMenu?.path).toBe('/services/development/web');\n  });\n\n  it('returns null for undefined or empty path', () => {\n    const menuUndefinedPath = findMenuByPath(menus);\n    const menuEmptyPath = findMenuByPath(menus, '');\n    expect(menuUndefinedPath).toBeNull();\n    expect(menuEmptyPath).toBeNull();\n  });\n\n  it('checks for root menu when path does not exist', () => {\n    const { findMenu, rootMenu, rootMenuPath } = findRootMenuByPath(\n      menus,\n      '/non-existent',\n    );\n    expect(findMenu).toBeNull();\n    expect(rootMenu).toBeUndefined();\n    expect(rootMenuPath).toBeUndefined();\n  });\n});\n"
  },
  {
    "path": "packages/utils/src/helpers/__tests__/generate-menus.test.ts",
    "content": "import type { Router, RouteRecordRaw } from 'vue-router';\n\nimport { createRouter, createWebHistory } from 'vue-router';\n\nimport { describe, expect, it, vi } from 'vitest';\n\nimport { generateMenus } from '../generate-menus';\n\n// Nested route setup to test child inclusion and hideChildrenInMenu functionality\n\ndescribe('generateMenus', () => {\n  // 模拟路由数据\n  const mockRoutes = [\n    {\n      meta: { icon: 'home-icon', title: '首页' },\n      name: 'home',\n      path: '/home',\n    },\n    {\n      meta: { hideChildrenInMenu: true, icon: 'about-icon', title: '关于' },\n      name: 'about',\n      path: '/about',\n      children: [\n        {\n          path: 'team',\n          name: 'team',\n          meta: { icon: 'team-icon', title: '团队' },\n        },\n      ],\n    },\n  ] as RouteRecordRaw[];\n\n  // 模拟 Vue 路由器实例\n  const mockRouter = {\n    getRoutes: vi.fn(() => [\n      { name: 'home', path: '/home' },\n      { name: 'about', path: '/about' },\n      { name: 'team', path: '/about/team' },\n    ]),\n  };\n\n  it('the correct menu list should be generated according to the route', async () => {\n    const expectedMenus = [\n      {\n        badge: undefined,\n        badgeType: undefined,\n        badgeVariants: undefined,\n        icon: 'home-icon',\n        name: '首页',\n        order: undefined,\n        parent: undefined,\n        parents: undefined,\n        path: '/home',\n        show: true,\n        children: [],\n      },\n      {\n        badge: undefined,\n        badgeType: undefined,\n        badgeVariants: undefined,\n        icon: 'about-icon',\n        name: '关于',\n        order: undefined,\n        parent: undefined,\n        parents: undefined,\n        path: '/about',\n        show: true,\n        children: [],\n      },\n    ];\n\n    const menus = generateMenus(mockRoutes, mockRouter as any);\n    expect(menus).toEqual(expectedMenus);\n  });\n\n  it('includes additional meta properties in menu items', async () => {\n    const mockRoutesWithMeta = [\n      {\n        meta: { icon: 'user-icon', order: 1, title: 'Profile' },\n        name: 'profile',\n        path: '/profile',\n      },\n    ] as RouteRecordRaw[];\n\n    const menus = generateMenus(mockRoutesWithMeta, mockRouter as any);\n    expect(menus).toEqual([\n      {\n        badge: undefined,\n        badgeType: undefined,\n        badgeVariants: undefined,\n        icon: 'user-icon',\n        name: 'Profile',\n        order: 1,\n        parent: undefined,\n        parents: undefined,\n        path: '/profile',\n        show: true,\n        children: [],\n      },\n    ]);\n  });\n\n  it('handles dynamic route parameters correctly', async () => {\n    const mockRoutesWithParams = [\n      {\n        meta: { icon: 'details-icon', title: 'User Details' },\n        name: 'userDetails',\n        path: '/users/:userId',\n      },\n    ] as RouteRecordRaw[];\n\n    const menus = generateMenus(mockRoutesWithParams, mockRouter as any);\n    expect(menus).toEqual([\n      {\n        badge: undefined,\n        badgeType: undefined,\n        badgeVariants: undefined,\n        icon: 'details-icon',\n        name: 'User Details',\n        order: undefined,\n        parent: undefined,\n        parents: undefined,\n        path: '/users/:userId',\n        show: true,\n        children: [],\n      },\n    ]);\n  });\n\n  it('processes routes with redirects correctly', async () => {\n    const mockRoutesWithRedirect = [\n      {\n        name: 'redirectedRoute',\n        path: '/old-path',\n        redirect: '/new-path',\n      },\n      {\n        meta: { icon: 'path-icon', title: 'New Path' },\n        name: 'newPath',\n        path: '/new-path',\n      },\n    ] as RouteRecordRaw[];\n\n    const menus = generateMenus(mockRoutesWithRedirect, mockRouter as any);\n    expect(menus).toEqual([\n      // Assuming your generateMenus function excludes redirect routes from the menu\n      {\n        badge: undefined,\n        badgeType: undefined,\n        badgeVariants: undefined,\n        icon: undefined,\n        name: 'redirectedRoute',\n        order: undefined,\n        parent: undefined,\n        parents: undefined,\n        path: '/old-path',\n        show: true,\n        children: [],\n      },\n      {\n        badge: undefined,\n        badgeType: undefined,\n        badgeVariants: undefined,\n        icon: 'path-icon',\n        name: 'New Path',\n        order: undefined,\n        parent: undefined,\n        parents: undefined,\n        path: '/new-path',\n        show: true,\n        children: [],\n      },\n    ]);\n  });\n\n  const routes: any = [\n    {\n      meta: { order: 2, title: 'Home' },\n      name: 'home',\n      path: '/',\n    },\n    {\n      meta: { order: 1, title: 'About' },\n      name: 'about',\n      path: '/about',\n    },\n  ];\n\n  const router: Router = createRouter({\n    history: createWebHistory(),\n    routes,\n  });\n\n  it('should generate menu list with correct order', async () => {\n    const menus = generateMenus(routes, router);\n    const expectedMenus = [\n      {\n        badge: undefined,\n        badgeType: undefined,\n        badgeVariants: undefined,\n        icon: undefined,\n        name: 'About',\n        order: 1,\n        parent: undefined,\n        parents: undefined,\n        path: '/about',\n        show: true,\n        children: [],\n      },\n      {\n        badge: undefined,\n        badgeType: undefined,\n        badgeVariants: undefined,\n        icon: undefined,\n        name: 'Home',\n        order: 2,\n        parent: undefined,\n        parents: undefined,\n        path: '/',\n        show: true,\n        children: [],\n      },\n    ];\n\n    expect(menus).toEqual(expectedMenus);\n  });\n\n  it('should handle empty routes', async () => {\n    const emptyRoutes: any[] = [];\n    const menus = generateMenus(emptyRoutes, router);\n    expect(menus).toEqual([]);\n  });\n});\n"
  },
  {
    "path": "packages/utils/src/helpers/__tests__/generate-routes-frontend.test.ts",
    "content": "import type { RouteRecordRaw } from 'vue-router';\n\nimport { describe, expect, it } from 'vitest';\n\nimport {\n  generateRoutesByFrontend,\n  hasAuthority,\n} from '../generate-routes-frontend';\n\n// Mock 路由数据\nconst mockRoutes = [\n  {\n    meta: {\n      authority: ['admin', 'user'],\n      hideInMenu: false,\n    },\n    path: '/dashboard',\n    children: [\n      {\n        path: '/dashboard/overview',\n        meta: { authority: ['admin'], hideInMenu: false },\n      },\n      {\n        path: '/dashboard/stats',\n        meta: { authority: ['user'], hideInMenu: true },\n      },\n    ],\n  },\n  {\n    meta: { authority: ['admin'], hideInMenu: false },\n    path: '/settings',\n  },\n  {\n    meta: { hideInMenu: false },\n    path: '/profile',\n  },\n] as RouteRecordRaw[];\n\ndescribe('hasAuthority', () => {\n  it('should return true if there is no authority defined', () => {\n    expect(hasAuthority(mockRoutes[2], ['admin'])).toBe(true);\n  });\n\n  it('should return true if the user has the required authority', () => {\n    expect(hasAuthority(mockRoutes[0], ['admin'])).toBe(true);\n  });\n\n  it('should return false if the user does not have the required authority', () => {\n    expect(hasAuthority(mockRoutes[1], ['user'])).toBe(false);\n  });\n});\n\ndescribe('generateRoutesByFrontend', () => {\n  it('should handle routes without children', async () => {\n    const generatedRoutes = await generateRoutesByFrontend(mockRoutes, [\n      'user',\n    ]);\n    expect(generatedRoutes).toEqual(\n      expect.arrayContaining([\n        expect.objectContaining({\n          path: '/profile', // This route has no children and should be included\n        }),\n      ]),\n    );\n  });\n\n  it('should handle empty roles array', async () => {\n    const generatedRoutes = await generateRoutesByFrontend(mockRoutes, []);\n    expect(generatedRoutes).toEqual(\n      expect.arrayContaining([\n        // Only routes without authority should be included\n        expect.objectContaining({\n          path: '/profile',\n        }),\n      ]),\n    );\n    expect(generatedRoutes).not.toEqual(\n      expect.arrayContaining([\n        expect.objectContaining({\n          path: '/dashboard',\n        }),\n        expect.objectContaining({\n          path: '/settings',\n        }),\n      ]),\n    );\n  });\n\n  it('should handle missing meta fields', async () => {\n    const routesWithMissingMeta = [\n      { path: '/path1' }, // No meta\n      { meta: {}, path: '/path2' }, // Empty meta\n      { meta: { authority: ['admin'] }, path: '/path3' }, // Only authority\n    ];\n    const generatedRoutes = await generateRoutesByFrontend(\n      routesWithMissingMeta as RouteRecordRaw[],\n      ['admin'],\n    );\n    expect(generatedRoutes).toEqual([\n      { path: '/path1' },\n      { meta: {}, path: '/path2' },\n      { meta: { authority: ['admin'] }, path: '/path3' },\n    ]);\n  });\n});\n"
  },
  {
    "path": "packages/utils/src/helpers/__tests__/merge-route-modules.test.ts",
    "content": "import type { RouteRecordRaw } from 'vue-router';\n\nimport type { RouteModuleType } from '../merge-route-modules';\n\nimport { describe, expect, it } from 'vitest';\n\nimport { mergeRouteModules } from '../merge-route-modules';\n\ndescribe('mergeRouteModules', () => {\n  it('should merge route modules correctly', () => {\n    const routeModules: Record<string, RouteModuleType> = {\n      './dynamic-routes/about.ts': {\n        default: [\n          {\n            component: () => Promise.resolve({ template: '<div>About</div>' }),\n            name: 'About',\n            path: '/about',\n          },\n        ],\n      },\n      './dynamic-routes/home.ts': {\n        default: [\n          {\n            component: () => Promise.resolve({ template: '<div>Home</div>' }),\n            name: 'Home',\n            path: '/',\n          },\n        ],\n      },\n    };\n\n    const expectedRoutes: RouteRecordRaw[] = [\n      {\n        component: expect.any(Function),\n        name: 'About',\n        path: '/about',\n      },\n      {\n        component: expect.any(Function),\n        name: 'Home',\n        path: '/',\n      },\n    ];\n\n    const mergedRoutes = mergeRouteModules(routeModules);\n    expect(mergedRoutes).toEqual(expectedRoutes);\n  });\n\n  it('should handle empty modules', () => {\n    const routeModules: Record<string, RouteModuleType> = {};\n    const expectedRoutes: RouteRecordRaw[] = [];\n\n    const mergedRoutes = mergeRouteModules(routeModules);\n    expect(mergedRoutes).toEqual(expectedRoutes);\n  });\n\n  it('should handle modules with no default export', () => {\n    const routeModules: Record<string, RouteModuleType> = {\n      './dynamic-routes/empty.ts': {\n        default: [],\n      },\n    };\n    const expectedRoutes: RouteRecordRaw[] = [];\n\n    const mergedRoutes = mergeRouteModules(routeModules);\n    expect(mergedRoutes).toEqual(expectedRoutes);\n  });\n});\n"
  },
  {
    "path": "packages/utils/src/helpers/enum-options.ts",
    "content": "/**\n * @author dap\n * @description 枚举选项\n */\n\n/**\n * 定义options类型\n */\nexport interface EnumsOption {\n  /**\n   * 枚举名称 建议使用全大写字母_\n   */\n  enumName: string;\n  /**\n   * option的标签\n   */\n  label: string;\n  /**\n   * option的值\n   */\n  value: boolean | number | string;\n}\n\nexport type EnumResult<T extends readonly EnumsOption[]> = {\n  [key in T[number]['enumName']]: Extract<\n    T[number],\n    { enumName: key }\n  >['value'];\n};\n\n/**\n * 将options转为枚举\n * 注意自定义的options需要加上as const作为常量处理\n * 详见: packages\\utils\\src\\helpers\\__tests__\\enum-options.test.ts\n * @param options 枚举选项\n * @returns 转枚举\n */\nexport function optionsToEnum<T extends readonly EnumsOption[]>(\n  options: T,\n): EnumResult<T> {\n  type K = T[number]['enumName'];\n  const result = {} as EnumResult<T>;\n  options.forEach((item) => {\n    result[item.enumName as K] = item.value;\n  });\n  return result;\n}\n"
  },
  {
    "path": "packages/utils/src/helpers/find-menu-by-path.ts",
    "content": "import type { MenuRecordRaw } from '@vben-core/typings';\n\nfunction findMenuByPath(\n  list: MenuRecordRaw[],\n  path?: string,\n): MenuRecordRaw | null {\n  for (const menu of list) {\n    if (menu.path === path) {\n      return menu;\n    }\n    const findMenu = menu.children && findMenuByPath(menu.children, path);\n    if (findMenu) {\n      return findMenu;\n    }\n  }\n  return null;\n}\n\n/**\n * 查找根菜单\n * @param menus\n * @param path\n */\nfunction findRootMenuByPath(menus: MenuRecordRaw[], path?: string, level = 0) {\n  const findMenu = findMenuByPath(menus, path);\n  const rootMenuPath = findMenu?.parents?.[level];\n  const rootMenu = rootMenuPath\n    ? menus.find((item) => item.path === rootMenuPath)\n    : undefined;\n  return {\n    findMenu,\n    rootMenu,\n    rootMenuPath,\n  };\n}\n\nexport { findMenuByPath, findRootMenuByPath };\n"
  },
  {
    "path": "packages/utils/src/helpers/generate-menus.ts",
    "content": "import type { Router, RouteRecordRaw } from 'vue-router';\n\nimport type {\n  ExRouteRecordRaw,\n  MenuRecordRaw,\n  RouteMeta,\n} from '@vben-core/typings';\n\nimport { filterTree, mapTree } from '@vben-core/shared/utils';\n\n/**\n * 根据 routes 生成菜单列表\n * @param routes - 路由配置列表\n * @param router - Vue Router 实例\n * @returns 生成的菜单列表\n */\nfunction generateMenus(\n  routes: RouteRecordRaw[],\n  router: Router,\n): MenuRecordRaw[] {\n  // 将路由列表转换为一个以 name 为键的对象映射\n  const finalRoutesMap: { [key: string]: string } = Object.fromEntries(\n    router.getRoutes().map(({ name, path }) => [name, path]),\n  );\n\n  let menus = mapTree<ExRouteRecordRaw, MenuRecordRaw>(routes, (route) => {\n    // 获取最终的路由路径\n    const path = finalRoutesMap[route.name as string] ?? route.path ?? '';\n\n    const {\n      meta = {} as RouteMeta,\n      name: routeName,\n      redirect,\n      children = [],\n    } = route;\n    const {\n      activeIcon,\n      badge,\n      badgeType,\n      badgeVariants,\n      hideChildrenInMenu = false,\n      icon,\n      link,\n      order,\n      title = '',\n    } = meta;\n\n    // 确保菜单名称不为空\n    const name = (title || routeName || '') as string;\n\n    // 处理子菜单\n    const resultChildren = hideChildrenInMenu\n      ? []\n      : ((children as MenuRecordRaw[]) ?? []);\n\n    // 设置子菜单的父子关系\n    if (resultChildren.length > 0) {\n      resultChildren.forEach((child) => {\n        child.parents = [...(route.parents ?? []), path];\n        child.parent = path;\n      });\n    }\n\n    // 确定最终路径\n    const resultPath = hideChildrenInMenu ? redirect || path : link || path;\n\n    return {\n      activeIcon,\n      badge,\n      badgeType,\n      badgeVariants,\n      icon,\n      name,\n      order,\n      parent: route.parent,\n      parents: route.parents,\n      path: resultPath,\n      show: !meta.hideInMenu,\n      children: resultChildren,\n    };\n  });\n\n  // 对菜单进行排序，避免order=0时被替换成999的问题\n  menus = menus.sort((a, b) => (a?.order ?? 999) - (b?.order ?? 999));\n\n  // 过滤掉隐藏的菜单项\n  return filterTree(menus, (menu) => !!menu.show);\n}\n\nexport { generateMenus };\n"
  },
  {
    "path": "packages/utils/src/helpers/generate-routes-backend.ts",
    "content": "import type { RouteRecordRaw } from 'vue-router';\n\nimport type {\n  ComponentRecordType,\n  GenerateMenuAndRoutesOptions,\n  RouteRecordStringComponent,\n} from '@vben-core/typings';\n\nimport { mapTree } from '@vben-core/shared/utils';\n\n/**\n * 动态生成路由 - 后端方式\n */\nasync function generateRoutesByBackend(\n  options: GenerateMenuAndRoutesOptions,\n): Promise<RouteRecordRaw[]> {\n  const { fetchMenuListAsync, layoutMap = {}, pageMap = {} } = options;\n\n  try {\n    const menuRoutes = await fetchMenuListAsync?.();\n    if (!menuRoutes) {\n      return [];\n    }\n\n    const normalizePageMap: ComponentRecordType = {};\n\n    for (const [key, value] of Object.entries(pageMap)) {\n      normalizePageMap[normalizeViewPath(key)] = value;\n    }\n\n    const routes = convertRoutes(menuRoutes, layoutMap, normalizePageMap);\n\n    return routes;\n  } catch (error) {\n    console.error(error);\n    throw error;\n  }\n}\n\nfunction convertRoutes(\n  routes: RouteRecordStringComponent[],\n  layoutMap: ComponentRecordType,\n  pageMap: ComponentRecordType,\n): RouteRecordRaw[] {\n  return mapTree(routes, (node) => {\n    const route = node as unknown as RouteRecordRaw;\n    const { component, name } = node;\n\n    if (!name) {\n      console.error('route name is required', route);\n    }\n\n    // layout转换\n    if (component && layoutMap[component]) {\n      route.component = layoutMap[component];\n      // 页面组件转换\n    } else if (component) {\n      const normalizePath = normalizeViewPath(component);\n      const pageKey = normalizePath.endsWith('.vue')\n        ? normalizePath\n        : `${normalizePath}.vue`;\n      if (pageMap[pageKey]) {\n        route.component = pageMap[pageKey];\n      } else {\n        // console.error(`route component is invalid: ${pageKey}`, route);\n        // route.component = pageMap['/_core/fallback/not-found.vue'];\n        console.error(`未找到对应组件: /views${component}.vue`);\n        // 默认为404页面\n        route.component = layoutMap.NotFoundComponent;\n      }\n    }\n\n    return route;\n  });\n}\n\nfunction normalizeViewPath(path: string): string {\n  // 去除相对路径前缀\n  const normalizedPath = path.replace(/^(\\.\\/|\\.\\.\\/)+/, '');\n\n  // 确保路径以 '/' 开头\n  const viewPath = normalizedPath.startsWith('/')\n    ? normalizedPath\n    : `/${normalizedPath}`;\n\n  // 这里耦合了vben-admin的目录结构\n  return viewPath.replace(/^\\/views/, '');\n}\nexport { generateRoutesByBackend };\n"
  },
  {
    "path": "packages/utils/src/helpers/generate-routes-frontend.ts",
    "content": "import type { RouteRecordRaw } from 'vue-router';\n\nimport { filterTree, mapTree } from '@vben-core/shared/utils';\n\n/**\n * 动态生成路由 - 前端方式\n */\nasync function generateRoutesByFrontend(\n  routes: RouteRecordRaw[],\n  roles: string[],\n  forbiddenComponent?: RouteRecordRaw['component'],\n): Promise<RouteRecordRaw[]> {\n  // 根据角色标识过滤路由表,判断当前用户是否拥有指定权限\n  const finalRoutes = filterTree(routes, (route) => {\n    return hasAuthority(route, roles);\n  });\n\n  if (!forbiddenComponent) {\n    return finalRoutes;\n  }\n\n  // 如果有禁止访问的页面，将禁止访问的页面替换为403页面\n  return mapTree(finalRoutes, (route) => {\n    if (menuHasVisibleWithForbidden(route)) {\n      route.component = forbiddenComponent;\n    }\n    return route;\n  });\n}\n\n/**\n * 判断路由是否有权限访问\n * @param route\n * @param access\n */\nfunction hasAuthority(route: RouteRecordRaw, access: string[]) {\n  const authority = route.meta?.authority;\n  if (!authority) {\n    return true;\n  }\n  const canAccess = access.some((value) => authority.includes(value));\n\n  return canAccess || (!canAccess && menuHasVisibleWithForbidden(route));\n}\n\n/**\n * 判断路由是否在菜单中显示，但是访问会被重定向到403\n * @param route\n */\nfunction menuHasVisibleWithForbidden(route: RouteRecordRaw) {\n  return (\n    !!route.meta?.authority &&\n    Reflect.has(route.meta || {}, 'menuVisibleWithForbidden') &&\n    !!route.meta?.menuVisibleWithForbidden\n  );\n}\n\nexport { generateRoutesByFrontend, hasAuthority };\n"
  },
  {
    "path": "packages/utils/src/helpers/get-popup-container.ts",
    "content": "/**\n * If the node is holding inside a form, return the form element,\n * otherwise return the parent node of the given element or\n * the document body if the element is not provided.\n */\nexport function getPopupContainer(node?: HTMLElement): HTMLElement {\n  return (\n    node?.closest('form') ?? (node?.parentNode as HTMLElement) ?? document.body\n  );\n}\n\n/**\n * VxeTable专用弹窗层\n * 解决问题: https://gitee.com/dapppp/ruoyi-plus-vben5/issues/IB1DM3\n * @param node 触发的元素\n * @param tableId 表格ID，用于区分不同表格（可选）\n * @returns 挂载节点\n */\nexport function getVxePopupContainer(\n  node?: HTMLElement,\n  tableId?: string,\n): HTMLElement {\n  if (!node) return document.body;\n\n  // 检查是否在固定列内\n  const isInFixedColumn =\n    node.closest('.vxe-table--fixed-wrapper') ||\n    node.closest('.vxe-table--fixed-left-wrapper') ||\n    node.closest('.vxe-table--fixed-right-wrapper');\n\n  // 如果在固定列内，则挂载到固定列容器\n  if (isInFixedColumn) {\n    // 优先查找表格容器及父级容器\n    const tableContainer =\n      // 查找通用固定列容器\n      node.closest('.vxe-table--fixed-wrapper') ||\n      // 查找固定列容器（左侧固定列）\n      node.closest('.vxe-table--fixed-left-wrapper') ||\n      // 查找固定列容器（右侧固定列）\n      node.closest('.vxe-table--fixed-right-wrapper');\n\n    // 如果指定了tableId，可以查找特定ID的表格\n    if (tableId && tableContainer) {\n      const specificTable = tableContainer.closest(\n        `[data-table-id=\"${tableId}\"]`,\n      );\n      if (specificTable) {\n        return specificTable as HTMLElement;\n      }\n    }\n\n    return tableContainer as HTMLElement;\n  }\n\n  /**\n   * 设置行高度需要特殊处理\n   */\n  const fixedHeightElement = node.closest('td.col--cs-height');\n  if (fixedHeightElement) {\n    // 默认为hidden 显示异常\n    (fixedHeightElement as HTMLTableCellElement).style.overflow = 'visible';\n  }\n\n  // 兜底方案：使用元素的父节点或文档体\n  return (node.parentNode as HTMLElement) || document.body;\n}\n"
  },
  {
    "path": "packages/utils/src/helpers/index.ts",
    "content": "export * from './enum-options';\nexport * from './find-menu-by-path';\nexport * from './generate-menus';\nexport * from './generate-routes-backend';\nexport * from './generate-routes-frontend';\nexport * from './get-popup-container';\nexport * from './merge-route-modules';\nexport * from './mitt';\nexport * from './request';\nexport * from './reset-routes';\nexport * from './safe';\nexport * from './tree';\nexport * from './unmount-global-loading';\nexport * from './uuid';\n"
  },
  {
    "path": "packages/utils/src/helpers/merge-route-modules.ts",
    "content": "import type { RouteRecordRaw } from 'vue-router';\n\n// 定义模块类型\ninterface RouteModuleType {\n  default: RouteRecordRaw[];\n}\n\n/**\n * 合并动态路由模块的默认导出\n * @param routeModules 动态导入的路由模块对象\n * @returns 合并后的路由配置数组\n */\nfunction mergeRouteModules(\n  routeModules: Record<string, unknown>,\n): RouteRecordRaw[] {\n  const mergedRoutes: RouteRecordRaw[] = [];\n\n  for (const routeModule of Object.values(routeModules)) {\n    const moduleRoutes = (routeModule as RouteModuleType)?.default ?? [];\n    mergedRoutes.push(...moduleRoutes);\n  }\n\n  return mergedRoutes;\n}\n\nexport { mergeRouteModules };\n\nexport type { RouteModuleType };\n"
  },
  {
    "path": "packages/utils/src/helpers/mitt.ts",
    "content": "/**\n * copy to https://github.com/developit/mitt\n * Expand clear method\n */\nexport type EventType = string | symbol;\n\n// An event handler can take an optional event argument\n// and should not return a value\nexport type Handler<T = unknown> = (event: T) => void;\nexport type WildcardHandler<T = Record<string, unknown>> = (\n  type: keyof T,\n  event: T[keyof T],\n) => void;\n\n// An array of all currently registered event handlers for a type\nexport type EventHandlerList<T = unknown> = Array<Handler<T>>;\nexport type WildCardEventHandlerList<T = Record<string, unknown>> = Array<\n  WildcardHandler<T>\n>;\n\n// A map of event types and their corresponding event handlers.\nexport type EventHandlerMap<Events extends Record<EventType, unknown>> = Map<\n  '*' | keyof Events,\n  EventHandlerList<Events[keyof Events]> | WildCardEventHandlerList<Events>\n>;\n\nexport interface Emitter<Events extends Record<EventType, unknown>> {\n  all: EventHandlerMap<Events>;\n\n  clear(): void;\n  emit<Key extends keyof Events>(\n    type: undefined extends Events[Key] ? Key : never,\n  ): void;\n\n  emit<Key extends keyof Events>(type: Key, event: Events[Key]): void;\n  off(type: '*', handler: WildcardHandler<Events>): void;\n\n  off<Key extends keyof Events>(\n    type: Key,\n    handler?: Handler<Events[Key]>,\n  ): void;\n  on(type: '*', handler: WildcardHandler<Events>): void;\n  on<Key extends keyof Events>(type: Key, handler: Handler<Events[Key]>): void;\n}\n\n/**\n * Mitt: Tiny (~200b) functional event emitter / pubsub.\n * @name mitt\n * @returns {Mitt} any\n */\nexport function mitt<Events extends Record<EventType, unknown>>(\n  all?: EventHandlerMap<Events>,\n): Emitter<Events> {\n  type GenericEventHandler =\n    | Handler<Events[keyof Events]>\n    | WildcardHandler<Events>;\n  all = all || new Map();\n\n  return {\n    /**\n     * A Map of event names to registered handler functions.\n     */\n    all,\n\n    /**\n     * Clear all\n     */\n    clear() {\n      this.all.clear();\n    },\n\n    /**\n     * Invoke all handlers for the given type.\n     * If present, `'*'` handlers are invoked after type-matched handlers.\n     *\n     * Note: Manually firing '*' handlers is not supported.\n     *\n     * @param {string|symbol} type The event type to invoke\n     * @param {Any} [evt] Any value (object is recommended and powerful), passed to each handler\n     * @memberOf mitt\n     */\n    emit<Key extends keyof Events>(type: Key, evt?: Events[Key]) {\n      let handlers = all?.get(type);\n      if (handlers) {\n        [...(handlers as EventHandlerList<Events[keyof Events]>)].forEach(\n          (handler) => {\n            handler(evt as Events[Key]);\n          },\n        );\n      }\n\n      handlers = all?.get('*');\n      if (handlers) {\n        [...(handlers as WildCardEventHandlerList<Events>)].forEach(\n          (handler) => {\n            handler(type, evt as Events[Key]);\n          },\n        );\n      }\n    },\n\n    /**\n     * Remove an event handler for the given type.\n     * If `handler` is omitted, all handlers of the given type are removed.\n     * @param {string|symbol} type Type of event to unregister `handler` from (`'*'` to remove a wildcard handler)\n     * @param {Function} [handler] Handler function to remove\n     * @memberOf mitt\n     */\n    off<Key extends keyof Events>(type: Key, handler?: GenericEventHandler) {\n      const handlers: Array<GenericEventHandler> | undefined = all?.get(type);\n      if (handlers) {\n        if (handler) {\n          handlers.splice(handlers.indexOf(handler) >>> 0, 1);\n        } else {\n          all?.set(type, []);\n        }\n      }\n    },\n\n    /**\n     * Register an event handler for the given type.\n     * @param {string|symbol} type Type of event to listen for, or `'*'` for all events\n     * @param {Function} handler Function to call in response to given event\n     * @memberOf mitt\n     */\n    on<Key extends keyof Events>(type: Key, handler: GenericEventHandler) {\n      const handlers: Array<GenericEventHandler> | undefined = all?.get(type);\n      if (handlers) {\n        handlers.push(handler);\n      } else {\n        all?.set(type, [handler] as EventHandlerList<Events[keyof Events]>);\n      }\n    },\n  };\n}\n"
  },
  {
    "path": "packages/utils/src/helpers/request.ts",
    "content": "/**\n * 一些发送请求 需要用到的工具\n */\n\n/**\n * Add the object as a parameter to the URL\n * @param baseUrl url\n * @param obj\n * @returns {string}\n * eg:\n *  let obj = {a: '3', b: '4'}\n *  setObjToUrlParams('www.baidu.com', obj)\n *  ==>www.baidu.com?a=3&b=4\n */\nexport function setObjToUrlParams(baseUrl: string, obj: any): string {\n  let parameters = '';\n  for (const key in obj) {\n    parameters += `${key}=${encodeURIComponent(obj[key])}&`;\n  }\n  parameters = parameters.replace(/&$/, '');\n  return /\\?$/.test(baseUrl)\n    ? baseUrl + parameters\n    : baseUrl.replace(/\\/?$/, '?') + parameters;\n}\n"
  },
  {
    "path": "packages/utils/src/helpers/reset-routes.ts",
    "content": "import type { Router, RouteRecordName, RouteRecordRaw } from 'vue-router';\n\nimport { traverseTreeValues } from '@vben-core/shared/utils';\n\n/**\n * @zh_CN 重置所有路由，如有指定白名单除外\n */\nexport function resetStaticRoutes(router: Router, routes: RouteRecordRaw[]) {\n  // 获取静态路由所有节点包含子节点的 name，并排除不存在 name 字段的路由\n  const staticRouteNames = traverseTreeValues<\n    RouteRecordRaw,\n    RouteRecordName | undefined\n  >(routes, (route) => {\n    // 这些路由需要指定 name，防止在路由重置时，不能删除没有指定 name 的路由\n    if (!route.name) {\n      console.warn(\n        `The route with the path ${route.path} needs to have the field name specified.`,\n      );\n    }\n    return route.name;\n  });\n\n  const { getRoutes, hasRoute, removeRoute } = router;\n  const allRoutes = getRoutes();\n  allRoutes.forEach(({ name }) => {\n    // 存在于路由表且非白名单才需要删除\n    if (name && !staticRouteNames.includes(name) && hasRoute(name)) {\n      removeRoute(name);\n    }\n  });\n}\n"
  },
  {
    "path": "packages/utils/src/helpers/safe.ts",
    "content": "/**\n * 跟后台逻辑一致\n * Number.isSafeInteger形参只能为Number类型 其他的直接返回false\n * @param str 数字\n * @returns 安全数内返回number类型 否则返回原字符串\n */\nexport function safeParseNumber(str: string): number | string {\n  const num = Number(str);\n  return Number.isSafeInteger(num) ? num : str;\n}\n"
  },
  {
    "path": "packages/utils/src/helpers/tree.ts",
    "content": "/* eslint-disable @typescript-eslint/no-non-null-assertion */\ninterface TreeHelperConfig {\n  children: string;\n  id: string;\n  pid: string;\n}\n\ntype Fn = (node: any, parentNode?: any) => any;\n\n// 默认配置\nconst DEFAULT_CONFIG: TreeHelperConfig = {\n  id: 'id',\n  pid: 'parentId',\n  children: 'children',\n};\n\n// 获取配置。  Object.assign 从一个或多个源对象复制到目标对象\nconst getConfig = (config: Partial<TreeHelperConfig>) =>\n  Object.assign({}, DEFAULT_CONFIG, config);\n\n// tree from list\n// 列表中的树\nexport function listToTree<T = any>(\n  list: any[],\n  config: Partial<TreeHelperConfig> = {},\n): T[] {\n  const conf = getConfig(config) as TreeHelperConfig;\n  const nodeMap = new Map();\n  const result: T[] = [];\n  const { id, pid, children } = conf;\n\n  for (const node of list) {\n    node[children] = node[children] || [];\n    nodeMap.set(node[id], node);\n  }\n  for (const node of list) {\n    const parent = nodeMap.get(node[pid]);\n    (parent ? parent[children] : result).push(node);\n  }\n  return result;\n}\n\nexport function treeToList<T = any>(\n  tree: any,\n  config: Partial<TreeHelperConfig> = {},\n): T {\n  config = getConfig(config);\n  const { children } = config;\n  const result: any = [...tree];\n  for (let i = 0; i < result.length; i++) {\n    if (!result[i][children!]) continue;\n    result.splice(i + 1, 0, ...result[i][children!]);\n  }\n  return result;\n}\n\nexport function findNode<T = any>(\n  tree: any,\n  func: Fn,\n  config: Partial<TreeHelperConfig> = {},\n): null | T {\n  config = getConfig(config);\n  const { children } = config;\n  const list = [...tree];\n  for (const node of list) {\n    if (func(node)) return node;\n    node[children!] && list.push(...node[children!]);\n  }\n  return null;\n}\n\nexport function findNodeAll<T = any>(\n  tree: any,\n  func: Fn,\n  config: Partial<TreeHelperConfig> = {},\n): T[] {\n  config = getConfig(config);\n  const { children } = config;\n  const list = [...tree];\n  const result: T[] = [];\n  for (const node of list) {\n    func(node) && result.push(node);\n    node[children!] && list.push(...node[children!]);\n  }\n  return result;\n}\n\nexport function findPath<T = any>(\n  tree: any,\n  func: Fn,\n  config: Partial<TreeHelperConfig> = {},\n): null | T | T[] {\n  config = getConfig(config);\n  const path: T[] = [];\n  const list = [...tree];\n  const visitedSet = new Set();\n  const { children } = config;\n  while (list.length > 0) {\n    const node = list[0];\n    if (visitedSet.has(node)) {\n      path.pop();\n      list.shift();\n    } else {\n      visitedSet.add(node);\n      node[children!] && list.unshift(...node[children!]);\n      path.push(node);\n      if (func(node)) {\n        return path;\n      }\n    }\n  }\n  return null;\n}\n\nexport function findPathAll(\n  tree: any,\n  func: Fn,\n  config: Partial<TreeHelperConfig> = {},\n) {\n  config = getConfig(config);\n  const path: any[] = [];\n  const list = [...tree];\n  const result: any[] = [];\n  const { children } = config;\n  const visitedSet = new Set();\n  while (list.length > 0) {\n    const node = list[0];\n    if (visitedSet.has(node)) {\n      path.pop();\n      list.shift();\n    } else {\n      visitedSet.add(node);\n      node[children!] && list.unshift(...node[children!]);\n      path.push(node);\n      func(node) && result.push([...path]);\n    }\n  }\n  return result;\n}\n\nexport function filter<T = any>(\n  tree: T[],\n  func: (n: T) => boolean,\n  // Partial 将 T 中的所有属性设为可选\n  config: Partial<TreeHelperConfig> = {},\n): T[] {\n  // 获取配置\n  config = getConfig(config);\n  const children = config.children as string;\n\n  function listFilter(list: T[]) {\n    return list\n      .map((node: any) => ({ ...node }))\n      .filter((node) => {\n        // 递归调用 对含有children项  进行再次调用自身函数 listFilter\n        node[children] = node[children] && listFilter(node[children]);\n        // 执行传入的回调 func 进行过滤\n        return func(node) || (node[children] && node[children].length > 0);\n      });\n  }\n\n  return listFilter(tree);\n}\n\nexport function forEach<T = any>(\n  tree: T[],\n  func: (n: T) => any,\n  config: Partial<TreeHelperConfig> = {},\n): void {\n  config = getConfig(config);\n  const list: any[] = [...tree];\n  const { children } = config;\n  for (let i = 0; i < list.length; i++) {\n    // func 返回true就终止遍历，避免大量节点场景下无意义循环，引起浏览器卡顿\n    if (func(list[i])) {\n      return;\n    }\n    children &&\n      list[i][children] &&\n      list.splice(i + 1, 0, ...list[i][children]);\n  }\n}\n\n/**\n * @description: Extract tree specified structure\n * @description: 提取树指定结构\n */\nexport function treeMap<T = any>(\n  treeData: T[],\n  opt: { children?: string; conversion: Fn },\n): T[] {\n  return treeData.map((item) => treeMapEach(item, opt));\n}\n\n/**\n * @description: Extract tree specified structure\n * @description: 提取树指定结构\n */\nexport function treeMapEach(\n  data: any,\n  { conversion, children = 'children' }: { children?: string; conversion: Fn },\n) {\n  const haveChildren =\n    Array.isArray(data[children]) && data[children].length > 0;\n  const conversionData = conversion(data) || {};\n  return haveChildren\n    ? {\n        ...conversionData,\n        [children]: data[children].map((i: number) =>\n          treeMapEach(i, {\n            children,\n            conversion,\n          }),\n        ),\n      }\n    : {\n        ...conversionData,\n      };\n}\n\n/**\n * 递归遍历树结构\n * @param treeDatas 树\n * @param callBack 回调\n * @param parentNode 父节点\n */\nexport function eachTree(treeDatas: any[], callBack: Fn, parentNode = {}) {\n  treeDatas.forEach((element) => {\n    const newNode = callBack(element, parentNode) || element;\n    if (element.children) {\n      eachTree(element.children, callBack, newNode);\n    }\n  });\n}\n\n// 如果节点的children为空, 则删除children属性\nexport function removeEmptyChildren(data: any[], childrenField = 'children') {\n  data.forEach((item) => {\n    if (!item[childrenField]) {\n      return;\n    }\n    if (item[childrenField].length > 0) {\n      removeEmptyChildren(item[childrenField]);\n    } else {\n      Reflect.deleteProperty(item, childrenField);\n    }\n  });\n}\n\n// eslint-disable-next-line jsdoc/require-returns-check\n/**\n *\n * 添加全名 如 祖先节点-父节点-子节点\n * @param treeData 已经是tree数据\n * @param labelName 标签的字段名称\n * @param splitStr 分隔符\n * @returns void 无返回值 会修改原始数据\n */\nexport function addFullName(\n  treeData: any[],\n  labelName = 'label',\n  splitStr = '-',\n) {\n  function addFullNameProperty(node: any, parentNames: any[] = []) {\n    const fullNameParts = [...parentNames, node[labelName]];\n    node.fullName = fullNameParts.join(splitStr);\n    if (node.children && node.children.length > 0) {\n      node.children.forEach((childNode: any) => {\n        addFullNameProperty(childNode, fullNameParts);\n      });\n    }\n  }\n\n  treeData.forEach((item: any) => {\n    addFullNameProperty(item);\n  });\n}\n\n/**\n * https://blog.csdn.net/Web_J/article/details/129281329\n * 给出节点nodeId 找到所有父节点ID\n * @param treeList 树形结构list\n * @param nodeId 要寻找的节点ID\n * @param config config\n * @returns 父节点ID数组\n */\nexport function findParentsIds(\n  treeList: any[],\n  nodeId: number,\n  config: Partial<TreeHelperConfig> = {},\n) {\n  const conf = getConfig(config) as TreeHelperConfig;\n  const { id, children } = conf;\n\n  // 用于存储所有父节点ID的数组\n  const parentIds: number[] = [];\n\n  function traverse(node: any, nodeId: number) {\n    if (node[id] === nodeId) {\n      return true;\n    }\n    if (node[children]) {\n      // 如果当前节点有子节点，则继续遍历子节点\n      for (const childNode of node[children]) {\n        if (traverse(childNode, nodeId)) {\n          // 如果在子节点中找到了子节点的父节点，则将当前节点的ID添加到父节点ID数组中，并返回true表示已经找到了子节点\n          parentIds.push(node[id]);\n          return true;\n        }\n      }\n    }\n    return false;\n  }\n\n  for (const node of treeList) {\n    if (traverse(node, nodeId)) {\n      // 如果在当前节点的子树中找到了子节点的父节点，则直接退出循环\n      break;\n    }\n  }\n\n  return parentIds.sort();\n}\n\n/**\n * 给出节点数组 找到所有父节点ID\n * @param treeList 树形结构list\n * @param nodeIds 要寻找的节点ID list\n * @param config config\n * @returns 父节点ID数组\n */\nexport function findGroupParentIds(\n  treeList: any[],\n  nodeIds: number[],\n  config: Partial<TreeHelperConfig> = {},\n) {\n  // 用于存储所有父节点ID的Set 主要为了去重\n  const parentIds = new Set<number>();\n\n  nodeIds.forEach((nodeId) => {\n    findParentsIds(treeList, nodeId, config).forEach((parentId) => {\n      parentIds.add(parentId);\n    });\n  });\n\n  return [...parentIds].sort();\n}\n\n/**\n * 找到所有ID 返回数组\n * @param treeList list\n * @param config\n * @returns ID数组\n */\nexport function findAllIds(\n  treeList: any[],\n  config: Partial<TreeHelperConfig> = DEFAULT_CONFIG,\n) {\n  const conf = getConfig(config) as TreeHelperConfig;\n  const { id, children } = conf;\n  const ids: number[] = [];\n\n  treeList.forEach((item) => {\n    if (item[children]) {\n      const tempIds = findAllIds(item[children], config);\n      ids.push(...tempIds);\n    }\n    ids.push(item[id]);\n  });\n\n  return [...ids].sort();\n}\n\n/**\n * @description 这里抄的filterByLevel函数\n * @description 主要用于获取指定层级的节点数组\n */\nexport function findIdsByLevel(\n  level = 1,\n  list?: any[],\n  config: Partial<TreeHelperConfig> = DEFAULT_CONFIG,\n  currentLevel = 1,\n) {\n  if (!level) {\n    return [];\n  }\n  const res: (number | string)[] = [];\n  const data = list || [];\n  for (const item of data) {\n    const { id: keyField, children: childrenField } = config;\n    const key = keyField ? item[keyField] : '';\n    const children = childrenField ? item[childrenField] : [];\n    res.push(key);\n    if (children && children.length > 0 && currentLevel < level) {\n      currentLevel += 1;\n      res.push(...findIdsByLevel(level, children, config, currentLevel));\n    }\n  }\n  return res as number[] | string[];\n}\n"
  },
  {
    "path": "packages/utils/src/helpers/unmount-global-loading.ts",
    "content": "/**\n * 移除并销毁loading\n * 放在这里是而不是放在 index.html 的app标签内，是因为这样比较不会生硬，渲染过快可能会有闪烁\n * 通过先添加css动画隐藏，在动画结束后在移除loading节点来改善体验\n * 不好的地方是会增加一些代码量\n * 自定义loading可以见：https://doc.vben.pro/guide/in-depth/loading.html\n */\nexport function unmountGlobalLoading() {\n  // 查找全局 loading 元素\n  const loadingElement = document.querySelector('#__app-loading__');\n\n  if (loadingElement) {\n    // 添加隐藏类，触发过渡动画\n    loadingElement.classList.add('hidden');\n\n    // 查找所有需要移除的注入 loading 元素\n    const injectLoadingElements = document.querySelectorAll(\n      '[data-app-loading^=\"inject\"]',\n    );\n\n    // 当过渡动画结束时，移除 loading 元素和所有注入的 loading 元素\n    loadingElement.addEventListener(\n      'transitionend',\n      () => {\n        loadingElement.remove(); // 移除 loading 元素\n        injectLoadingElements.forEach((el) => el.remove()); // 移除所有注入的 loading 元素\n      },\n      { once: true },\n    ); // 确保事件只触发一次\n  }\n}\n"
  },
  {
    "path": "packages/utils/src/helpers/uuid.ts",
    "content": "const hexList: string[] = [];\nfor (let i = 0; i <= 15; i++) {\n  hexList[i] = i.toString(16);\n}\n\nexport function buildUUID(): string {\n  let uuid = '';\n  for (let i = 1; i <= 36; i++) {\n    switch (i) {\n      case 9:\n      case 14:\n      case 19:\n      case 24: {\n        uuid += '-';\n\n        break;\n      }\n      case 15: {\n        uuid += 4;\n\n        break;\n      }\n      case 20: {\n        uuid += hexList[(Math.random() * 4) | 8];\n\n        break;\n      }\n      default: {\n        uuid += hexList[Math.trunc(Math.random() * 16)];\n      }\n    }\n  }\n  return uuid.replaceAll('-', '');\n}\n\nlet unique = 0;\nexport function buildShortUUID(prefix = ''): string {\n  const time = Date.now();\n  const random = Math.floor(Math.random() * 1_000_000_000);\n  unique++;\n  return `${prefix}_${random}${unique}${String(time)}`;\n}\n"
  },
  {
    "path": "packages/utils/src/index.ts",
    "content": "export * from './encryption';\nexport * from './helpers';\nexport * from '@vben-core/shared/cache';\nexport * from '@vben-core/shared/color';\nexport * from '@vben-core/shared/utils';\nexport { fileTypeFromBlob } from 'file-type';\n"
  },
  {
    "path": "packages/utils/tsconfig.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/tsconfig\",\n  \"extends\": \"@vben/tsconfig/library.json\",\n  \"compilerOptions\": {\n    \"types\": [\"@vben-core/typings/vue-router\"]\n  },\n  \"include\": [\"src\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "pnpm-workspace.yaml",
    "content": "packages:\n  - internal/*\n  - internal/lint-configs/*\n  - packages/*\n  - packages/@core/base/*\n  - packages/@core/ui-kit/*\n  - packages/@core/forward/*\n  - packages/@core/*\n  - packages/effects/*\n  - packages/business/*\n  - apps/*\n  - scripts/*\n  - docs\n  - playground\n\ncatalog:\n  '@ast-grep/napi': ^0.37.0\n  '@changesets/changelog-github': ^0.5.1\n  '@changesets/cli': ^2.29.5\n  '@changesets/git': ^3.0.4\n  '@clack/prompts': ^0.10.1\n  '@commitlint/cli': ^19.8.1\n  '@commitlint/config-conventional': ^19.8.1\n  '@ctrl/tinycolor': ^4.1.0\n  '@eslint/js': ^9.30.1\n  '@faker-js/faker': ^9.9.0\n  '@iconify/json': ^2.2.354\n  '@iconify/tailwind': ^1.2.0\n  '@iconify/vue': ^5.0.0\n  '@intlify/core-base': ^11.1.7\n  '@intlify/unplugin-vue-i18n': ^6.0.8\n  '@jspm/generator': ^2.6.2\n  '@manypkg/get-packages': ^3.0.0\n  '@nolebase/vitepress-plugin-git-changelog': ^2.18.0\n  '@playwright/test': ^1.53.2\n  '@pnpm/workspace.read-manifest': ^1000.2.0\n  '@stylistic/stylelint-plugin': ^3.1.3\n  '@tailwindcss/nesting': 0.0.0-insiders.565cd3e\n  '@tailwindcss/typography': ^0.5.16\n  '@tanstack/vue-query': ^5.81.5\n  '@tanstack/vue-store': ^0.7.1\n  '@types/archiver': ^6.0.3\n  '@types/eslint': ^9.6.1\n  '@types/html-minifier-terser': ^7.0.2\n  '@types/json-bigint': ^1.0.4\n  '@types/jsonwebtoken': ^9.0.10\n  '@types/lodash.clonedeep': ^4.5.9\n  '@types/lodash.get': ^4.4.9\n  '@types/lodash.isequal': ^4.5.8\n  '@types/lodash.set': ^4.3.9\n  '@types/node': ^22.16.0\n  '@types/nprogress': ^0.2.3\n  '@types/postcss-import': ^14.0.3\n  '@types/qrcode': ^1.5.5\n  '@types/qs': ^6.14.0\n  '@types/sortablejs': ^1.15.8\n  '@typescript-eslint/eslint-plugin': ^8.35.1\n  '@typescript-eslint/parser': ^8.35.1\n  '@vee-validate/zod': ^4.15.1\n  '@vite-pwa/vitepress': ^1.0.0\n  '@vitejs/plugin-vue': ^6.0.1\n  '@vitejs/plugin-vue-jsx': ^5.0.1\n  '@vue/reactivity': ^3.5.17\n  '@vue/shared': ^3.5.17\n  '@vue/test-utils': ^2.4.6\n  '@vueuse/core': ^13.4.0\n  '@vueuse/integrations': ^13.4.0\n  '@vueuse/motion': ^3.0.3\n  ant-design-vue: ^4.2.6\n  archiver: ^7.0.1\n  autoprefixer: ^10.4.21\n  axios: ^1.10.0\n  axios-mock-adapter: ^2.1.0\n  cac: ^6.7.14\n  chalk: ^5.4.1\n  cheerio: ^1.1.0\n  circular-dependency-scanner: ^2.3.0\n  class-variance-authority: ^0.7.1\n  clsx: ^2.1.1\n  commitlint-plugin-function-rules: ^4.0.2\n  consola: ^3.4.2\n  cross-env: ^7.0.3\n  cspell: ^8.19.4\n  cssnano: ^7.0.7\n  cz-git: ^1.11.2\n  czg: ^1.11.1\n  dayjs: ^1.11.13\n  defu: ^6.1.4\n  depcheck: ^1.4.7\n  dotenv: ^16.6.1\n  echarts: ^5.6.0\n  element-plus: ^2.10.2\n  eslint: ^9.30.1\n  eslint-config-turbo: ^2.5.4\n  eslint-plugin-command: ^3.3.1\n  eslint-plugin-eslint-comments: ^3.2.0\n  eslint-plugin-import-x: ^4.16.1\n  eslint-plugin-jsdoc: ^50.8.0\n  eslint-plugin-jsonc: ^2.20.1\n  eslint-plugin-n: ^17.20.0\n  eslint-plugin-no-only-tests: ^3.3.0\n  eslint-plugin-perfectionist: ^4.15.0\n  eslint-plugin-prettier: ^5.5.1\n  eslint-plugin-regexp: ^2.9.0\n  eslint-plugin-unicorn: ^59.0.1\n  eslint-plugin-unused-imports: ^4.1.4\n  eslint-plugin-vitest: ^0.5.4\n  eslint-plugin-vue: ^10.2.0\n  execa: ^9.6.0\n  find-up: ^7.0.0\n  get-port: ^7.1.0\n  globals: ^16.3.0\n  h3: ^1.15.3\n  happy-dom: ^17.6.3\n  html-minifier-terser: ^7.2.0\n  is-ci: ^4.1.0\n  json-bigint: ^1.0.0\n  jsonc-eslint-parser: ^2.4.0\n  jsonwebtoken: ^9.0.2\n  lefthook: 1.11.12\n  lodash.clonedeep: ^4.5.0\n  lodash.get: ^4.4.2\n  lodash.isequal: ^4.5.0\n  lodash.set: ^4.3.2\n  lucide-vue-next: ^0.507.0\n  medium-zoom: ^1.1.0\n  naive-ui: ^2.42.0\n  nitropack: ^2.11.13\n  nprogress: ^0.2.0\n  ora: ^8.2.0\n  pinia: ^3.0.3\n  pinia-plugin-persistedstate: ^4.4.1\n  pkg-types: ^2.2.0\n  playwright: ^1.53.2\n  postcss: ^8.5.6\n  postcss-antd-fixes: ^0.2.0\n  postcss-html: ^1.8.0\n  postcss-import: ^16.1.1\n  postcss-preset-env: ^10.2.4\n  postcss-scss: ^4.0.9\n  prettier: ^3.6.2\n  prettier-plugin-tailwindcss: ^0.6.13\n  publint: ^0.3.12\n  qrcode: ^1.5.4\n  qs: ^6.14.0\n  radix-vue: ^1.9.17\n  resolve.exports: ^2.0.3\n  rimraf: ^6.0.1\n  rollup: ^4.44.1\n  rollup-plugin-visualizer: ^5.14.0\n  sass: ^1.89.2\n  secure-ls: ^2.0.0\n  sortablejs: ^1.15.6\n  stylelint: ^16.21.0\n  stylelint-config-recess-order: ^6.1.0\n  stylelint-config-recommended: ^16.0.0\n  stylelint-config-recommended-scss: ^14.1.0\n  stylelint-config-recommended-vue: ^1.6.1\n  stylelint-config-standard: ^38.0.0\n  stylelint-order: ^7.0.0\n  stylelint-prettier: ^5.0.3\n  stylelint-scss: ^6.12.1\n  tailwind-merge: ^2.6.0\n  tailwindcss: ^3.4.17\n  tailwindcss-animate: ^1.0.7\n  theme-colors: ^0.1.0\n  tippy.js: ^6.3.7\n  turbo: ^2.5.4\n  typescript: ^5.8.3\n  unbuild: ^3.6.1\n  unplugin-element-plus: ^0.10.0\n  vee-validate: ^4.15.1\n  vite: ^7.1.2\n  vite-plugin-compression: ^0.5.1\n  vite-plugin-dts: ^4.5.4\n  vite-plugin-html: ^3.2.2\n  vite-plugin-lazy-import: ^1.0.7\n  vite-plugin-pwa: ^1.0.1\n  vite-plugin-vue-devtools: ^7.7.7\n  vitepress: ^1.6.3\n  vitepress-plugin-group-icons: ^1.6.1\n  vitest: ^3.2.4\n  vue: ^3.5.17\n  vue-eslint-parser: ^10.2.0\n  vue-i18n: ^11.1.7\n  vue-json-viewer: ^3.0.4\n  vue-router: ^4.5.1\n  vue-tippy: ^6.7.1\n  vue-tsc: 2.2.10\n  vxe-pc-ui: 4.10.36\n  vxe-table: ^4.16.11\n  watermark-js-plus: ^1.6.2\n  zod: ^3.25.67\n  zod-defaults: ^0.1.3\n"
  },
  {
    "path": "scripts/clean.mjs",
    "content": "import { promises as fs } from 'node:fs';\nimport { join, normalize } from 'node:path';\n\nconst rootDir = process.cwd();\n\n// 控制并发数量，避免创建过多的并发任务\nconst CONCURRENCY_LIMIT = 10;\n\n// 需要跳过的目录，避免进入这些目录进行清理\nconst SKIP_DIRS = new Set(['.DS_Store', '.git', '.idea', '.vscode']);\n\n/**\n * 处理单个文件/目录项\n * @param {string} currentDir - 当前目录路径\n * @param {string} item - 文件/目录名\n * @param {string[]} targets - 要删除的目标列表\n * @param {number} _depth - 当前递归深度\n * @returns {Promise<boolean>} - 是否需要进一步递归处理\n */\nasync function processItem(currentDir, item, targets, _depth) {\n  // 跳过特殊目录\n  if (SKIP_DIRS.has(item)) {\n    return false;\n  }\n\n  try {\n    const itemPath = normalize(join(currentDir, item));\n\n    if (targets.includes(item)) {\n      // 匹配到目标目录或文件时直接删除\n      await fs.rm(itemPath, { force: true, recursive: true });\n      console.log(`✅ Deleted: ${itemPath}`);\n      return false; // 已删除，无需递归\n    }\n\n    // 使用 readdir 的 withFileTypes 选项，避免额外的 lstat 调用\n    return true; // 可能需要递归，由调用方决定\n  } catch (error) {\n    // 更详细的错误信息\n    if (error.code === 'ENOENT') {\n      // 文件不存在，可能已被删除，这是正常情况\n      return false;\n    } else if (error.code === 'EPERM' || error.code === 'EACCES') {\n      console.error(`❌ Permission denied: ${item} in ${currentDir}`);\n    } else {\n      console.error(\n        `❌ Error handling item ${item} in ${currentDir}: ${error.message}`,\n      );\n    }\n    return false;\n  }\n}\n\n/**\n * 递归查找并删除目标目录（并发优化版本）\n * @param {string} currentDir - 当前遍历的目录路径\n * @param {string[]} targets - 要删除的目标列表\n * @param {number} depth - 当前递归深度，避免过深递归\n */\nasync function cleanTargetsRecursively(currentDir, targets, depth = 0) {\n  // 限制递归深度，避免无限递归\n  if (depth > 10) {\n    console.warn(`Max recursion depth reached at: ${currentDir}`);\n    return;\n  }\n\n  let dirents;\n  try {\n    // 使用 withFileTypes 选项，一次性获取文件类型信息，避免后续 lstat 调用\n    dirents = await fs.readdir(currentDir, { withFileTypes: true });\n  } catch (error) {\n    // 如果无法读取目录，可能已被删除或权限不足\n    console.warn(`Cannot read directory ${currentDir}: ${error.message}`);\n    return;\n  }\n\n  // 分批处理，控制并发数量\n  for (let i = 0; i < dirents.length; i += CONCURRENCY_LIMIT) {\n    const batch = dirents.slice(i, i + CONCURRENCY_LIMIT);\n\n    const tasks = batch.map(async (dirent) => {\n      const item = dirent.name;\n      const shouldRecurse = await processItem(currentDir, item, targets, depth);\n\n      // 如果是目录且没有被删除，则递归处理\n      if (shouldRecurse && dirent.isDirectory()) {\n        const itemPath = normalize(join(currentDir, item));\n        return cleanTargetsRecursively(itemPath, targets, depth + 1);\n      }\n\n      return null;\n    });\n\n    // 并发执行当前批次的任务\n    const results = await Promise.allSettled(tasks);\n\n    // 检查是否有失败的任务（可选：用于调试）\n    const failedTasks = results.filter(\n      (result) => result.status === 'rejected',\n    );\n    if (failedTasks.length > 0) {\n      console.warn(\n        `${failedTasks.length} tasks failed in batch starting at index ${i} in directory: ${currentDir}`,\n      );\n    }\n  }\n}\n\n(async function startCleanup() {\n  // 要删除的目录及文件名称\n  const targets = ['node_modules', 'dist', '.turbo', 'dist.zip'];\n  const deleteLockFile = process.argv.includes('--del-lock');\n  const cleanupTargets = [...targets];\n\n  if (deleteLockFile) {\n    cleanupTargets.push('pnpm-lock.yaml');\n  }\n\n  console.log(\n    `🚀 Starting cleanup of targets: ${cleanupTargets.join(', ')} from root: ${rootDir}`,\n  );\n\n  const startTime = Date.now();\n\n  try {\n    // 先统计要删除的目标数量\n    console.log('📊 Scanning for cleanup targets...');\n\n    await cleanTargetsRecursively(rootDir, cleanupTargets);\n\n    const endTime = Date.now();\n    const duration = (endTime - startTime) / 1000;\n\n    console.log(\n      `✨ Cleanup process completed successfully in ${duration.toFixed(2)}s`,\n    );\n  } catch (error) {\n    console.error(`💥 Unexpected error during cleanup: ${error.message}`);\n    process.exit(1);\n  }\n})();\n"
  },
  {
    "path": "scripts/deploy/Dockerfile",
    "content": "FROM node:22-slim AS builder\n\n# --max-old-space-size\nENV PNPM_HOME=\"/pnpm\"\nENV PATH=\"$PNPM_HOME:$PATH\"\nENV NODE_OPTIONS=--max-old-space-size=8192\nENV TZ=Asia/Shanghai\n\nRUN npm i -g corepack\n\nWORKDIR /app\n\n# copy package.json and pnpm-lock.yaml to workspace\nCOPY . /app\n\n# 安装依赖\nRUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile\nRUN pnpm run build --filter=\\!./docs\n\nRUN echo \"Builder Success 🎉\"\n\nFROM nginx:stable-alpine AS production\n\n# 配置 nginx\nRUN echo \"types { application/javascript js mjs; }\" > /etc/nginx/conf.d/mjs.conf \\\n    && rm -rf /etc/nginx/conf.d/default.conf\n\n# 复制构建产物\nCOPY --from=builder /app/playground/dist /usr/share/nginx/html\n\n# 复制 nginx 配置\nCOPY --from=builder /app/scripts/deploy/nginx.conf /etc/nginx/nginx.conf\n\nEXPOSE 8080\n\n# 启动 nginx\nCMD [\"nginx\", \"-g\", \"daemon off;\"]\n"
  },
  {
    "path": "scripts/deploy/build-local-docker-image.sh",
    "content": "#!/bin/bash\n\nSCRIPT_DIR=$( cd -- \"$( dirname -- \"${BASH_SOURCE[0]}\" )\" &> /dev/null && pwd )\nLOG_FILE=${SCRIPT_DIR}/build-local-docker-image.log\nERROR=\"\"\nIMAGE_NAME=\"vben-admin-local\"\n\nfunction stop_and_remove_container() {\n    # Stop and remove the existing container\n    docker stop ${IMAGE_NAME} >/dev/null 2>&1\n    docker rm ${IMAGE_NAME} >/dev/null 2>&1\n}\n\nfunction remove_image() {\n    # Remove the existing image\n    docker rmi vben-admin-pro >/dev/null 2>&1\n}\n\nfunction install_dependencies() {\n    # Install all dependencies\n    cd ${SCRIPT_DIR}\n    pnpm install || ERROR=\"install_dependencies failed\"\n}\n\nfunction build_image() {\n    # build docker\n    docker build ../../ -f Dockerfile -t ${IMAGE_NAME} || ERROR=\"build_image failed\"\n}\n\nfunction log_message() {\n    if [[ ${ERROR} != \"\" ]];\n    then\n        >&2 echo \"build failed, Please check build-local-docker-image.log for more details\"\n        >&2 echo \"ERROR: ${ERROR}\"\n        exit 1\n    else\n        echo \"docker image with tag '${IMAGE_NAME}' built sussessfully. Use below sample command to run the container\"\n        echo \"\"\n        echo \"docker run -d -p 8010:8080 --name ${IMAGE_NAME} ${IMAGE_NAME}\"\n    fi\n}\n\necho \"Info: Stopping and removing existing container and image\" | tee ${LOG_FILE}\nstop_and_remove_container\nremove_image\n\necho \"Info: Installing dependencies\" | tee -a ${LOG_FILE}\ninstall_dependencies 1>> ${LOG_FILE} 2>> ${LOG_FILE}\n\nif [[ ${ERROR} == \"\" ]]; then\n    echo \"Info: Building docker image\" | tee -a ${LOG_FILE}\n    build_image 1>> ${LOG_FILE} 2>> ${LOG_FILE}\nfi\n\nlog_message | tee -a ${LOG_FILE}\n"
  },
  {
    "path": "scripts/deploy/nginx.conf",
    "content": "\n#user  nobody;\nworker_processes 1;\n\n#error_log  logs/error.log;\n#error_log  logs/error.log  notice;\n#error_log  logs/error.log  info;\n\n#pid        logs/nginx.pid;\n\n\nevents {\n  worker_connections 1024;\n}\n\n\nhttp {\n  include mime.types;\n  default_type application/octet-stream;\n\n  types {\n    application/javascript  js mjs;\n    text/css                css;\n    text/html               html;\n  }\n\n  sendfile on;\n  # tcp_nopush     on;\n\n  #keepalive_timeout  0;\n  # keepalive_timeout 65;\n\n  # gzip on;\n  # gzip_buffers 32 16k;\n  # gzip_comp_level 6;\n  # gzip_min_length 1k;\n  # gzip_static on;\n  # gzip_types text/plain\n  #   text/css\n  #   application/javascript\n  #   application/json\n  #   application/x-javascript\n  #   text/xml\n  #   application/xml\n  #   application/xml+rss\n  #   text/javascript; #设置压缩的文件类型\n  # gzip_vary on;\n\n  server {\n    listen 8080;\n    server_name localhost;\n\n    location / {\n      root /usr/share/nginx/html;\n      try_files $uri $uri/ /index.html;\n      index index.html;\n      # Enable CORS\n      add_header 'Access-Control-Allow-Origin' '*';\n      add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';\n      add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';\n      if ($request_method = 'OPTIONS') {\n        add_header 'Access-Control-Max-Age' 1728000;\n        add_header 'Content-Type' 'text/plain charset=UTF-8';\n        add_header 'Content-Length' 0;\n        return 204;\n      }\n    }\n\n    error_page 500 502 503 504 /50x.html;\n\n    location = /50x.html {\n        root /usr/share/nginx/html;\n    }\n  }\n}\n"
  },
  {
    "path": "scripts/turbo-run/README.md",
    "content": "# @vben/turbo-run\n\n`turbo-run` 是一个命令行工具，允许你在多个包中并行运行命令。它提供了一个交互式的界面，让你可以选择要运行命令的包。\n\n## 特性\n\n- 🚀 交互式选择要运行的包\n- 📦 支持 monorepo 项目结构\n- 🔍 自动检测可用的命令\n- 🎯 精确过滤目标包\n\n## 安装\n\n```bash\npnpm add -D @vben/turbo-run\n```\n\n## 使用方法\n\n基本语法：\n\n```bash\nturbo-run [script]\n```\n\n例如，如果你想运行 `dev` 命令：\n\n```bash\nturbo-run dev\n```\n\n工具会自动检测哪些包有 `dev` 命令，并提供一个交互式界面让你选择要运行的包。\n\n## 示例\n\n假设你的项目中有以下包：\n\n- `@vben/app`\n- `@vben/admin`\n- `@vben/website`\n\n当你运行：\n\n```bash\nturbo-run dev\n```\n\n工具会：\n\n1. 检测哪些包有 `dev` 命令\n2. 显示一个交互式选择界面\n3. 让你选择要运行命令的包\n4. 使用 `pnpm --filter` 在选定的包中运行命令\n\n## 注意事项\n\n- 确保你的项目使用 pnpm 作为包管理器\n- 确保目标包在 `package.json` 中定义了相应的脚本命令\n- 该工具需要在 monorepo 项目的根目录下运行\n"
  },
  {
    "path": "scripts/turbo-run/bin/turbo-run.mjs",
    "content": "#!/usr/bin/env node\n\nimport('../dist/index.mjs');\n"
  },
  {
    "path": "scripts/turbo-run/build.config.ts",
    "content": "import { defineBuildConfig } from 'unbuild';\n\nexport default defineBuildConfig({\n  clean: true,\n  declaration: true,\n  entries: ['src/index'],\n});\n"
  },
  {
    "path": "scripts/turbo-run/package.json",
    "content": "{\n  \"name\": \"@vben/turbo-run\",\n  \"version\": \"5.5.9\",\n  \"private\": true,\n  \"license\": \"MIT\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"stub\": \"pnpm unbuild --stub\"\n  },\n  \"files\": [\n    \"dist\"\n  ],\n  \"bin\": {\n    \"turbo-run\": \"./bin/turbo-run.mjs\"\n  },\n  \"main\": \"./dist/index.mjs\",\n  \"module\": \"./dist/index.mjs\",\n  \"exports\": {\n    \".\": {\n      \"default\": \"./dist/index.mjs\"\n    },\n    \"./package.json\": \"./package.json\"\n  },\n  \"dependencies\": {\n    \"@clack/prompts\": \"catalog:\",\n    \"@vben/node-utils\": \"workspace:*\",\n    \"cac\": \"catalog:\"\n  }\n}\n"
  },
  {
    "path": "scripts/turbo-run/src/index.ts",
    "content": "import { colors, consola } from '@vben/node-utils';\n\nimport { cac } from 'cac';\n\nimport { run } from './run';\n\ntry {\n  const turboRun = cac('turbo-run');\n\n  turboRun\n    .command('[script]')\n    .usage(`Run turbo interactively.`)\n    .action(async (command: string) => {\n      run({ command });\n    });\n\n  // Invalid command\n  turboRun.on('command:*', () => {\n    consola.error(colors.red('Invalid command!'));\n    process.exit(1);\n  });\n\n  turboRun.usage('turbo-run');\n  turboRun.help();\n  turboRun.parse();\n} catch (error) {\n  consola.error(error);\n  process.exit(1);\n}\n"
  },
  {
    "path": "scripts/turbo-run/src/run.ts",
    "content": "import { execaCommand, getPackages } from '@vben/node-utils';\n\nimport { cancel, isCancel, select } from '@clack/prompts';\n\ninterface RunOptions {\n  command?: string;\n}\n\nexport async function run(options: RunOptions) {\n  const { command } = options;\n  if (!command) {\n    console.error('Please enter the command to run');\n    process.exit(1);\n  }\n  const { packages } = await getPackages();\n  // const appPkgs = await findApps(process.cwd(), packages);\n  // const websitePkg = packages.find(\n  //   (item) => item.packageJson.name === '@vben/website',\n  // );\n\n  // 只显示有对应命令的包\n  const selectPkgs = packages.filter((pkg) => {\n    return (pkg?.packageJson as Record<string, any>)?.scripts?.[command];\n  });\n\n  let selectPkg: string | symbol;\n  if (selectPkgs.length > 1) {\n    selectPkg = await select<string>({\n      message: `Select the app you need to run [${command}]:`,\n      options: selectPkgs.map((item) => ({\n        label: item?.packageJson.name,\n        value: item?.packageJson.name,\n      })),\n    });\n\n    if (isCancel(selectPkg) || !selectPkg) {\n      cancel('👋 Has cancelled');\n      process.exit(0);\n    }\n  } else {\n    selectPkg = selectPkgs[0]?.packageJson?.name ?? '';\n  }\n\n  if (!selectPkg) {\n    console.error('No app found');\n    process.exit(1);\n  }\n\n  execaCommand(`pnpm --filter=${selectPkg} run ${command}`, {\n    stdio: 'inherit',\n  });\n}\n\n/**\n * 过滤app包\n * @param root\n * @param packages\n */\n// async function findApps(root: string, packages: Package[]) {\n//   // apps内的\n//   const appPackages = packages.filter((pkg) => {\n//     const viteConfigExists = fs.existsSync(join(pkg.dir, 'vite.config.mts'));\n//     return pkg.dir.startsWith(join(root, 'apps')) && viteConfigExists;\n//   });\n\n//   return appPackages;\n// }\n"
  },
  {
    "path": "scripts/turbo-run/tsconfig.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/tsconfig\",\n  \"extends\": \"@vben/tsconfig/node.json\",\n  \"include\": [\"src\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "scripts/vsh/README.md",
    "content": "# @vben/vsh\n\n一个 Shell 脚本工具集合，用于 Vue Vben Admin 项目的开发和管理。\n\n## 功能特性\n\n- 🚀 基于 Node.js 的现代化 Shell 工具\n- 📦 支持模块化开发和按需加载\n- 🔍 提供依赖检查和分析功能\n- 🔄 支持循环依赖扫描\n- 📝 提供包发布检查功能\n\n## 安装\n\n```bash\n# 使用 pnpm 安装\npnpm add -D @vben/vsh\n\n# 或者使用 npm\nnpm install -D @vben/vsh\n\n# 或者使用 yarn\nyarn add -D @vben/vsh\n```\n\n## 使用方法\n\n### 全局安装\n\n```bash\n# 全局安装\npnpm add -g @vben/vsh\n\n# 使用 vsh 命令\nvsh [command]\n```\n\n### 本地使用\n\n```bash\n# 在 package.json 中添加脚本\n{\n  \"scripts\": {\n    \"vsh\": \"vsh\"\n  }\n}\n\n# 运行命令\npnpm vsh [command]\n```\n\n## 命令列表\n\n- `vsh check-deps`: 检查项目依赖\n- `vsh scan-circular`: 扫描循环依赖\n- `vsh publish-check`: 检查包发布配置\n"
  },
  {
    "path": "scripts/vsh/bin/vsh.mjs",
    "content": "#!/usr/bin/env node\n\nimport('../dist/index.mjs');\n"
  },
  {
    "path": "scripts/vsh/build.config.ts",
    "content": "import { defineBuildConfig } from 'unbuild';\n\nexport default defineBuildConfig({\n  clean: true,\n  declaration: true,\n  entries: ['src/index'],\n});\n"
  },
  {
    "path": "scripts/vsh/package.json",
    "content": "{\n  \"name\": \"@vben/vsh\",\n  \"version\": \"5.5.9\",\n  \"private\": true,\n  \"license\": \"MIT\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"stub\": \"pnpm unbuild --stub\"\n  },\n  \"files\": [\n    \"dist\"\n  ],\n  \"bin\": {\n    \"vsh\": \"./bin/vsh.mjs\"\n  },\n  \"main\": \"./dist/index.mjs\",\n  \"module\": \"./dist/index.mjs\",\n  \"exports\": {\n    \".\": {\n      \"default\": \"./dist/index.mjs\"\n    },\n    \"./package.json\": \"./package.json\"\n  },\n  \"dependencies\": {\n    \"@vben/node-utils\": \"workspace:*\",\n    \"cac\": \"catalog:\",\n    \"circular-dependency-scanner\": \"catalog:\",\n    \"depcheck\": \"catalog:\",\n    \"publint\": \"catalog:\"\n  }\n}\n"
  },
  {
    "path": "scripts/vsh/src/check-circular/index.ts",
    "content": "import type { CAC } from 'cac';\n\nimport { extname } from 'node:path';\n\nimport { getStagedFiles } from '@vben/node-utils';\n\nimport { circularDepsDetect } from 'circular-dependency-scanner';\n\n// 默认配置\nconst DEFAULT_CONFIG = {\n  allowedExtensions: ['.cjs', '.js', '.jsx', '.mjs', '.ts', '.tsx', '.vue'],\n  ignoreDirs: [\n    'dist',\n    '.turbo',\n    'output',\n    '.cache',\n    'scripts',\n    'internal',\n    'packages/effects/request/src/',\n    'packages/@core/ui-kit/menu-ui/src/',\n    'packages/@core/ui-kit/popup-ui/src/',\n  ],\n  threshold: 0, // 循环依赖的阈值\n} as const;\n\n// 类型定义\ntype CircularDependencyResult = string[];\n\ninterface CheckCircularConfig {\n  allowedExtensions?: string[];\n  ignoreDirs?: string[];\n  threshold?: number;\n}\n\ninterface CommandOptions {\n  config?: CheckCircularConfig;\n  staged: boolean;\n  verbose: boolean;\n}\n\n// 缓存机制\nconst cache = new Map<string, CircularDependencyResult[]>();\n\n/**\n * 格式化循环依赖的输出\n * @param circles - 循环依赖结果\n */\nfunction formatCircles(circles: CircularDependencyResult[]): void {\n  if (circles.length === 0) {\n    console.log('✅ No circular dependencies found');\n    return;\n  }\n\n  console.log('⚠️ Circular dependencies found:');\n  circles.forEach((circle, index) => {\n    console.log(`\\nCircular dependency #${index + 1}:`);\n    circle.forEach((file) => console.log(`  → ${file}`));\n  });\n}\n\n/**\n * 检查项目中的循环依赖\n * @param options - 检查选项\n * @param options.staged - 是否只检查暂存区文件\n * @param options.verbose - 是否显示详细信息\n * @param options.config - 自定义配置\n * @returns Promise<void>\n */\nasync function checkCircular({\n  config = {},\n  staged,\n  verbose,\n}: CommandOptions): Promise<void> {\n  try {\n    // 合并配置\n    const finalConfig = {\n      ...DEFAULT_CONFIG,\n      ...config,\n    };\n\n    // 生成忽略模式\n    const ignorePattern = `**/{${finalConfig.ignoreDirs.join(',')}}/**`;\n\n    // 检查缓存\n    const cacheKey = `${staged}-${process.cwd()}-${ignorePattern}`;\n    if (cache.has(cacheKey)) {\n      const cachedResults = cache.get(cacheKey);\n      if (cachedResults) {\n        verbose && formatCircles(cachedResults);\n      }\n      return;\n    }\n\n    // 检测循环依赖\n    const results = await circularDepsDetect({\n      absolute: staged,\n      cwd: process.cwd(),\n      ignore: [ignorePattern],\n    });\n\n    if (staged) {\n      let files = await getStagedFiles();\n      const allowedExtensions = new Set(finalConfig.allowedExtensions);\n\n      // 过滤文件列表\n      files = files.filter((file) => allowedExtensions.has(extname(file)));\n\n      const circularFiles: CircularDependencyResult[] = [];\n\n      for (const file of files) {\n        for (const result of results) {\n          const resultFiles = result.flat();\n          if (resultFiles.includes(file)) {\n            circularFiles.push(result);\n          }\n        }\n      }\n\n      // 更新缓存\n      cache.set(cacheKey, circularFiles);\n      verbose && formatCircles(circularFiles);\n    } else {\n      // 更新缓存\n      cache.set(cacheKey, results);\n      verbose && formatCircles(results);\n    }\n\n    // 如果发现循环依赖，只输出警告信息\n    if (results.length > 0) {\n      console.log(\n        '\\n⚠️ Warning: Circular dependencies found, please check and fix',\n      );\n    }\n  } catch (error) {\n    console.error(\n      '❌ Error checking circular dependencies:',\n      error instanceof Error ? error.message : error,\n    );\n  }\n}\n\n/**\n * 定义检查循环依赖的命令\n * @param cac - CAC实例\n */\nfunction defineCheckCircularCommand(cac: CAC): void {\n  cac\n    .command('check-circular')\n    .option('--staged', 'Only check staged files')\n    .option('--verbose', 'Show detailed information')\n    .option('--threshold <number>', 'Threshold for circular dependencies', {\n      default: 0,\n    })\n    .option('--ignore-dirs <dirs>', 'Directories to ignore, comma separated')\n    .usage('Analyze project circular dependencies')\n    .action(async ({ ignoreDirs, staged, threshold, verbose }) => {\n      const config: CheckCircularConfig = {\n        threshold: Number(threshold),\n        ...(ignoreDirs && { ignoreDirs: ignoreDirs.split(',') }),\n      };\n\n      await checkCircular({\n        config,\n        staged,\n        verbose: verbose ?? true,\n      });\n    });\n}\n\nexport { type CheckCircularConfig, defineCheckCircularCommand };\n"
  },
  {
    "path": "scripts/vsh/src/check-dep/index.ts",
    "content": "import type { CAC } from 'cac';\n\nimport { getPackages } from '@vben/node-utils';\n\nimport depcheck from 'depcheck';\n\n// 默认配置\nconst DEFAULT_CONFIG = {\n  // 需要忽略的依赖匹配\n  ignoreMatches: [\n    'vite',\n    'vitest',\n    'unbuild',\n    '@vben/tsconfig',\n    '@vben/vite-config',\n    '@vben/tailwind-config',\n    '@types/*',\n    '@vben-core/design',\n  ],\n  // 需要忽略的包\n  ignorePackages: [\n    '@vben/backend-mock',\n    '@vben/commitlint-config',\n    '@vben/eslint-config',\n    '@vben/node-utils',\n    '@vben/prettier-config',\n    '@vben/stylelint-config',\n    '@vben/tailwind-config',\n    '@vben/tsconfig',\n    '@vben/vite-config',\n    '@vben/vsh',\n  ],\n  // 需要忽略的文件模式\n  ignorePatterns: ['dist', 'node_modules', 'public'],\n};\n\ninterface DepcheckResult {\n  dependencies: string[];\n  devDependencies: string[];\n  missing: Record<string, string[]>;\n}\n\ninterface DepcheckConfig {\n  ignoreMatches?: string[];\n  ignorePackages?: string[];\n  ignorePatterns?: string[];\n}\n\ninterface PackageInfo {\n  dir: string;\n  packageJson: {\n    name: string;\n  };\n}\n\n/**\n * 清理依赖检查结果\n * @param unused - 依赖检查结果\n */\nfunction cleanDepcheckResult(unused: DepcheckResult): void {\n  // 删除file:前缀的依赖提示，该依赖是本地依赖\n  Reflect.deleteProperty(unused.missing, 'file:');\n\n  // 清理路径依赖\n  Object.keys(unused.missing).forEach((key) => {\n    unused.missing[key] = (unused.missing[key] || []).filter(\n      (item: string) => !item.startsWith('/'),\n    );\n    if (unused.missing[key].length === 0) {\n      Reflect.deleteProperty(unused.missing, key);\n    }\n  });\n}\n\n/**\n * 格式化依赖检查结果\n * @param pkgName - 包名\n * @param unused - 依赖检查结果\n */\nfunction formatDepcheckResult(pkgName: string, unused: DepcheckResult): void {\n  const hasIssues =\n    Object.keys(unused.missing).length > 0 ||\n    unused.dependencies.length > 0 ||\n    unused.devDependencies.length > 0;\n\n  if (!hasIssues) {\n    return;\n  }\n\n  console.log('\\n📦 Package:', pkgName);\n\n  if (Object.keys(unused.missing).length > 0) {\n    console.log('❌ Missing dependencies:');\n    Object.entries(unused.missing).forEach(([dep, files]) => {\n      console.log(`  - ${dep}:`);\n      files.forEach((file) => console.log(`    → ${file}`));\n    });\n  }\n\n  if (unused.dependencies.length > 0) {\n    console.log('⚠️ Unused dependencies:');\n    unused.dependencies.forEach((dep) => console.log(`  - ${dep}`));\n  }\n\n  if (unused.devDependencies.length > 0) {\n    console.log('⚠️ Unused devDependencies:');\n    unused.devDependencies.forEach((dep) => console.log(`  - ${dep}`));\n  }\n}\n\n/**\n * 运行依赖检查\n * @param config - 配置选项\n */\nasync function runDepcheck(config: DepcheckConfig = {}): Promise<void> {\n  try {\n    const finalConfig = {\n      ...DEFAULT_CONFIG,\n      ...config,\n    };\n\n    const { packages } = await getPackages();\n\n    let hasIssues = false;\n\n    await Promise.all(\n      packages.map(async (pkg: PackageInfo) => {\n        // 跳过需要忽略的包\n        if (finalConfig.ignorePackages.includes(pkg.packageJson.name)) {\n          return;\n        }\n\n        const unused = await depcheck(pkg.dir, {\n          ignoreMatches: finalConfig.ignoreMatches,\n          ignorePatterns: finalConfig.ignorePatterns,\n        });\n\n        cleanDepcheckResult(unused);\n\n        const pkgHasIssues =\n          Object.keys(unused.missing).length > 0 ||\n          unused.dependencies.length > 0 ||\n          unused.devDependencies.length > 0;\n\n        if (pkgHasIssues) {\n          hasIssues = true;\n          formatDepcheckResult(pkg.packageJson.name, unused);\n        }\n      }),\n    );\n\n    if (!hasIssues) {\n      console.log('\\n✅ Dependency check completed, no issues found');\n    }\n  } catch (error) {\n    console.error(\n      '❌ Dependency check failed:',\n      error instanceof Error ? error.message : error,\n    );\n  }\n}\n\n/**\n * 定义依赖检查命令\n * @param cac - CAC实例\n */\nfunction defineDepcheckCommand(cac: CAC): void {\n  cac\n    .command('check-dep')\n    .option(\n      '--ignore-packages <packages>',\n      'Packages to ignore, comma separated',\n    )\n    .option(\n      '--ignore-matches <matches>',\n      'Dependency patterns to ignore, comma separated',\n    )\n    .option(\n      '--ignore-patterns <patterns>',\n      'File patterns to ignore, comma separated',\n    )\n    .usage('Analyze project dependencies')\n    .action(async ({ ignoreMatches, ignorePackages, ignorePatterns }) => {\n      const config: DepcheckConfig = {\n        ...(ignorePackages && { ignorePackages: ignorePackages.split(',') }),\n        ...(ignoreMatches && { ignoreMatches: ignoreMatches.split(',') }),\n        ...(ignorePatterns && { ignorePatterns: ignorePatterns.split(',') }),\n      };\n\n      await runDepcheck(config);\n    });\n}\n\nexport { defineDepcheckCommand, type DepcheckConfig };\n"
  },
  {
    "path": "scripts/vsh/src/code-workspace/index.ts",
    "content": "import type { CAC } from 'cac';\n\nimport { join, relative } from 'node:path';\n\nimport {\n  colors,\n  consola,\n  findMonorepoRoot,\n  getPackages,\n  gitAdd,\n  outputJSON,\n  prettierFormat,\n  toPosixPath,\n} from '@vben/node-utils';\n\nconst CODE_WORKSPACE_FILE = join('vben-admin.code-workspace');\n\ninterface CodeWorkspaceCommandOptions {\n  autoCommit?: boolean;\n  spaces?: number;\n}\n\nasync function createCodeWorkspace({\n  autoCommit = false,\n  spaces = 2,\n}: CodeWorkspaceCommandOptions) {\n  const { packages, rootDir } = await getPackages();\n\n  let folders = packages.map((pkg) => {\n    const { dir, packageJson } = pkg;\n    return {\n      name: packageJson.name,\n      path: toPosixPath(relative(rootDir, dir)),\n    };\n  });\n\n  folders = folders.filter(Boolean);\n\n  const monorepoRoot = findMonorepoRoot();\n  const outputPath = join(monorepoRoot, CODE_WORKSPACE_FILE);\n  await outputJSON(outputPath, { folders }, spaces);\n\n  await prettierFormat(outputPath);\n  if (autoCommit) {\n    await gitAdd(CODE_WORKSPACE_FILE, monorepoRoot);\n  }\n}\n\nasync function runCodeWorkspace({\n  autoCommit,\n  spaces,\n}: CodeWorkspaceCommandOptions) {\n  await createCodeWorkspace({\n    autoCommit,\n    spaces,\n  });\n  if (autoCommit) {\n    return;\n  }\n  consola.log('');\n  consola.success(colors.green(`${CODE_WORKSPACE_FILE} is updated!`));\n  consola.log('');\n}\n\nfunction defineCodeWorkspaceCommand(cac: CAC) {\n  cac\n    .command('code-workspace')\n    .usage('Update the `.code-workspace` file')\n    .option('--spaces [number]', '.code-workspace JSON file spaces.', {\n      default: 2,\n    })\n    .option('--auto-commit', 'auto commit .code-workspace JSON file.', {\n      default: false,\n    })\n    .action(runCodeWorkspace);\n}\n\nexport { defineCodeWorkspaceCommand };\n"
  },
  {
    "path": "scripts/vsh/src/index.ts",
    "content": "import { colors, consola } from '@vben/node-utils';\n\nimport { cac } from 'cac';\n\nimport { version } from '../package.json';\nimport { defineCheckCircularCommand } from './check-circular';\nimport { defineDepcheckCommand } from './check-dep';\nimport { defineCodeWorkspaceCommand } from './code-workspace';\nimport { defineLintCommand } from './lint';\nimport { definePubLintCommand } from './publint';\n\n// 命令描述\nconst COMMAND_DESCRIPTIONS = {\n  'check-circular': 'Check for circular dependencies',\n  'check-dep': 'Check for unused dependencies',\n  'code-workspace': 'Manage VS Code workspace settings',\n  lint: 'Run linting on the project',\n  publint: 'Check package.json files for publishing standards',\n} as const;\n\n/**\n * Initialize and run the CLI\n */\nasync function main(): Promise<void> {\n  try {\n    const vsh = cac('vsh');\n\n    // Register commands\n    defineLintCommand(vsh);\n    definePubLintCommand(vsh);\n    defineCodeWorkspaceCommand(vsh);\n    defineCheckCircularCommand(vsh);\n    defineDepcheckCommand(vsh);\n\n    // Handle invalid commands\n    vsh.on('command:*', ([cmd]) => {\n      consola.error(\n        colors.red(`Invalid command: ${cmd}`),\n        '\\n',\n        colors.yellow('Available commands:'),\n        '\\n',\n        Object.entries(COMMAND_DESCRIPTIONS)\n          .map(([cmd, desc]) => `  ${colors.cyan(cmd)} - ${desc}`)\n          .join('\\n'),\n      );\n      process.exit(1);\n    });\n\n    // Set up CLI\n    vsh.usage('vsh <command> [options]');\n    vsh.help();\n    vsh.version(version);\n\n    // Parse arguments\n    vsh.parse();\n  } catch (error) {\n    consola.error(\n      colors.red('An unexpected error occurred:'),\n      '\\n',\n      error instanceof Error ? error.message : error,\n    );\n    process.exit(1);\n  }\n}\n\n// Run the CLI\nmain().catch((error) => {\n  consola.error(\n    colors.red('Failed to start CLI:'),\n    '\\n',\n    error instanceof Error ? error.message : error,\n  );\n  process.exit(1);\n});\n"
  },
  {
    "path": "scripts/vsh/src/lint/index.ts",
    "content": "import type { CAC } from 'cac';\n\nimport { execaCommand } from '@vben/node-utils';\n\ninterface LintCommandOptions {\n  /**\n   * Format lint problem.\n   */\n  format?: boolean;\n}\n\nasync function runLint({ format }: LintCommandOptions) {\n  // process.env.FORCE_COLOR = '3';\n\n  if (format) {\n    await execaCommand(`stylelint \"**/*.{vue,css,less,scss}\" --cache --fix`, {\n      stdio: 'inherit',\n    });\n    await execaCommand(`eslint . --cache --fix`, {\n      stdio: 'inherit',\n    });\n    await execaCommand(`prettier . --write --cache --log-level warn`, {\n      stdio: 'inherit',\n    });\n    return;\n  }\n  await Promise.all([\n    execaCommand(`eslint . --cache`, {\n      stdio: 'inherit',\n    }),\n    execaCommand(`prettier . --ignore-unknown --check --cache`, {\n      stdio: 'inherit',\n    }),\n    execaCommand(`stylelint \"**/*.{vue,css,less,scss}\" --cache`, {\n      stdio: 'inherit',\n    }),\n  ]);\n}\n\nfunction defineLintCommand(cac: CAC) {\n  cac\n    .command('lint')\n    .usage('Batch execute project lint check.')\n    .option('--format', 'Format lint problem.')\n    .action(runLint);\n}\n\nexport { defineLintCommand };\n"
  },
  {
    "path": "scripts/vsh/src/publint/index.ts",
    "content": "import type { CAC } from 'cac';\nimport type { Result } from 'publint';\n\nimport { basename, dirname, join } from 'node:path';\n\nimport {\n  colors,\n  consola,\n  ensureFile,\n  findMonorepoRoot,\n  generatorContentHash,\n  getPackages,\n  outputJSON,\n  readJSON,\n  UNICODE,\n} from '@vben/node-utils';\nimport { publint } from 'publint';\nimport { formatMessage } from 'publint/utils';\n\nconst CACHE_FILE = join(\n  'node_modules',\n  '.cache',\n  'publint',\n  '.pkglintcache.json',\n);\n\ninterface PubLintCommandOptions {\n  /**\n   * Only errors are checked, no program exit is performed\n   */\n  check?: boolean;\n}\n\n/**\n * Get files that require lint\n * @param files\n */\nasync function getLintFiles(files: string[] = []) {\n  const lintFiles: string[] = [];\n\n  if (files?.length > 0) {\n    return files.filter((file) => basename(file) === 'package.json');\n  }\n\n  const { packages } = await getPackages();\n\n  for (const { dir } of packages) {\n    lintFiles.push(join(dir, 'package.json'));\n  }\n  return lintFiles;\n}\n\nfunction getCacheFile() {\n  const root = findMonorepoRoot();\n  return join(root, CACHE_FILE);\n}\n\nasync function readCache(cacheFile: string) {\n  try {\n    await ensureFile(cacheFile);\n    return await readJSON(cacheFile);\n  } catch {\n    return {};\n  }\n}\n\nasync function runPublint(files: string[], { check }: PubLintCommandOptions) {\n  const lintFiles = await getLintFiles(files);\n  const cacheFile = getCacheFile();\n\n  const cacheData = await readCache(cacheFile);\n  const cache: Record<string, { hash: string; result: Result }> = cacheData;\n\n  const results = await Promise.all(\n    lintFiles.map(async (file) => {\n      try {\n        const pkgJson = await readJSON(file);\n\n        if (pkgJson.private) {\n          return null;\n        }\n\n        Reflect.deleteProperty(pkgJson, 'dependencies');\n        Reflect.deleteProperty(pkgJson, 'devDependencies');\n        Reflect.deleteProperty(pkgJson, 'peerDependencies');\n        const content = JSON.stringify(pkgJson);\n        const hash = generatorContentHash(content);\n\n        const publintResult: Result =\n          cache?.[file]?.hash === hash\n            ? (cache?.[file]?.result ?? [])\n            : await publint({\n                level: 'suggestion',\n                pkgDir: dirname(file),\n                strict: true,\n              });\n\n        cache[file] = {\n          hash,\n          result: publintResult,\n        };\n\n        return { pkgJson, pkgPath: file, publintResult };\n      } catch {\n        return null;\n      }\n    }),\n  );\n\n  await outputJSON(cacheFile, cache);\n  printResult(results, check);\n}\n\nfunction printResult(\n  results: Array<{\n    pkgJson: Record<string, number | string>;\n    pkgPath: string;\n    publintResult: Result;\n  } | null>,\n  check?: boolean,\n) {\n  let errorCount = 0;\n  let warningCount = 0;\n  let suggestionsCount = 0;\n\n  for (const result of results) {\n    if (!result) {\n      continue;\n    }\n    const { pkgJson, pkgPath, publintResult } = result;\n    const messages = publintResult?.messages ?? [];\n    if (messages?.length < 1) {\n      continue;\n    }\n\n    consola.log('');\n    consola.log(pkgPath);\n    for (const message of messages) {\n      switch (message.type) {\n        case 'error': {\n          errorCount++;\n\n          break;\n        }\n        case 'suggestion': {\n          suggestionsCount++;\n          break;\n        }\n        case 'warning': {\n          warningCount++;\n\n          break;\n        }\n        // No default\n      }\n      const ruleUrl = `https://publint.dev/rules#${message.code.toLocaleLowerCase()}`;\n      consola.log(\n        `  ${formatMessage(message, pkgJson)}${colors.dim(` ${ruleUrl}`)}`,\n      );\n    }\n  }\n\n  const totalCount = warningCount + errorCount + suggestionsCount;\n  if (totalCount > 0) {\n    consola.error(\n      colors.red(\n        `${UNICODE.FAILURE} ${totalCount} problem (${errorCount} errors, ${warningCount} warnings, ${suggestionsCount} suggestions)`,\n      ),\n    );\n    !check && process.exit(1);\n  } else {\n    consola.log(colors.green(`${UNICODE.SUCCESS} No problem`));\n  }\n}\n\nfunction definePubLintCommand(cac: CAC) {\n  cac\n    .command('publint [...files]')\n    .usage('Check if the monorepo package conforms to the publint standard.')\n    .option('--check', 'Only errors are checked, no program exit is performed.')\n    .action(runPublint);\n}\n\nexport { definePubLintCommand };\n"
  },
  {
    "path": "scripts/vsh/tsconfig.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/tsconfig\",\n  \"extends\": \"@vben/tsconfig/node.json\",\n  \"include\": [\"src\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "stylelint.config.mjs",
    "content": "export default {\n  extends: ['@vben/stylelint-config'],\n  root: true,\n};\n"
  },
  {
    "path": "turbo.json",
    "content": "{\n  \"$schema\": \"https://turbo.build/schema.json\",\n  \"globalDependencies\": [\n    \"pnpm-lock.yaml\",\n    \"**/.env.*local\",\n    \"**/tsconfig*.json\",\n    \"internal/node-utils/*.json\",\n    \"internal/node-utils/src/**/*.ts\",\n    \"internal/tailwind-config/src/**/*.ts\",\n    \"internal/vite-config/*.json\",\n    \"internal/vite-config/src/**/*.ts\",\n    \"scripts/*/src/**/*.ts\",\n    \"scripts/*/src/**/*.json\"\n  ],\n  \"globalEnv\": [\"NODE_ENV\"],\n  \"tasks\": {\n    \"build\": {\n      \"dependsOn\": [\"^build\"],\n      \"outputs\": [\n        \"dist/**\",\n        \"dist.zip\",\n        \".vitepress/dist.zip\",\n        \".vitepress/dist/**\"\n      ]\n    },\n\n    \"@vben/web-antd#build:prod\": {\n      \"dependsOn\": [\"^build\"],\n      \"outputs\": [\"dist/**\"]\n    },\n    \"@vben/web-antd#build:test\": {\n      \"dependsOn\": [\"^build\"],\n      \"outputs\": [\"dist/**\"]\n    },\n\n    \"preview\": {\n      \"dependsOn\": [\"^build\"],\n      \"outputs\": [\"dist/**\"]\n    },\n    \"build:analyze\": {\n      \"dependsOn\": [\"^build\"],\n      \"outputs\": [\"dist/**\"]\n    },\n    \"@vben/backend-mock#build\": {\n      \"dependsOn\": [\"^build\"],\n      \"outputs\": [\".nitro/**\", \".output/**\"]\n    },\n    \"test:e2e\": {},\n    \"dev\": {\n      \"dependsOn\": [],\n      \"outputs\": [],\n      \"cache\": false,\n      \"persistent\": true\n    },\n    \"typecheck\": {\n      \"outputs\": []\n    }\n  }\n}\n"
  }
]