diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c5451e2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +# 依赖相关 +node_modules +.pnpm-store/ + +# 测试与打包 +coverage/ + +# 编辑器 +.vscode/* + +# docker构建 +Dockerfile +.dockerignore + +# CI(等资源够了再推送,暂时也不关闭CI webhook) +.drone.yml diff --git a/FOLDER_STRUCTURE.md b/FOLDER_STRUCTURE.md new file mode 100644 index 0000000..348239f --- /dev/null +++ b/FOLDER_STRUCTURE.md @@ -0,0 +1,58 @@ + . + ├─ node_modules + │   ├─ @element-plus + │   │   └─ icons-vue + │   ├─ @eslint + │   │   └─ js + │   ├─ @typescript-eslint + │   │   ├─ eslint-plugin + │   │   └─ parser + │   ├─ @vitejs + │   │   └─ plugin-vue + │   ├─ @vitest + │   │   └─ coverage-v8 + │   ├─ @vue + │   │   ├─ eslint-config-typescript + │   │   └─ test-utils + │   ├─ element-plus + │   ├─ eslint + │   ├─ eslint-plugin-vue + │   ├─ jsdom + │   ├─ sass + │   ├─ typescript + │   ├─ vite + │   ├─ vitest + │   ├─ vue + │   └─ vue-tsc + ├─ src + │   ├─ components + │   │   ├─ Avatar + │   │   │   └─ __snapshots__ + │   │   ├─ Button + │   │   │   └─ __snapshots__ + │   │   ├─ ChatBubble + │   │   │   └─ __snapshots__ + │   │   ├─ Container + │   │   │   └─ __snapshots__ + │   │   ├─ Divider + │   │   │   └─ __snapshots__ + │   │   ├─ Empty + │   │   │   └─ __snapshots__ + │   │   ├─ Grid + │   │   │   └─ __snapshots__ + │   │   ├─ Icon + │   │   │   └─ __snapshots__ + │   │   ├─ Image + │   │   │   └─ __snapshots__ + │   │   ├─ Loading + │   │   │   └─ __snapshots__ + │   │   ├─ Message + │   │   ├─ Notification + │   │   ├─ Result + │   │   ├─ Space + │   │   ├─ Tag + │   │   └─ Text + │   └─ styles + └─ test + + 55 directories diff --git a/README.md b/README.md new file mode 100644 index 0000000..4f65596 --- /dev/null +++ b/README.md @@ -0,0 +1,87 @@ +# KnowAI UI + +## 概述 + +KnowAI UI 是一个基于 Vue 3 和 TypeScript 的企业级 UI 组件库,专为 AI 应用场景设计。它提供了一套美观、易用、可定制的组件,帮助开发者快速构建现代化的 AI 应用界面。 + +## 模块分类 + +### 组件分类设计 + +- **基础层**:提供核心组件和基础功能,如按钮、图标、文本等 +- **布局层**:提供页面布局相关组件,如容器、栅格、间距等 +- **反馈层**:提供用户交互反馈相关组件,如消息、通知、加载等 + +### 样式系统架构 + +为了实现高度的可定制性和一致性,样式系统采用以下架构: +- 基于 SCSS 的变量系统,支持主题定制 +- 模块化的样式文件组织,便于维护和扩展 +- 统一的设计规范,确保组件视觉一致性 +- 支持响应式设计和暗黑模式 + +## 核心模块 + +### 组件模块 + +组件模块提供丰富的 UI 组件,满足各种应用场景需求: + +#### 基础组件 +- **Button** - 灵活的按钮组件,支持多种样式和状态 +- **Icon** - 丰富的图标系统,支持自定义和动态加载 +- **Text** - 文本组件,支持多种格式和样式控制 +- **Divider** - 分割线组件,用于内容分隔 +- **Avatar** - 头像组件,支持多种图片源和样式 +- **Tag** - 标签组件,用于内容分类和标记 +- **Image** - 图片组件,支持懒加载和错误处理 + +#### 布局组件 +- **Container** - 布局容器,提供基础的页面布局结构 +- **Grid** - 栅格系统,支持响应式布局和灵活的列配置 +- **Space** - 间距组件,提供统一的元素间距管理 + +#### 反馈组件 +- **ChatBubble** - 聊天气泡组件,适用于对话界面 +- **Message** - 全局提示组件,提供操作反馈 +- **Notification** - 通知提醒组件,用于系统通知 +- **Loading** - 加载中组件,提供加载状态指示 +- **Result** - 结果页组件,用于操作结果展示 +- **Empty** - 空状态组件,用于空数据展示 + +#### 核心功能 +- **主题系统**:支持明亮/暗黑主题切换和自定义主题 +- **变量系统**:基于 SCSS 的变量管理,统一控制颜色、尺寸等 +- **工具类**:提供常用的辅助样式类,如布局、文本、动画等 + +#### 架构特点 +- 支持 CSS 变量,便于运行时主题切换 +- 模块化的样式组织,按需加载 +- 响应式设计,适配不同设备尺寸 +- 统一的设计语言,确保组件间视觉一致性 + +## 目录结构 + +``` +knowai-ui/ +├── src/ # 源代码目录 +│ ├── components/ # 组件目录 +│ ├── styles/ # 样式文件 +│ └── index.ts # 主入口文件 +├── test/ # 测试文件 +├── .gitignore # Git 忽略配置 +├── Dockerfile # Docker 构建配置 +├── README.md # 项目说明文档 +├── eslint.config.js # ESLint 配置 +├── package.json # 项目配置 +├── pnpm-lock.yaml # 依赖锁定文件 +├── tsconfig.json # TypeScript 配置 +├── tsconfig.node.json # Node 环境 TypeScript 配置 +├── vite.config.ts # Vite 构建配置 +└── vitest.config.ts # Vitest 测试配置 +``` + +## 更新记录 + +### 2025-11-14 +- 完成tsc eslint测试,因框架内部通信问题放弃Vitest测试(但保留测试文件),推送1.0.0镜像。 + diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..9614c1b --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,105 @@ +import js from '@eslint/js'; +import typescript from '@typescript-eslint/eslint-plugin'; +import typescriptParser from '@typescript-eslint/parser'; +import vueEslintPlugin from 'eslint-plugin-vue'; + +export default [ + js.configs.recommended, + ...vueEslintPlugin.configs['flat/recommended'], + { + ignores: [ + '**/*.test.ts', + '**/*.spec.ts', + 'test/**', + 'dist/**', + 'node_modules/**', + 'coverage/**', + '*.config.js', + '*.config.ts', + ], + }, + // Vue文件规则 + { + files: ['**/*.vue'], + languageOptions: { + parser: vueEslintPlugin.parser, + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + parser: typescriptParser, + projectService: false, // 用tsc检查过了 + extraFileExtensions: ['.vue'], // 添加.vue作为额外文件扩展名 + }, + }, + plugins: { + '@typescript-eslint': typescript, + vue: vueEslintPlugin, + }, + rules: { + // Vue特定规则 + 'vue/multi-word-component-names': 'off', + 'vue/no-v-model-argument': 'error', + 'vue/script-setup-uses-vars': 'error', + + // TypeScript规则 + '@typescript-eslint/no-unused-vars': ['error', { + argsIgnorePattern: '^_', + varsIgnorePattern: '^[A-Z_]+$' + }], + + // 通用规则 + 'no-unused-vars': 'off', + 'no-console': 'warn', + 'no-debugger': 'error', + 'no-var': 'error', + 'prefer-const': 'error', + + // 代码风格 + 'indent': ['error', 2, { SwitchCase: 1 }], + 'quotes': ['error', 'single', { avoidEscape: true }], + 'semi': ['error', 'always'], + 'comma-dangle': ['error', 'never'], + 'eol-last': ['warn', 'always'], + 'no-trailing-spaces': 'warn', + }, + }, + // TypeScript文件规则(排除测试文件) + { + files: ['**/*.ts', '**/*.tsx'], + ignores: ['**/*.test.ts', '**/*.spec.ts', 'test/**/*.ts'], + languageOptions: { + parser: typescriptParser, + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + projectService: false, // 禁用projectService以提高性能 + }, + }, + plugins: { + '@typescript-eslint': typescript, + }, + rules: { + 'no-unused-vars': 'off', + '@typescript-eslint/no-unused-vars': ['error', { + argsIgnorePattern: '^_', + varsIgnorePattern: '^[A-Z_]+$' + }], + '@typescript-eslint/no-explicit-any': 'warn', + '@typescript-eslint/no-non-null-assertion': 'warn', + + // 通用规则 + 'no-console': 'warn', + 'no-debugger': 'error', + 'no-var': 'error', + 'prefer-const': 'error', + + // 代码风格 + 'indent': ['error', 2, { SwitchCase: 1 }], + 'quotes': ['error', 'single', { avoidEscape: true }], + 'semi': ['error', 'always'], + 'comma-dangle': ['error', 'never'], + 'eol-last': ['warn', 'always'], + 'no-trailing-spaces': 'warn', + }, + }, +]; \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..861bdd3 --- /dev/null +++ b/package.json @@ -0,0 +1,43 @@ +{ + "name": "knowai-ui", + "version": "1.0.0", + "description": "KnowAI UI组件库", + "main": "index.ts", + "type": "module", + "scripts": { + "build": "vite build", + "dev": "vite", + "preview": "vite preview", + "test": "vitest", + "type-check": "vue-tsc --noEmit", + "lint": "eslint ." + }, + "keywords": ["vue", "vue3", "ui", "components"], + "author": "KnowAI Team", + "license": "MIT", + "dependencies": {}, + "devDependencies": { + "vue": "^3.4.27", + "element-plus": "^2.7.0", + "@element-plus/icons-vue": "^2.3.2", + "@vitejs/plugin-vue": "^4.5.2", + "@vue/test-utils": "^2.4.6", + "vitest": "^0.34.6", + "@vitest/coverage-v8": "^0.34.6", + "vite": "^4.5.0", + "jsdom": "^22.1.0", + "sass": "^1.94.0", + "typescript": "^5.9.3", + "vue-tsc": "^2.2.12", + "eslint": "^9.39.1", + "@eslint/js": "^9.39.1", + "@typescript-eslint/eslint-plugin": "^8.46.4", + "@typescript-eslint/parser": "^8.46.4", + "@vue/eslint-config-typescript": "^14.1.0", + "eslint-plugin-vue": "^9.33.0" + }, + "peerDependencies": { + "vue": "^3.5.0", + "@element-plus/icons-vue": "^2.3.2" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..bc96473 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,3449 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + '@element-plus/icons-vue': + specifier: ^2.3.2 + version: 2.3.2(vue@3.5.24(typescript@5.9.3)) + '@eslint/js': + specifier: ^9.39.1 + version: 9.39.1 + '@typescript-eslint/eslint-plugin': + specifier: ^8.46.4 + version: 8.46.4(@typescript-eslint/parser@8.46.4(eslint@9.39.1)(typescript@5.9.3))(eslint@9.39.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^8.46.4 + version: 8.46.4(eslint@9.39.1)(typescript@5.9.3) + '@vitejs/plugin-vue': + specifier: ^4.5.2 + version: 4.6.2(vite@4.5.14(@types/node@20.19.25)(sass@1.94.0))(vue@3.5.24(typescript@5.9.3)) + '@vitest/coverage-v8': + specifier: ^0.34.6 + version: 0.34.6(vitest@0.34.6(jsdom@22.1.0)(sass@1.94.0)) + '@vue/eslint-config-typescript': + specifier: ^14.1.0 + version: 14.6.0(eslint-plugin-vue@9.33.0(eslint@9.39.1))(eslint@9.39.1)(typescript@5.9.3) + '@vue/test-utils': + specifier: ^2.4.6 + version: 2.4.6 + element-plus: + specifier: ^2.7.0 + version: 2.11.7(vue@3.5.24(typescript@5.9.3)) + eslint: + specifier: ^9.39.1 + version: 9.39.1 + eslint-plugin-vue: + specifier: ^9.33.0 + version: 9.33.0(eslint@9.39.1) + jsdom: + specifier: ^22.1.0 + version: 22.1.0 + sass: + specifier: ^1.94.0 + version: 1.94.0 + typescript: + specifier: ^5.9.3 + version: 5.9.3 + vite: + specifier: ^4.5.0 + version: 4.5.14(@types/node@20.19.25)(sass@1.94.0) + vitest: + specifier: ^0.34.6 + version: 0.34.6(jsdom@22.1.0)(sass@1.94.0) + vue: + specifier: ^3.4.27 + version: 3.5.24(typescript@5.9.3) + vue-tsc: + specifier: ^2.2.12 + version: 2.2.12(typescript@5.9.3) + +packages: + + '@ampproject/remapping@2.3.0': + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} + engines: {node: '>=6.0.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.28.5': + resolution: {integrity: sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/types@7.28.5': + resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==} + engines: {node: '>=6.9.0'} + + '@bcoe/v8-coverage@0.2.3': + resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} + + '@ctrl/tinycolor@3.6.1': + resolution: {integrity: sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==} + engines: {node: '>=10'} + + '@element-plus/icons-vue@2.3.2': + resolution: {integrity: sha512-OzIuTaIfC8QXEPmJvB4Y4kw34rSXdCJzxcD1kFStBvr8bK6X1zQAYDo0CNMjojnfTqRQCJ0I7prlErcoRiET2A==} + peerDependencies: + vue: ^3.2.0 + + '@esbuild/android-arm64@0.18.20': + resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.18.20': + resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.18.20': + resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.18.20': + resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.18.20': + resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.18.20': + resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.18.20': + resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.18.20': + resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.18.20': + resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.18.20': + resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.18.20': + resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.18.20': + resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.18.20': + resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.18.20': + resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.18.20': + resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.18.20': + resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-x64@0.18.20': + resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-x64@0.18.20': + resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.18.20': + resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.18.20': + resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.18.20': + resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.18.20': + resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@eslint-community/eslint-utils@4.9.0': + resolution: {integrity: sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.21.1': + resolution: {integrity: sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/config-helpers@0.4.2': + resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.17.0': + resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.3.1': + resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.39.1': + resolution: {integrity: sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.7': + resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.4.1': + resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@floating-ui/core@1.7.3': + resolution: {integrity: sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==} + + '@floating-ui/dom@1.7.4': + resolution: {integrity: sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==} + + '@floating-ui/utils@0.2.10': + resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} + + '@humanfs/core@0.19.1': + resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.7': + resolution: {integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + + '@istanbuljs/schema@0.1.3': + resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} + engines: {node: '>=8'} + + '@jest/schemas@29.6.3': + resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@one-ini/wasm@0.1.1': + resolution: {integrity: sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==} + + '@parcel/watcher-android-arm64@2.5.1': + resolution: {integrity: sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [android] + + '@parcel/watcher-darwin-arm64@2.5.1': + resolution: {integrity: sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [darwin] + + '@parcel/watcher-darwin-x64@2.5.1': + resolution: {integrity: sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [darwin] + + '@parcel/watcher-freebsd-x64@2.5.1': + resolution: {integrity: sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [freebsd] + + '@parcel/watcher-linux-arm-glibc@2.5.1': + resolution: {integrity: sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@parcel/watcher-linux-arm-musl@2.5.1': + resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + libc: [musl] + + '@parcel/watcher-linux-arm64-glibc@2.5.1': + resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@parcel/watcher-linux-arm64-musl@2.5.1': + resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@parcel/watcher-linux-x64-glibc@2.5.1': + resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@parcel/watcher-linux-x64-musl@2.5.1': + resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@parcel/watcher-win32-arm64@2.5.1': + resolution: {integrity: sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [win32] + + '@parcel/watcher-win32-ia32@2.5.1': + resolution: {integrity: sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==} + engines: {node: '>= 10.0.0'} + cpu: [ia32] + os: [win32] + + '@parcel/watcher-win32-x64@2.5.1': + resolution: {integrity: sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [win32] + + '@parcel/watcher@2.5.1': + resolution: {integrity: sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==} + engines: {node: '>= 10.0.0'} + + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + + '@sinclair/typebox@0.27.8': + resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} + + '@sxzz/popperjs-es@2.11.7': + resolution: {integrity: sha512-Ccy0NlLkzr0Ex2FKvh2X+OyERHXJ88XJ1MXtsI9y9fGexlaXaVTPzBCRBwIxFkORuOb+uBqeu+RqnpgYTEZRUQ==} + + '@tootallnate/once@2.0.0': + resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==} + engines: {node: '>= 10'} + + '@types/chai-subset@1.3.6': + resolution: {integrity: sha512-m8lERkkQj+uek18hXOZuec3W/fCRTrU4hrnXjH3qhHy96ytuPaPiWGgu7sJb7tZxZonO75vYAjCvpe/e4VUwRw==} + peerDependencies: + '@types/chai': <5.2.0 + + '@types/chai@4.3.20': + resolution: {integrity: sha512-/pC9HAB5I/xMlc5FP77qjCnI16ChlJfW0tGa0IUcFn38VJrTV6DeZ60NU5KZBtaOZqjdpwTWohz5HU1RrhiYxQ==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/istanbul-lib-coverage@2.0.6': + resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/lodash-es@4.17.12': + resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==} + + '@types/lodash@4.17.20': + resolution: {integrity: sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==} + + '@types/node@20.19.25': + resolution: {integrity: sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ==} + + '@types/web-bluetooth@0.0.16': + resolution: {integrity: sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ==} + + '@typescript-eslint/eslint-plugin@8.46.4': + resolution: {integrity: sha512-R48VhmTJqplNyDxCyqqVkFSZIx1qX6PzwqgcXn1olLrzxcSBDlOsbtcnQuQhNtnNiJ4Xe5gREI1foajYaYU2Vg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.46.4 + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/parser@8.46.4': + resolution: {integrity: sha512-tK3GPFWbirvNgsNKto+UmB/cRtn6TZfyw0D6IKrW55n6Vbs7KJoZtI//kpTKzE/DUmmnAFD8/Ca46s7Obs92/w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/project-service@8.46.4': + resolution: {integrity: sha512-nPiRSKuvtTN+no/2N1kt2tUh/HoFzeEgOm9fQ6XQk4/ApGqjx0zFIIaLJ6wooR1HIoozvj2j6vTi/1fgAz7UYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/scope-manager@8.46.4': + resolution: {integrity: sha512-tMDbLGXb1wC+McN1M6QeDx7P7c0UWO5z9CXqp7J8E+xGcJuUuevWKxuG8j41FoweS3+L41SkyKKkia16jpX7CA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.46.4': + resolution: {integrity: sha512-+/XqaZPIAk6Cjg7NWgSGe27X4zMGqrFqZ8atJsX3CWxH/jACqWnrWI68h7nHQld0y+k9eTTjb9r+KU4twLoo9A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/type-utils@8.46.4': + resolution: {integrity: sha512-V4QC8h3fdT5Wro6vANk6eojqfbv5bpwHuMsBcJUJkqs2z5XnYhJzyz9Y02eUmF9u3PgXEUiOt4w4KHR3P+z0PQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/types@8.46.4': + resolution: {integrity: sha512-USjyxm3gQEePdUwJBFjjGNG18xY9A2grDVGuk7/9AkjIF1L+ZrVnwR5VAU5JXtUnBL/Nwt3H31KlRDaksnM7/w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.46.4': + resolution: {integrity: sha512-7oV2qEOr1d4NWNmpXLR35LvCfOkTNymY9oyW+lUHkmCno7aOmIf/hMaydnJBUTBMRCOGZh8YjkFOc8dadEoNGA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/utils@8.46.4': + resolution: {integrity: sha512-AbSv11fklGXV6T28dp2Me04Uw90R2iJ30g2bgLz529Koehrmkbs1r7paFqr1vPCZi7hHwYxYtxfyQMRC8QaVSg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/visitor-keys@8.46.4': + resolution: {integrity: sha512-/++5CYLQqsO9HFGLI7APrxBJYo+5OCMpViuhV8q5/Qa3o5mMrF//eQHks+PXcsAVaLdn817fMuS7zqoXNNZGaw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@vitejs/plugin-vue@4.6.2': + resolution: {integrity: sha512-kqf7SGFoG+80aZG6Pf+gsZIVvGSCKE98JbiWqcCV9cThtg91Jav0yvYFC9Zb+jKetNGF6ZKeoaxgZfND21fWKw==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + vite: ^4.0.0 || ^5.0.0 + vue: ^3.2.25 + + '@vitest/coverage-v8@0.34.6': + resolution: {integrity: sha512-fivy/OK2d/EsJFoEoxHFEnNGTg+MmdZBAVK9Ka4qhXR2K3J0DS08vcGVwzDtXSuUMabLv4KtPcpSKkcMXFDViw==} + peerDependencies: + vitest: '>=0.32.0 <1' + + '@vitest/expect@0.34.6': + resolution: {integrity: sha512-QUzKpUQRc1qC7qdGo7rMK3AkETI7w18gTCUrsNnyjjJKYiuUB9+TQK3QnR1unhCnWRC0AbKv2omLGQDF/mIjOw==} + + '@vitest/runner@0.34.6': + resolution: {integrity: sha512-1CUQgtJSLF47NnhN+F9X2ycxUP0kLHQ/JWvNHbeBfwW8CzEGgeskzNnHDyv1ieKTltuR6sdIHV+nmR6kPxQqzQ==} + + '@vitest/snapshot@0.34.6': + resolution: {integrity: sha512-B3OZqYn6k4VaN011D+ve+AA4whM4QkcwcrwaKwAbyyvS/NB1hCWjFIBQxAQQSQir9/RtyAAGuq+4RJmbn2dH4w==} + + '@vitest/spy@0.34.6': + resolution: {integrity: sha512-xaCvneSaeBw/cz8ySmF7ZwGvL0lBjfvqc1LpQ/vcdHEvpLn3Ff1vAvjw+CoGn0802l++5L/pxb7whwcWAw+DUQ==} + + '@vitest/utils@0.34.6': + resolution: {integrity: sha512-IG5aDD8S6zlvloDsnzHw0Ut5xczlF+kv2BOTo+iXfPr54Yhi5qbVOgGB1hZaVq4iJ4C/MZ2J0y15IlsV/ZcI0A==} + + '@volar/language-core@2.4.15': + resolution: {integrity: sha512-3VHw+QZU0ZG9IuQmzT68IyN4hZNd9GchGPhbD9+pa8CVv7rnoOZwo7T8weIbrRmihqy3ATpdfXFnqRrfPVK6CA==} + + '@volar/source-map@2.4.15': + resolution: {integrity: sha512-CPbMWlUN6hVZJYGcU/GSoHu4EnCHiLaXI9n8c9la6RaI9W5JHX+NqG+GSQcB0JdC2FIBLdZJwGsfKyBB71VlTg==} + + '@volar/typescript@2.4.15': + resolution: {integrity: sha512-2aZ8i0cqPGjXb4BhkMsPYDkkuc2ZQ6yOpqwAuNwUoncELqoy5fRgOQtLR9gB0g902iS0NAkvpIzs27geVyVdPg==} + + '@vue/compiler-core@3.5.24': + resolution: {integrity: sha512-eDl5H57AOpNakGNAkFDH+y7kTqrQpJkZFXhWZQGyx/5Wh7B1uQYvcWkvZi11BDhscPgj8N7XV3oRwiPnx1Vrig==} + + '@vue/compiler-dom@3.5.24': + resolution: {integrity: sha512-1QHGAvs53gXkWdd3ZMGYuvQFXHW4ksKWPG8HP8/2BscrbZ0brw183q2oNWjMrSWImYLHxHrx1ItBQr50I/q2zw==} + + '@vue/compiler-sfc@3.5.24': + resolution: {integrity: sha512-8EG5YPRgmTB+YxYBM3VXy8zHD9SWHUJLIGPhDovo3Z8VOgvP+O7UP5vl0J4BBPWYD9vxtBabzW1EuEZ+Cqs14g==} + + '@vue/compiler-ssr@3.5.24': + resolution: {integrity: sha512-trOvMWNBMQ/odMRHW7Ae1CdfYx+7MuiQu62Jtu36gMLXcaoqKvAyh+P73sYG9ll+6jLB6QPovqoKGGZROzkFFg==} + + '@vue/compiler-vue2@2.7.16': + resolution: {integrity: sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==} + + '@vue/eslint-config-typescript@14.6.0': + resolution: {integrity: sha512-UpiRY/7go4Yps4mYCjkvlIbVWmn9YvPGQDxTAlcKLphyaD77LjIu3plH4Y9zNT0GB4f3K5tMmhhtRhPOgrQ/bQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^9.10.0 + eslint-plugin-vue: ^9.28.0 || ^10.0.0 + typescript: '>=4.8.4' + peerDependenciesMeta: + typescript: + optional: true + + '@vue/language-core@2.2.12': + resolution: {integrity: sha512-IsGljWbKGU1MZpBPN+BvPAdr55YPkj2nB/TBNGNC32Vy2qLG25DYu/NBN2vNtZqdRbTRjaoYrahLrToim2NanA==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@vue/reactivity@3.5.24': + resolution: {integrity: sha512-BM8kBhtlkkbnyl4q+HiF5R5BL0ycDPfihowulm02q3WYp2vxgPcJuZO866qa/0u3idbMntKEtVNuAUp5bw4teg==} + + '@vue/runtime-core@3.5.24': + resolution: {integrity: sha512-RYP/byyKDgNIqfX/gNb2PB55dJmM97jc9wyF3jK7QUInYKypK2exmZMNwnjueWwGceEkP6NChd3D2ZVEp9undQ==} + + '@vue/runtime-dom@3.5.24': + resolution: {integrity: sha512-Z8ANhr/i0XIluonHVjbUkjvn+CyrxbXRIxR7wn7+X7xlcb7dJsfITZbkVOeJZdP8VZwfrWRsWdShH6pngMxRjw==} + + '@vue/server-renderer@3.5.24': + resolution: {integrity: sha512-Yh2j2Y4G/0/4z/xJ1Bad4mxaAk++C2v4kaa8oSYTMJBJ00/ndPuxCnWeot0/7/qafQFLh5pr6xeV6SdMcE/G1w==} + peerDependencies: + vue: 3.5.24 + + '@vue/shared@3.5.24': + resolution: {integrity: sha512-9cwHL2EsJBdi8NY22pngYYWzkTDhld6fAD6jlaeloNGciNSJL6bLpbxVgXl96X00Jtc6YWQv96YA/0sxex/k1A==} + + '@vue/test-utils@2.4.6': + resolution: {integrity: sha512-FMxEjOpYNYiFe0GkaHsnJPXFHxQ6m4t8vI/ElPGpMWxZKpmRvQ33OIrvRXemy6yha03RxhOlQuy+gZMC3CQSow==} + + '@vueuse/core@9.13.0': + resolution: {integrity: sha512-pujnclbeHWxxPRqXWmdkKV5OX4Wk4YeK7wusHqRwU0Q7EFusHoqNA/aPhB6KCh9hEqJkLAJo7bb0Lh9b+OIVzw==} + + '@vueuse/metadata@9.13.0': + resolution: {integrity: sha512-gdU7TKNAUVlXXLbaF+ZCfte8BjRJQWPCa2J55+7/h+yDtzw3vOoGQDRXzI6pyKyo6bXFT5/QoPE4hAknExjRLQ==} + + '@vueuse/shared@9.13.0': + resolution: {integrity: sha512-UrnhU+Cnufu4S6JLCPZnkWh0WwZGUp72ktOF2DFptMlOs3TOdVv8xJN53zhHGARmVOsz5KqOls09+J1NR6sBKw==} + + abab@2.0.6: + resolution: {integrity: sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==} + deprecated: Use your platform's native atob() and btoa() methods instead + + abbrev@2.0.0: + resolution: {integrity: sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn-walk@8.3.4: + resolution: {integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==} + engines: {node: '>=0.4.0'} + + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} + engines: {node: '>=0.4.0'} + hasBin: true + + agent-base@6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + + alien-signals@1.0.13: + resolution: {integrity: sha512-OGj9yyTnJEttvzhTUWuscOvtqxq5vrhF7vL9oS0xJ2mK0ItPYP1/y+vCFebfxoEyAz0++1AIwJ5CMr+Fk3nDmg==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + assertion-error@1.1.0: + resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} + + async-validator@4.2.5: + resolution: {integrity: sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + boolbase@1.0.0: + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + + brace-expansion@1.1.12: + resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + + brace-expansion@2.0.2: + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + chai@4.5.0: + resolution: {integrity: sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==} + engines: {node: '>=4'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + check-error@1.0.3: + resolution: {integrity: sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==} + + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + commander@10.0.1: + resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} + engines: {node: '>=14'} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + confbox@0.1.8: + resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + + config-chain@1.1.13: + resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + cssstyle@3.0.0: + resolution: {integrity: sha512-N4u2ABATi3Qplzf0hWbVCdjenim8F3ojEXpBDF5hBpjzW182MjNGLqfmQ0SkSPeQ+V86ZXgeH8aXj6kayd4jgg==} + engines: {node: '>=14'} + + csstype@3.1.3: + resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + + data-urls@4.0.0: + resolution: {integrity: sha512-/mMTei/JXPqvFqQtfyTowxmJVwr2PVAeCcDxyFf6LhoOu/09TX2OX3kb2wzi4DMXcfj4OItwDOnhl5oziPnT6g==} + engines: {node: '>=14'} + + dayjs@1.11.19: + resolution: {integrity: sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==} + + de-indent@1.0.2: + resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + + deep-eql@4.1.4: + resolution: {integrity: sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==} + engines: {node: '>=6'} + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + detect-libc@1.0.3: + resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==} + engines: {node: '>=0.10'} + hasBin: true + + diff-sequences@29.6.3: + resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + domexception@4.0.0: + resolution: {integrity: sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==} + engines: {node: '>=12'} + deprecated: Use your platform's native DOMException instead + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + + editorconfig@1.0.4: + resolution: {integrity: sha512-L9Qe08KWTlqYMVvMcTIvMAdl1cDUubzRNYL+WfA4bLDMHe4nemKkpmYzkznE1FwLKu0EEmy6obgQKzMJrg4x9Q==} + engines: {node: '>=14'} + hasBin: true + + element-plus@2.11.7: + resolution: {integrity: sha512-Bh47wuzsqaNBNDkbtlOlZER1cGcOB8GsXp/+C9b95MOrk0wvoHUV4NKKK7xMkfYNFYdYysQ752oMhnExgAL6+g==} + peerDependencies: + vue: ^3.2.0 + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + entities@6.0.1: + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} + engines: {node: '>=0.12'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + esbuild@0.18.20: + resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==} + engines: {node: '>=12'} + hasBin: true + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-plugin-vue@9.33.0: + resolution: {integrity: sha512-174lJKuNsuDIlLpjeXc5E2Tss8P44uIimAfGD0b90k0NoirJqpG7stLuU9Vp/9ioTOrQdWVREc4mRd1BD+CvGw==} + engines: {node: ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.2.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 + + eslint-scope@7.2.2: + resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint@9.39.1: + resolution: {integrity: sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + espree@9.6.1: + resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + esquery@1.6.0: + resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fastq@1.19.1: + resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.3.3: + resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + + form-data@4.0.4: + resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==} + engines: {node: '>= 6'} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + get-func-name@2.0.2: + resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + glob@10.4.5: + resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} + hasBin: true + + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Glob versions prior to v9 are no longer supported + + globals@13.24.0: + resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} + engines: {node: '>=8'} + + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + he@1.2.0: + resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} + hasBin: true + + html-encoding-sniffer@3.0.0: + resolution: {integrity: sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==} + engines: {node: '>=12'} + + html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + + http-proxy-agent@5.0.0: + resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==} + engines: {node: '>= 6'} + + https-proxy-agent@5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + + immutable@5.1.4: + resolution: {integrity: sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + + istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + + istanbul-lib-source-maps@4.0.1: + resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==} + engines: {node: '>=10'} + + istanbul-reports@3.2.0: + resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} + engines: {node: '>=8'} + + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + + js-beautify@1.15.4: + resolution: {integrity: sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA==} + engines: {node: '>=14'} + hasBin: true + + js-cookie@3.0.5: + resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==} + engines: {node: '>=14'} + + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + hasBin: true + + jsdom@22.1.0: + resolution: {integrity: sha512-/9AVW7xNbsBv6GfWho4TTNjEo9fe6Zhf9O7s0Fhhr3u+awPwAJMKwAMXnkk5vBxflqLW9hTHX/0cs+P3gW+cQw==} + engines: {node: '>=16'} + peerDependencies: + canvas: ^2.5.0 + peerDependenciesMeta: + canvas: + optional: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + local-pkg@0.4.3: + resolution: {integrity: sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==} + engines: {node: '>=14'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash-es@4.17.21: + resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} + + lodash-unified@1.0.3: + resolution: {integrity: sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==} + peerDependencies: + '@types/lodash-es': '*' + lodash: '*' + lodash-es: '*' + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + + loupe@2.3.7: + resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==} + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + memoize-one@6.0.0: + resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + minimatch@9.0.1: + resolution: {integrity: sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==} + engines: {node: '>=16 || 14 >=14.17'} + + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + + mlly@1.8.0: + resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + muggle-string@0.4.1: + resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + node-addon-api@7.1.1: + resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + + nopt@7.2.1: + resolution: {integrity: sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + hasBin: true + + normalize-wheel-es@1.2.0: + resolution: {integrity: sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==} + + nth-check@2.1.1: + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + + nwsapi@2.2.22: + resolution: {integrity: sha512-ujSMe1OWVn55euT1ihwCI1ZcAaAU3nxUiDwfDQldc51ZXaB9m2AyOn6/jh1BLe2t/G8xd6uKG1UBF2aZJeg2SQ==} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-limit@4.0.0: + resolution: {integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse5@7.3.0: + resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + + path-browserify@1.0.1: + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + + pathe@1.1.2: + resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + pathval@1.1.1: + resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + pkg-types@1.3.1: + resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + + postcss-selector-parser@6.1.2: + resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} + engines: {node: '>=4'} + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + pretty-format@29.7.0: + resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + proto-list@1.2.4: + resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} + + psl@1.15.0: + resolution: {integrity: sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + querystringify@2.2.0: + resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + react-is@18.3.1: + resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + + requires-port@1.0.0: + resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rollup@3.29.5: + resolution: {integrity: sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==} + engines: {node: '>=14.18.0', npm: '>=8.0.0'} + hasBin: true + + rrweb-cssom@0.6.0: + resolution: {integrity: sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==} + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + sass@1.94.0: + resolution: {integrity: sha512-Dqh7SiYcaFtdv5Wvku6QgS5IGPm281L+ZtVD1U2FJa7Q0EFRlq8Z3sjYtz6gYObsYThUOz9ArwFqPZx+1azILQ==} + engines: {node: '>=14.0.0'} + hasBin: true + + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + + semver@7.7.3: + resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} + engines: {node: '>=10'} + hasBin: true + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.1.2: + resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==} + engines: {node: '>=12'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + strip-literal@1.3.0: + resolution: {integrity: sha512-PugKzOsyXpArk0yWmUwqOZecSO0GH0bPoctLcqNDH9J04pVW3lflYE0ujElBGTloevcxF5MofAOZ7C5l2b+wLg==} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + + test-exclude@6.0.0: + resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} + engines: {node: '>=8'} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinypool@0.7.0: + resolution: {integrity: sha512-zSYNUlYSMhJ6Zdou4cJwo/p7w5nmAH17GRfU/ui3ctvjXFErXXkruT4MWW6poDeXgCaIBlGLrfU6TbTXxyGMww==} + engines: {node: '>=14.0.0'} + + tinyspy@2.2.1: + resolution: {integrity: sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==} + engines: {node: '>=14.0.0'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + tough-cookie@4.1.4: + resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==} + engines: {node: '>=6'} + + tr46@4.1.1: + resolution: {integrity: sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==} + engines: {node: '>=14'} + + ts-api-utils@2.1.0: + resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + type-detect@4.1.0: + resolution: {integrity: sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==} + engines: {node: '>=4'} + + type-fest@0.20.2: + resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} + engines: {node: '>=10'} + + typescript-eslint@8.46.4: + resolution: {integrity: sha512-KALyxkpYV5Ix7UhvjTwJXZv76VWsHG+NjNlt/z+a17SOQSiOcBdUXdbJdyXi7RPxrBFECtFOiPwUJQusJuCqrg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + ufo@1.6.1: + resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==} + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + universalify@0.2.0: + resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} + engines: {node: '>= 4.0.0'} + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + url-parse@1.5.10: + resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + v8-to-istanbul@9.3.0: + resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==} + engines: {node: '>=10.12.0'} + + vite-node@0.34.6: + resolution: {integrity: sha512-nlBMJ9x6n7/Amaz6F3zJ97EBwR2FkzhBRxF5e+jE6LA3yi6Wtc2lyTij1OnDMIr34v5g/tVQtsVAzhT0jc5ygA==} + engines: {node: '>=v14.18.0'} + hasBin: true + + vite@4.5.14: + resolution: {integrity: sha512-+v57oAaoYNnO3hIu5Z/tJRZjq5aHM2zDve9YZ8HngVHbhk66RStobhb1sqPMIPEleV6cNKYK4eGrAbE9Ulbl2g==} + engines: {node: ^14.18.0 || >=16.0.0} + hasBin: true + peerDependencies: + '@types/node': '>= 14' + less: '*' + lightningcss: ^1.21.0 + sass: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + + vitest@0.34.6: + resolution: {integrity: sha512-+5CALsOvbNKnS+ZHMXtuUC7nL8/7F1F2DnHGjSsszX8zCjWSSviphCb/NuS9Nzf4Q03KyyDRBAXhF/8lffME4Q==} + engines: {node: '>=v14.18.0'} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@vitest/browser': '*' + '@vitest/ui': '*' + happy-dom: '*' + jsdom: '*' + playwright: '*' + safaridriver: '*' + webdriverio: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + playwright: + optional: true + safaridriver: + optional: true + webdriverio: + optional: true + + vscode-uri@3.1.0: + resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} + + vue-component-type-helpers@2.2.12: + resolution: {integrity: sha512-YbGqHZ5/eW4SnkPNR44mKVc6ZKQoRs/Rux1sxC6rdwXb4qpbOSYfDr9DsTHolOTGmIKgM9j141mZbBeg05R1pw==} + + vue-demi@0.14.10: + resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==} + engines: {node: '>=12'} + hasBin: true + peerDependencies: + '@vue/composition-api': ^1.0.0-rc.1 + vue: ^3.0.0-0 || ^2.6.0 + peerDependenciesMeta: + '@vue/composition-api': + optional: true + + vue-eslint-parser@10.2.0: + resolution: {integrity: sha512-CydUvFOQKD928UzZhTp4pr2vWz1L+H99t7Pkln2QSPdvmURT0MoC4wUccfCnuEaihNsu9aYYyk+bep8rlfkUXw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + + vue-eslint-parser@9.4.3: + resolution: {integrity: sha512-2rYRLWlIpaiN8xbPiDyXZXRgLGOtWxERV7ND5fFAv5qo1D2N9Fu9MNajBNc6o13lZ+24DAWCkQCvj4klgmcITg==} + engines: {node: ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: '>=6.0.0' + + vue-tsc@2.2.12: + resolution: {integrity: sha512-P7OP77b2h/Pmk+lZdJ0YWs+5tJ6J2+uOQPo7tlBnY44QqQSPYvS0qVT4wqDJgwrZaLe47etJLLQRFia71GYITw==} + hasBin: true + peerDependencies: + typescript: '>=5.0.0' + + vue@3.5.24: + resolution: {integrity: sha512-uTHDOpVQTMjcGgrqFPSb8iO2m1DUvo+WbGqoXQz8Y1CeBYQ0FXf2z1gLRaBtHjlRz7zZUBHxjVB5VTLzYkvftg==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + w3c-xmlserializer@4.0.0: + resolution: {integrity: sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==} + engines: {node: '>=14'} + + webidl-conversions@7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} + + whatwg-encoding@2.0.0: + resolution: {integrity: sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==} + engines: {node: '>=12'} + + whatwg-mimetype@3.0.0: + resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} + engines: {node: '>=12'} + + whatwg-url@12.0.1: + resolution: {integrity: sha512-Ed/LrqB8EPlGxjS+TrsXcpUond1mhccS3pchLhzSgPCnTimUCKj3IZE75pAs5m6heB2U2TMerKFUXheyHY+VDQ==} + engines: {node: '>=14'} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + ws@8.18.3: + resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + xml-name-validator@4.0.0: + resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} + engines: {node: '>=12'} + + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + yocto-queue@1.2.2: + resolution: {integrity: sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==} + engines: {node: '>=12.20'} + +snapshots: + + '@ampproject/remapping@2.3.0': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/parser@7.28.5': + dependencies: + '@babel/types': 7.28.5 + + '@babel/types@7.28.5': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@bcoe/v8-coverage@0.2.3': {} + + '@ctrl/tinycolor@3.6.1': {} + + '@element-plus/icons-vue@2.3.2(vue@3.5.24(typescript@5.9.3))': + dependencies: + vue: 3.5.24(typescript@5.9.3) + + '@esbuild/android-arm64@0.18.20': + optional: true + + '@esbuild/android-arm@0.18.20': + optional: true + + '@esbuild/android-x64@0.18.20': + optional: true + + '@esbuild/darwin-arm64@0.18.20': + optional: true + + '@esbuild/darwin-x64@0.18.20': + optional: true + + '@esbuild/freebsd-arm64@0.18.20': + optional: true + + '@esbuild/freebsd-x64@0.18.20': + optional: true + + '@esbuild/linux-arm64@0.18.20': + optional: true + + '@esbuild/linux-arm@0.18.20': + optional: true + + '@esbuild/linux-ia32@0.18.20': + optional: true + + '@esbuild/linux-loong64@0.18.20': + optional: true + + '@esbuild/linux-mips64el@0.18.20': + optional: true + + '@esbuild/linux-ppc64@0.18.20': + optional: true + + '@esbuild/linux-riscv64@0.18.20': + optional: true + + '@esbuild/linux-s390x@0.18.20': + optional: true + + '@esbuild/linux-x64@0.18.20': + optional: true + + '@esbuild/netbsd-x64@0.18.20': + optional: true + + '@esbuild/openbsd-x64@0.18.20': + optional: true + + '@esbuild/sunos-x64@0.18.20': + optional: true + + '@esbuild/win32-arm64@0.18.20': + optional: true + + '@esbuild/win32-ia32@0.18.20': + optional: true + + '@esbuild/win32-x64@0.18.20': + optional: true + + '@eslint-community/eslint-utils@4.9.0(eslint@9.39.1)': + dependencies: + eslint: 9.39.1 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/config-array@0.21.1': + dependencies: + '@eslint/object-schema': 2.1.7 + debug: 4.4.3 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.4.2': + dependencies: + '@eslint/core': 0.17.0 + + '@eslint/core@0.17.0': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.3.1': + dependencies: + ajv: 6.12.6 + debug: 4.4.3 + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.1 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@9.39.1': {} + + '@eslint/object-schema@2.1.7': {} + + '@eslint/plugin-kit@0.4.1': + dependencies: + '@eslint/core': 0.17.0 + levn: 0.4.1 + + '@floating-ui/core@1.7.3': + dependencies: + '@floating-ui/utils': 0.2.10 + + '@floating-ui/dom@1.7.4': + dependencies: + '@floating-ui/core': 1.7.3 + '@floating-ui/utils': 0.2.10 + + '@floating-ui/utils@0.2.10': {} + + '@humanfs/core@0.19.1': {} + + '@humanfs/node@0.16.7': + dependencies: + '@humanfs/core': 0.19.1 + '@humanwhocodes/retry': 0.4.3 + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.1.2 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + + '@istanbuljs/schema@0.1.3': {} + + '@jest/schemas@29.6.3': + dependencies: + '@sinclair/typebox': 0.27.8 + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.19.1 + + '@one-ini/wasm@0.1.1': {} + + '@parcel/watcher-android-arm64@2.5.1': + optional: true + + '@parcel/watcher-darwin-arm64@2.5.1': + optional: true + + '@parcel/watcher-darwin-x64@2.5.1': + optional: true + + '@parcel/watcher-freebsd-x64@2.5.1': + optional: true + + '@parcel/watcher-linux-arm-glibc@2.5.1': + optional: true + + '@parcel/watcher-linux-arm-musl@2.5.1': + optional: true + + '@parcel/watcher-linux-arm64-glibc@2.5.1': + optional: true + + '@parcel/watcher-linux-arm64-musl@2.5.1': + optional: true + + '@parcel/watcher-linux-x64-glibc@2.5.1': + optional: true + + '@parcel/watcher-linux-x64-musl@2.5.1': + optional: true + + '@parcel/watcher-win32-arm64@2.5.1': + optional: true + + '@parcel/watcher-win32-ia32@2.5.1': + optional: true + + '@parcel/watcher-win32-x64@2.5.1': + optional: true + + '@parcel/watcher@2.5.1': + dependencies: + detect-libc: 1.0.3 + is-glob: 4.0.3 + micromatch: 4.0.8 + node-addon-api: 7.1.1 + optionalDependencies: + '@parcel/watcher-android-arm64': 2.5.1 + '@parcel/watcher-darwin-arm64': 2.5.1 + '@parcel/watcher-darwin-x64': 2.5.1 + '@parcel/watcher-freebsd-x64': 2.5.1 + '@parcel/watcher-linux-arm-glibc': 2.5.1 + '@parcel/watcher-linux-arm-musl': 2.5.1 + '@parcel/watcher-linux-arm64-glibc': 2.5.1 + '@parcel/watcher-linux-arm64-musl': 2.5.1 + '@parcel/watcher-linux-x64-glibc': 2.5.1 + '@parcel/watcher-linux-x64-musl': 2.5.1 + '@parcel/watcher-win32-arm64': 2.5.1 + '@parcel/watcher-win32-ia32': 2.5.1 + '@parcel/watcher-win32-x64': 2.5.1 + optional: true + + '@pkgjs/parseargs@0.11.0': + optional: true + + '@sinclair/typebox@0.27.8': {} + + '@sxzz/popperjs-es@2.11.7': {} + + '@tootallnate/once@2.0.0': {} + + '@types/chai-subset@1.3.6(@types/chai@4.3.20)': + dependencies: + '@types/chai': 4.3.20 + + '@types/chai@4.3.20': {} + + '@types/estree@1.0.8': {} + + '@types/istanbul-lib-coverage@2.0.6': {} + + '@types/json-schema@7.0.15': {} + + '@types/lodash-es@4.17.12': + dependencies: + '@types/lodash': 4.17.20 + + '@types/lodash@4.17.20': {} + + '@types/node@20.19.25': + dependencies: + undici-types: 6.21.0 + + '@types/web-bluetooth@0.0.16': {} + + '@typescript-eslint/eslint-plugin@8.46.4(@typescript-eslint/parser@8.46.4(eslint@9.39.1)(typescript@5.9.3))(eslint@9.39.1)(typescript@5.9.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.46.4(eslint@9.39.1)(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.46.4 + '@typescript-eslint/type-utils': 8.46.4(eslint@9.39.1)(typescript@5.9.3) + '@typescript-eslint/utils': 8.46.4(eslint@9.39.1)(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.46.4 + eslint: 9.39.1 + graphemer: 1.4.0 + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.1.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.46.4(eslint@9.39.1)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.46.4 + '@typescript-eslint/types': 8.46.4 + '@typescript-eslint/typescript-estree': 8.46.4(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.46.4 + debug: 4.4.3 + eslint: 9.39.1 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.46.4(typescript@5.9.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.46.4(typescript@5.9.3) + '@typescript-eslint/types': 8.46.4 + debug: 4.4.3 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.46.4': + dependencies: + '@typescript-eslint/types': 8.46.4 + '@typescript-eslint/visitor-keys': 8.46.4 + + '@typescript-eslint/tsconfig-utils@8.46.4(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + + '@typescript-eslint/type-utils@8.46.4(eslint@9.39.1)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/types': 8.46.4 + '@typescript-eslint/typescript-estree': 8.46.4(typescript@5.9.3) + '@typescript-eslint/utils': 8.46.4(eslint@9.39.1)(typescript@5.9.3) + debug: 4.4.3 + eslint: 9.39.1 + ts-api-utils: 2.1.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.46.4': {} + + '@typescript-eslint/typescript-estree@8.46.4(typescript@5.9.3)': + dependencies: + '@typescript-eslint/project-service': 8.46.4(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.46.4(typescript@5.9.3) + '@typescript-eslint/types': 8.46.4 + '@typescript-eslint/visitor-keys': 8.46.4 + debug: 4.4.3 + fast-glob: 3.3.3 + is-glob: 4.0.3 + minimatch: 9.0.5 + semver: 7.7.3 + ts-api-utils: 2.1.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.46.4(eslint@9.39.1)(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1) + '@typescript-eslint/scope-manager': 8.46.4 + '@typescript-eslint/types': 8.46.4 + '@typescript-eslint/typescript-estree': 8.46.4(typescript@5.9.3) + eslint: 9.39.1 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.46.4': + dependencies: + '@typescript-eslint/types': 8.46.4 + eslint-visitor-keys: 4.2.1 + + '@vitejs/plugin-vue@4.6.2(vite@4.5.14(@types/node@20.19.25)(sass@1.94.0))(vue@3.5.24(typescript@5.9.3))': + dependencies: + vite: 4.5.14(@types/node@20.19.25)(sass@1.94.0) + vue: 3.5.24(typescript@5.9.3) + + '@vitest/coverage-v8@0.34.6(vitest@0.34.6(jsdom@22.1.0)(sass@1.94.0))': + dependencies: + '@ampproject/remapping': 2.3.0 + '@bcoe/v8-coverage': 0.2.3 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 4.0.1 + istanbul-reports: 3.2.0 + magic-string: 0.30.21 + picocolors: 1.1.1 + std-env: 3.10.0 + test-exclude: 6.0.0 + v8-to-istanbul: 9.3.0 + vitest: 0.34.6(jsdom@22.1.0)(sass@1.94.0) + transitivePeerDependencies: + - supports-color + + '@vitest/expect@0.34.6': + dependencies: + '@vitest/spy': 0.34.6 + '@vitest/utils': 0.34.6 + chai: 4.5.0 + + '@vitest/runner@0.34.6': + dependencies: + '@vitest/utils': 0.34.6 + p-limit: 4.0.0 + pathe: 1.1.2 + + '@vitest/snapshot@0.34.6': + dependencies: + magic-string: 0.30.21 + pathe: 1.1.2 + pretty-format: 29.7.0 + + '@vitest/spy@0.34.6': + dependencies: + tinyspy: 2.2.1 + + '@vitest/utils@0.34.6': + dependencies: + diff-sequences: 29.6.3 + loupe: 2.3.7 + pretty-format: 29.7.0 + + '@volar/language-core@2.4.15': + dependencies: + '@volar/source-map': 2.4.15 + + '@volar/source-map@2.4.15': {} + + '@volar/typescript@2.4.15': + dependencies: + '@volar/language-core': 2.4.15 + path-browserify: 1.0.1 + vscode-uri: 3.1.0 + + '@vue/compiler-core@3.5.24': + dependencies: + '@babel/parser': 7.28.5 + '@vue/shared': 3.5.24 + entities: 4.5.0 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + + '@vue/compiler-dom@3.5.24': + dependencies: + '@vue/compiler-core': 3.5.24 + '@vue/shared': 3.5.24 + + '@vue/compiler-sfc@3.5.24': + dependencies: + '@babel/parser': 7.28.5 + '@vue/compiler-core': 3.5.24 + '@vue/compiler-dom': 3.5.24 + '@vue/compiler-ssr': 3.5.24 + '@vue/shared': 3.5.24 + estree-walker: 2.0.2 + magic-string: 0.30.21 + postcss: 8.5.6 + source-map-js: 1.2.1 + + '@vue/compiler-ssr@3.5.24': + dependencies: + '@vue/compiler-dom': 3.5.24 + '@vue/shared': 3.5.24 + + '@vue/compiler-vue2@2.7.16': + dependencies: + de-indent: 1.0.2 + he: 1.2.0 + + '@vue/eslint-config-typescript@14.6.0(eslint-plugin-vue@9.33.0(eslint@9.39.1))(eslint@9.39.1)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/utils': 8.46.4(eslint@9.39.1)(typescript@5.9.3) + eslint: 9.39.1 + eslint-plugin-vue: 9.33.0(eslint@9.39.1) + fast-glob: 3.3.3 + typescript-eslint: 8.46.4(eslint@9.39.1)(typescript@5.9.3) + vue-eslint-parser: 10.2.0(eslint@9.39.1) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@vue/language-core@2.2.12(typescript@5.9.3)': + dependencies: + '@volar/language-core': 2.4.15 + '@vue/compiler-dom': 3.5.24 + '@vue/compiler-vue2': 2.7.16 + '@vue/shared': 3.5.24 + alien-signals: 1.0.13 + minimatch: 9.0.5 + muggle-string: 0.4.1 + path-browserify: 1.0.1 + optionalDependencies: + typescript: 5.9.3 + + '@vue/reactivity@3.5.24': + dependencies: + '@vue/shared': 3.5.24 + + '@vue/runtime-core@3.5.24': + dependencies: + '@vue/reactivity': 3.5.24 + '@vue/shared': 3.5.24 + + '@vue/runtime-dom@3.5.24': + dependencies: + '@vue/reactivity': 3.5.24 + '@vue/runtime-core': 3.5.24 + '@vue/shared': 3.5.24 + csstype: 3.1.3 + + '@vue/server-renderer@3.5.24(vue@3.5.24(typescript@5.9.3))': + dependencies: + '@vue/compiler-ssr': 3.5.24 + '@vue/shared': 3.5.24 + vue: 3.5.24(typescript@5.9.3) + + '@vue/shared@3.5.24': {} + + '@vue/test-utils@2.4.6': + dependencies: + js-beautify: 1.15.4 + vue-component-type-helpers: 2.2.12 + + '@vueuse/core@9.13.0(vue@3.5.24(typescript@5.9.3))': + dependencies: + '@types/web-bluetooth': 0.0.16 + '@vueuse/metadata': 9.13.0 + '@vueuse/shared': 9.13.0(vue@3.5.24(typescript@5.9.3)) + vue-demi: 0.14.10(vue@3.5.24(typescript@5.9.3)) + transitivePeerDependencies: + - '@vue/composition-api' + - vue + + '@vueuse/metadata@9.13.0': {} + + '@vueuse/shared@9.13.0(vue@3.5.24(typescript@5.9.3))': + dependencies: + vue-demi: 0.14.10(vue@3.5.24(typescript@5.9.3)) + transitivePeerDependencies: + - '@vue/composition-api' + - vue + + abab@2.0.6: {} + + abbrev@2.0.0: {} + + acorn-jsx@5.3.2(acorn@8.15.0): + dependencies: + acorn: 8.15.0 + + acorn-walk@8.3.4: + dependencies: + acorn: 8.15.0 + + acorn@8.15.0: {} + + agent-base@6.0.2: + dependencies: + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + ajv@6.12.6: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + alien-signals@1.0.13: {} + + ansi-regex@5.0.1: {} + + ansi-regex@6.2.2: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@5.2.0: {} + + ansi-styles@6.2.3: {} + + argparse@2.0.1: {} + + assertion-error@1.1.0: {} + + async-validator@4.2.5: {} + + asynckit@0.4.0: {} + + balanced-match@1.0.2: {} + + boolbase@1.0.0: {} + + brace-expansion@1.1.12: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.0.2: + dependencies: + balanced-match: 1.0.2 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + cac@6.7.14: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + callsites@3.1.0: {} + + chai@4.5.0: + dependencies: + assertion-error: 1.1.0 + check-error: 1.0.3 + deep-eql: 4.1.4 + get-func-name: 2.0.2 + loupe: 2.3.7 + pathval: 1.1.1 + type-detect: 4.1.0 + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + check-error@1.0.3: + dependencies: + get-func-name: 2.0.2 + + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + commander@10.0.1: {} + + concat-map@0.0.1: {} + + confbox@0.1.8: {} + + config-chain@1.1.13: + dependencies: + ini: 1.3.8 + proto-list: 1.2.4 + + convert-source-map@2.0.0: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + cssesc@3.0.0: {} + + cssstyle@3.0.0: + dependencies: + rrweb-cssom: 0.6.0 + + csstype@3.1.3: {} + + data-urls@4.0.0: + dependencies: + abab: 2.0.6 + whatwg-mimetype: 3.0.0 + whatwg-url: 12.0.1 + + dayjs@1.11.19: {} + + de-indent@1.0.2: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + decimal.js@10.6.0: {} + + deep-eql@4.1.4: + dependencies: + type-detect: 4.1.0 + + deep-is@0.1.4: {} + + delayed-stream@1.0.0: {} + + detect-libc@1.0.3: + optional: true + + diff-sequences@29.6.3: {} + + domexception@4.0.0: + dependencies: + webidl-conversions: 7.0.0 + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + eastasianwidth@0.2.0: {} + + editorconfig@1.0.4: + dependencies: + '@one-ini/wasm': 0.1.1 + commander: 10.0.1 + minimatch: 9.0.1 + semver: 7.7.3 + + element-plus@2.11.7(vue@3.5.24(typescript@5.9.3)): + dependencies: + '@ctrl/tinycolor': 3.6.1 + '@element-plus/icons-vue': 2.3.2(vue@3.5.24(typescript@5.9.3)) + '@floating-ui/dom': 1.7.4 + '@popperjs/core': '@sxzz/popperjs-es@2.11.7' + '@types/lodash': 4.17.20 + '@types/lodash-es': 4.17.12 + '@vueuse/core': 9.13.0(vue@3.5.24(typescript@5.9.3)) + async-validator: 4.2.5 + dayjs: 1.11.19 + lodash: 4.17.21 + lodash-es: 4.17.21 + lodash-unified: 1.0.3(@types/lodash-es@4.17.12)(lodash-es@4.17.21)(lodash@4.17.21) + memoize-one: 6.0.0 + normalize-wheel-es: 1.2.0 + vue: 3.5.24(typescript@5.9.3) + transitivePeerDependencies: + - '@vue/composition-api' + + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + + entities@4.5.0: {} + + entities@6.0.1: {} + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + esbuild@0.18.20: + optionalDependencies: + '@esbuild/android-arm': 0.18.20 + '@esbuild/android-arm64': 0.18.20 + '@esbuild/android-x64': 0.18.20 + '@esbuild/darwin-arm64': 0.18.20 + '@esbuild/darwin-x64': 0.18.20 + '@esbuild/freebsd-arm64': 0.18.20 + '@esbuild/freebsd-x64': 0.18.20 + '@esbuild/linux-arm': 0.18.20 + '@esbuild/linux-arm64': 0.18.20 + '@esbuild/linux-ia32': 0.18.20 + '@esbuild/linux-loong64': 0.18.20 + '@esbuild/linux-mips64el': 0.18.20 + '@esbuild/linux-ppc64': 0.18.20 + '@esbuild/linux-riscv64': 0.18.20 + '@esbuild/linux-s390x': 0.18.20 + '@esbuild/linux-x64': 0.18.20 + '@esbuild/netbsd-x64': 0.18.20 + '@esbuild/openbsd-x64': 0.18.20 + '@esbuild/sunos-x64': 0.18.20 + '@esbuild/win32-arm64': 0.18.20 + '@esbuild/win32-ia32': 0.18.20 + '@esbuild/win32-x64': 0.18.20 + + escape-string-regexp@4.0.0: {} + + eslint-plugin-vue@9.33.0(eslint@9.39.1): + dependencies: + '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1) + eslint: 9.39.1 + globals: 13.24.0 + natural-compare: 1.4.0 + nth-check: 2.1.1 + postcss-selector-parser: 6.1.2 + semver: 7.7.3 + vue-eslint-parser: 9.4.3(eslint@9.39.1) + xml-name-validator: 4.0.0 + transitivePeerDependencies: + - supports-color + + eslint-scope@7.2.2: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-scope@8.4.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint@9.39.1: + dependencies: + '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.21.1 + '@eslint/config-helpers': 0.4.2 + '@eslint/core': 0.17.0 + '@eslint/eslintrc': 3.3.1 + '@eslint/js': 9.39.1 + '@eslint/plugin-kit': 0.4.1 + '@humanfs/node': 0.16.7 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.8 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.6.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.4 + transitivePeerDependencies: + - supports-color + + espree@10.4.0: + dependencies: + acorn: 8.15.0 + acorn-jsx: 5.3.2(acorn@8.15.0) + eslint-visitor-keys: 4.2.1 + + espree@9.6.1: + dependencies: + acorn: 8.15.0 + acorn-jsx: 5.3.2(acorn@8.15.0) + eslint-visitor-keys: 3.4.3 + + esquery@1.6.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + estree-walker@2.0.2: {} + + esutils@2.0.3: {} + + fast-deep-equal@3.1.3: {} + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fastq@1.19.1: + dependencies: + reusify: 1.1.0 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.3.3 + keyv: 4.5.4 + + flatted@3.3.3: {} + + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + form-data@4.0.4: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + + fs.realpath@1.0.0: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + get-func-name@2.0.2: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + glob@10.4.5: + dependencies: + foreground-child: 3.3.1 + jackspeak: 3.4.3 + minimatch: 9.0.5 + minipass: 7.1.2 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + + globals@13.24.0: + dependencies: + type-fest: 0.20.2 + + globals@14.0.0: {} + + gopd@1.2.0: {} + + graphemer@1.4.0: {} + + has-flag@4.0.0: {} + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + he@1.2.0: {} + + html-encoding-sniffer@3.0.0: + dependencies: + whatwg-encoding: 2.0.0 + + html-escaper@2.0.2: {} + + http-proxy-agent@5.0.0: + dependencies: + '@tootallnate/once': 2.0.0 + agent-base: 6.0.2 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + https-proxy-agent@5.0.1: + dependencies: + agent-base: 6.0.2 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + + ignore@5.3.2: {} + + ignore@7.0.5: {} + + immutable@5.1.4: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + inherits@2.0.4: {} + + ini@1.3.8: {} + + is-extglob@2.1.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-number@7.0.0: {} + + is-potential-custom-element-name@1.0.1: {} + + isexe@2.0.0: {} + + istanbul-lib-coverage@3.2.2: {} + + istanbul-lib-report@3.0.1: + dependencies: + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 + + istanbul-lib-source-maps@4.0.1: + dependencies: + debug: 4.4.3 + istanbul-lib-coverage: 3.2.2 + source-map: 0.6.1 + transitivePeerDependencies: + - supports-color + + istanbul-reports@3.2.0: + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + + js-beautify@1.15.4: + dependencies: + config-chain: 1.1.13 + editorconfig: 1.0.4 + glob: 10.4.5 + js-cookie: 3.0.5 + nopt: 7.2.1 + + js-cookie@3.0.5: {} + + js-yaml@4.1.1: + dependencies: + argparse: 2.0.1 + + jsdom@22.1.0: + dependencies: + abab: 2.0.6 + cssstyle: 3.0.0 + data-urls: 4.0.0 + decimal.js: 10.6.0 + domexception: 4.0.0 + form-data: 4.0.4 + html-encoding-sniffer: 3.0.0 + http-proxy-agent: 5.0.0 + https-proxy-agent: 5.0.1 + is-potential-custom-element-name: 1.0.1 + nwsapi: 2.2.22 + parse5: 7.3.0 + rrweb-cssom: 0.6.0 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 4.1.4 + w3c-xmlserializer: 4.0.0 + webidl-conversions: 7.0.0 + whatwg-encoding: 2.0.0 + whatwg-mimetype: 3.0.0 + whatwg-url: 12.0.1 + ws: 8.18.3 + xml-name-validator: 4.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + json-buffer@3.0.1: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + local-pkg@0.4.3: {} + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash-es@4.17.21: {} + + lodash-unified@1.0.3(@types/lodash-es@4.17.12)(lodash-es@4.17.21)(lodash@4.17.21): + dependencies: + '@types/lodash-es': 4.17.12 + lodash: 4.17.21 + lodash-es: 4.17.21 + + lodash.merge@4.6.2: {} + + lodash@4.17.21: {} + + loupe@2.3.7: + dependencies: + get-func-name: 2.0.2 + + lru-cache@10.4.3: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + make-dir@4.0.0: + dependencies: + semver: 7.7.3 + + math-intrinsics@1.1.0: {} + + memoize-one@6.0.0: {} + + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.12 + + minimatch@9.0.1: + dependencies: + brace-expansion: 2.0.2 + + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.2 + + minipass@7.1.2: {} + + mlly@1.8.0: + dependencies: + acorn: 8.15.0 + pathe: 2.0.3 + pkg-types: 1.3.1 + ufo: 1.6.1 + + ms@2.1.3: {} + + muggle-string@0.4.1: {} + + nanoid@3.3.11: {} + + natural-compare@1.4.0: {} + + node-addon-api@7.1.1: + optional: true + + nopt@7.2.1: + dependencies: + abbrev: 2.0.0 + + normalize-wheel-es@1.2.0: {} + + nth-check@2.1.1: + dependencies: + boolbase: 1.0.0 + + nwsapi@2.2.22: {} + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-limit@4.0.0: + dependencies: + yocto-queue: 1.2.2 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + package-json-from-dist@1.0.1: {} + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parse5@7.3.0: + dependencies: + entities: 6.0.1 + + path-browserify@1.0.1: {} + + path-exists@4.0.0: {} + + path-is-absolute@1.0.1: {} + + path-key@3.1.1: {} + + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.2 + + pathe@1.1.2: {} + + pathe@2.0.3: {} + + pathval@1.1.1: {} + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + pkg-types@1.3.1: + dependencies: + confbox: 0.1.8 + mlly: 1.8.0 + pathe: 2.0.3 + + postcss-selector-parser@6.1.2: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prelude-ls@1.2.1: {} + + pretty-format@29.7.0: + dependencies: + '@jest/schemas': 29.6.3 + ansi-styles: 5.2.0 + react-is: 18.3.1 + + proto-list@1.2.4: {} + + psl@1.15.0: + dependencies: + punycode: 2.3.1 + + punycode@2.3.1: {} + + querystringify@2.2.0: {} + + queue-microtask@1.2.3: {} + + react-is@18.3.1: {} + + readdirp@4.1.2: {} + + requires-port@1.0.0: {} + + resolve-from@4.0.0: {} + + reusify@1.1.0: {} + + rollup@3.29.5: + optionalDependencies: + fsevents: 2.3.3 + + rrweb-cssom@0.6.0: {} + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + safer-buffer@2.1.2: {} + + sass@1.94.0: + dependencies: + chokidar: 4.0.3 + immutable: 5.1.4 + source-map-js: 1.2.1 + optionalDependencies: + '@parcel/watcher': 2.5.1 + + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + + semver@7.7.3: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + siginfo@2.0.0: {} + + signal-exit@4.1.0: {} + + source-map-js@1.2.1: {} + + source-map@0.6.1: {} + + stackback@0.0.2: {} + + std-env@3.10.0: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.2 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.1.2: + dependencies: + ansi-regex: 6.2.2 + + strip-json-comments@3.1.1: {} + + strip-literal@1.3.0: + dependencies: + acorn: 8.15.0 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + symbol-tree@3.2.4: {} + + test-exclude@6.0.0: + dependencies: + '@istanbuljs/schema': 0.1.3 + glob: 7.2.3 + minimatch: 3.1.2 + + tinybench@2.9.0: {} + + tinypool@0.7.0: {} + + tinyspy@2.2.1: {} + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + tough-cookie@4.1.4: + dependencies: + psl: 1.15.0 + punycode: 2.3.1 + universalify: 0.2.0 + url-parse: 1.5.10 + + tr46@4.1.1: + dependencies: + punycode: 2.3.1 + + ts-api-utils@2.1.0(typescript@5.9.3): + dependencies: + typescript: 5.9.3 + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + type-detect@4.1.0: {} + + type-fest@0.20.2: {} + + typescript-eslint@8.46.4(eslint@9.39.1)(typescript@5.9.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.46.4(@typescript-eslint/parser@8.46.4(eslint@9.39.1)(typescript@5.9.3))(eslint@9.39.1)(typescript@5.9.3) + '@typescript-eslint/parser': 8.46.4(eslint@9.39.1)(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.46.4(typescript@5.9.3) + '@typescript-eslint/utils': 8.46.4(eslint@9.39.1)(typescript@5.9.3) + eslint: 9.39.1 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + typescript@5.9.3: {} + + ufo@1.6.1: {} + + undici-types@6.21.0: {} + + universalify@0.2.0: {} + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + url-parse@1.5.10: + dependencies: + querystringify: 2.2.0 + requires-port: 1.0.0 + + util-deprecate@1.0.2: {} + + v8-to-istanbul@9.3.0: + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + '@types/istanbul-lib-coverage': 2.0.6 + convert-source-map: 2.0.0 + + vite-node@0.34.6(@types/node@20.19.25)(sass@1.94.0): + dependencies: + cac: 6.7.14 + debug: 4.4.3 + mlly: 1.8.0 + pathe: 1.1.2 + picocolors: 1.1.1 + vite: 4.5.14(@types/node@20.19.25)(sass@1.94.0) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - stylus + - sugarss + - supports-color + - terser + + vite@4.5.14(@types/node@20.19.25)(sass@1.94.0): + dependencies: + esbuild: 0.18.20 + postcss: 8.5.6 + rollup: 3.29.5 + optionalDependencies: + '@types/node': 20.19.25 + fsevents: 2.3.3 + sass: 1.94.0 + + vitest@0.34.6(jsdom@22.1.0)(sass@1.94.0): + dependencies: + '@types/chai': 4.3.20 + '@types/chai-subset': 1.3.6(@types/chai@4.3.20) + '@types/node': 20.19.25 + '@vitest/expect': 0.34.6 + '@vitest/runner': 0.34.6 + '@vitest/snapshot': 0.34.6 + '@vitest/spy': 0.34.6 + '@vitest/utils': 0.34.6 + acorn: 8.15.0 + acorn-walk: 8.3.4 + cac: 6.7.14 + chai: 4.5.0 + debug: 4.4.3 + local-pkg: 0.4.3 + magic-string: 0.30.21 + pathe: 1.1.2 + picocolors: 1.1.1 + std-env: 3.10.0 + strip-literal: 1.3.0 + tinybench: 2.9.0 + tinypool: 0.7.0 + vite: 4.5.14(@types/node@20.19.25)(sass@1.94.0) + vite-node: 0.34.6(@types/node@20.19.25)(sass@1.94.0) + why-is-node-running: 2.3.0 + optionalDependencies: + jsdom: 22.1.0 + transitivePeerDependencies: + - less + - lightningcss + - sass + - stylus + - sugarss + - supports-color + - terser + + vscode-uri@3.1.0: {} + + vue-component-type-helpers@2.2.12: {} + + vue-demi@0.14.10(vue@3.5.24(typescript@5.9.3)): + dependencies: + vue: 3.5.24(typescript@5.9.3) + + vue-eslint-parser@10.2.0(eslint@9.39.1): + dependencies: + debug: 4.4.3 + eslint: 9.39.1 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.6.0 + semver: 7.7.3 + transitivePeerDependencies: + - supports-color + + vue-eslint-parser@9.4.3(eslint@9.39.1): + dependencies: + debug: 4.4.3 + eslint: 9.39.1 + eslint-scope: 7.2.2 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + esquery: 1.6.0 + lodash: 4.17.21 + semver: 7.7.3 + transitivePeerDependencies: + - supports-color + + vue-tsc@2.2.12(typescript@5.9.3): + dependencies: + '@volar/typescript': 2.4.15 + '@vue/language-core': 2.2.12(typescript@5.9.3) + typescript: 5.9.3 + + vue@3.5.24(typescript@5.9.3): + dependencies: + '@vue/compiler-dom': 3.5.24 + '@vue/compiler-sfc': 3.5.24 + '@vue/runtime-dom': 3.5.24 + '@vue/server-renderer': 3.5.24(vue@3.5.24(typescript@5.9.3)) + '@vue/shared': 3.5.24 + optionalDependencies: + typescript: 5.9.3 + + w3c-xmlserializer@4.0.0: + dependencies: + xml-name-validator: 4.0.0 + + webidl-conversions@7.0.0: {} + + whatwg-encoding@2.0.0: + dependencies: + iconv-lite: 0.6.3 + + whatwg-mimetype@3.0.0: {} + + whatwg-url@12.0.1: + dependencies: + tr46: 4.1.1 + webidl-conversions: 7.0.0 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + word-wrap@1.2.5: {} + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.3 + string-width: 5.1.2 + strip-ansi: 7.1.2 + + wrappy@1.0.2: {} + + ws@8.18.3: {} + + xml-name-validator@4.0.0: {} + + xmlchars@2.2.0: {} + + yocto-queue@0.1.0: {} + + yocto-queue@1.2.2: {} diff --git a/src/components/Avatar/Avatar.test.ts b/src/components/Avatar/Avatar.test.ts new file mode 100644 index 0000000..4a05418 --- /dev/null +++ b/src/components/Avatar/Avatar.test.ts @@ -0,0 +1,191 @@ +import { describe, it, expect } from 'vitest'; +import { mount } from '@vue/test-utils'; +import KAvatar from './index.vue'; + +describe('KAvatar Component', () => { + // 基础渲染测试 + it('should render correctly', () => { + const wrapper = mount(KAvatar); + expect(wrapper.exists()).toBe(true); + expect(wrapper.classes()).toContain('k-avatar'); + }); + + // 图片类型测试 + it('should render image when src is provided', () => { + const wrapper = mount(KAvatar, { + props: { + src: 'https://example.com/avatar.jpg', + alt: 'User Avatar' + } + }); + expect(wrapper.find('.k-avatar__img').exists()).toBe(true); + expect(wrapper.find('.k-avatar__img').attributes('src')).toBe('https://example.com/avatar.jpg'); + expect(wrapper.find('.k-avatar__img').attributes('alt')).toBe('User Avatar'); + }); + + // 文字类型测试 + it('should render text when text is provided', () => { + const wrapper = mount(KAvatar, { + props: { + text: 'JD' + } + }); + expect(wrapper.find('.k-avatar__text').exists()).toBe(true); + expect(wrapper.find('.k-avatar__text').text()).toBe('JD'); + }); + + // 默认图标测试 + it('should render default icon when no src and text are provided', () => { + const wrapper = mount(KAvatar); + expect(wrapper.find('.k-avatar__icon').exists()).toBe(true); + }); + + // 尺寸属性测试 - small + it('should render small size avatar', () => { + const wrapper = mount(KAvatar, { + props: { + size: 'small' + } + }); + expect(wrapper.classes()).toContain('k-avatar--small'); + }); + + // 尺寸属性测试 - medium + it('should render medium size avatar by default', () => { + const wrapper = mount(KAvatar); + expect(wrapper.classes()).toContain('k-avatar--medium'); + }); + + // 尺寸属性测试 - large + it('should render large size avatar', () => { + const wrapper = mount(KAvatar, { + props: { + size: 'large' + } + }); + expect(wrapper.classes()).toContain('k-avatar--large'); + }); + + // 形状属性测试 - circle + it('should render circle shape avatar by default', () => { + const wrapper = mount(KAvatar); + expect(wrapper.classes()).toContain('k-avatar--circle'); + }); + + // 形状属性测试 - square + it('should render square shape avatar', () => { + const wrapper = mount(KAvatar, { + props: { + shape: 'square' + } + }); + expect(wrapper.classes()).toContain('k-avatar--square'); + }); + + // 图片适配测试 + it('should set correct object-fit style', () => { + const wrapper = mount(KAvatar, { + props: { + src: 'https://example.com/avatar.jpg', + fit: 'contain' + } + }); + const imgElement = wrapper.find('.k-avatar__img').element; + expect(imgElement.style.objectFit).toBe('contain'); + }); + + // 背景色测试 + it('should set custom background color', () => { + const wrapper = mount(KAvatar, { + props: { + text: 'JD', + bgColor: '#1890ff' + } + }); + const avatarElement = wrapper.element; + // 验证背景色属性存在 + expect(avatarElement.style.backgroundColor).toBeTruthy(); + // 由于浏览器会将十六进制转换为RGB,我们只需验证背景色已设置 + expect(avatarElement.style.backgroundColor).not.toBe(''); + }); + + // 文字颜色测试 + it('should set custom text color', () => { + const wrapper = mount(KAvatar, { + props: { + text: 'JD', + textColor: '#ffffff' + } + }); + const avatarElement = wrapper.element; + // 验证文字颜色属性存在 + expect(avatarElement.style.color).toBeTruthy(); + // 由于浏览器会将十六进制转换为RGB,我们只需验证文字颜色已设置 + expect(avatarElement.style.color).not.toBe(''); + }); + + // 图片加载错误测试 + it('should handle image loading error', async () => { + const wrapper = mount(KAvatar, { + props: { + src: 'https://example.com/nonexistent.jpg', + text: 'Error' + } + }); + + // 模拟图片加载失败 + wrapper.find('.k-avatar__img').trigger('error'); + + // 等待DOM更新 + await wrapper.vm.$nextTick(); + + expect(wrapper.classes()).toContain('k-avatar--error'); + expect(wrapper.find('.k-avatar__text').exists()).toBe(true); + expect(wrapper.find('.k-avatar__text').text()).toBe('ERROR'); // 注意:现在文本会自动转为大写 + }); + + // 图标插槽测试 + it('should render custom icon from slot', () => { + const wrapper = mount(KAvatar, { + slots: { + icon: '
Custom
' + } + }); + expect(wrapper.find('.custom-icon').exists()).toBe(true); + expect(wrapper.find('.custom-icon').text()).toBe('Custom'); + }); + + // 组合属性测试 + it('should work correctly with multiple props combined', () => { + const wrapper = mount(KAvatar, { + props: { + src: 'https://example.com/avatar.jpg', + size: 'large', + shape: 'square', + fit: 'cover', + bgColor: '#f0f2f5', + textColor: '#333' + } + }); + + expect(wrapper.classes()).toContain('k-avatar--large'); + expect(wrapper.classes()).toContain('k-avatar--square'); + + const imgElement = wrapper.find('.k-avatar__img').element; + const avatarElement = wrapper.element; + + expect(imgElement.style.objectFit).toBe('cover'); + expect(avatarElement.style.backgroundColor).toBeTruthy(); + expect(avatarElement.style.color).toBeTruthy(); + }); + + // 文本转换测试 + it('should convert text to uppercase', () => { + const wrapper = mount(KAvatar, { + props: { + text: 'jd' + } + }); + expect(wrapper.find('.k-avatar__text').text()).toBe('JD'); + }); +}); \ No newline at end of file diff --git a/src/components/Avatar/README.md b/src/components/Avatar/README.md new file mode 100644 index 0000000..d4de6dc --- /dev/null +++ b/src/components/Avatar/README.md @@ -0,0 +1,59 @@ +# Avatar 头像组件 + +Avatar组件用于显示用户头像,支持图片、文字和图标三种显示形式,并提供多种尺寸和形状选项。 + +## 参数接口 (Props) + +| 参数名 | 类型 | 默认值 | 说明 | +|--------|------|--------|------| +| src | string | '' | 头像图片路径 | +| alt | string | '' | 图片无法显示时的替代文本 | +| size | 'small' \| 'medium' \| 'large' | 'medium' | 头像尺寸 | +| shape | 'circle' \| 'square' | 'circle' | 头像形状 | +| text | string | '' | 无图片时显示的文本 | +| fit | 'fill' \| 'contain' \| 'cover' \| 'none' \| 'scale-down' | 'cover' | 图片填充方式 | +| bgColor | string | '' | 自定义背景颜色 | +| textColor | string | '' | 自定义文本颜色 | + +## 插槽 (Slots) + +| 插槽名 | 说明 | +|--------|------| +| icon | 当 `src` 为空且 `text` 为空时,自定义显示的图标内容。 | + + +## 事件 (Events) + +Avatar组件没有提供事件。 + +## 使用示例 + +```vue + + + +``` \ No newline at end of file diff --git a/src/components/Avatar/index.scss b/src/components/Avatar/index.scss new file mode 100644 index 0000000..5748558 --- /dev/null +++ b/src/components/Avatar/index.scss @@ -0,0 +1,55 @@ +// Avatar组件样式 + + +.k-avatar { + position: relative; + display: inline-flex; + align-items: center; + justify-content: center; + overflow: hidden; + background-color: #f0f2f5; + color: #333; + font-size: 14px; + user-select: none; + + &--small { + width: 32px; + height: 32px; + font-size: 12px; + } + + &--medium { + width: 40px; + height: 40px; + font-size: 14px; + } + + &--large { + width: 56px; + height: 56px; + font-size: 18px; + } + + &--circle { + border-radius: 50%; + } + + &--square { + border-radius: $border-radius-base; + } + + &__img { + width: 100%; + height: 100%; + // 图片裁剪填充 + object-fit: cover; + } + + &__text { + text-transform: uppercase; + } + + &__icon { + font-size: inherit; + } +} \ No newline at end of file diff --git a/src/components/Avatar/index.ts b/src/components/Avatar/index.ts new file mode 100644 index 0000000..46714ad --- /dev/null +++ b/src/components/Avatar/index.ts @@ -0,0 +1,5 @@ +import Avatar from './index.vue'; +export * from './types'; + +export { Avatar }; +export default Avatar; diff --git a/src/components/Avatar/index.vue b/src/components/Avatar/index.vue new file mode 100644 index 0000000..d487d0d --- /dev/null +++ b/src/components/Avatar/index.vue @@ -0,0 +1,117 @@ + + + diff --git a/src/components/Avatar/types.ts b/src/components/Avatar/types.ts new file mode 100644 index 0000000..180e93f --- /dev/null +++ b/src/components/Avatar/types.ts @@ -0,0 +1,13 @@ +export type Color = string; + +export interface AvatarProps { + src?: string; + alt?: string; + size?: 'small' | 'medium' | 'large' | number | string; + shape?: 'circle' | 'square'; + icon?: string; + text?: string; + fit?: 'fill' | 'contain' | 'cover' | 'none' | 'scale-down'; + textColor?: Color; + bgColor?: Color; +} diff --git a/src/components/Button/Button.test.ts b/src/components/Button/Button.test.ts new file mode 100644 index 0000000..7e73f72 --- /dev/null +++ b/src/components/Button/Button.test.ts @@ -0,0 +1,297 @@ +import { describe, it, expect, vi } from 'vitest'; +import { mount } from '@vue/test-utils'; +import Button from './index.vue'; + +describe('KButton Component', () => { + describe('基础渲染', () => { + it('应该正确渲染按钮组件', () => { + const wrapper = mount(Button, { + slots: { + default: '按钮' + } + }); + expect(wrapper.exists()).toBe(true); + expect(wrapper.classes()).toContain('k-button'); + expect(wrapper.text()).toBe('按钮'); + }); + + it('应该正确渲染默认类型按钮', () => { + const wrapper = mount(Button); + expect(wrapper.classes()).toContain('k-button--default'); + }); + + it('应该正确渲染默认尺寸按钮', () => { + const wrapper = mount(Button); + expect(wrapper.classes()).toContain('k-button--medium'); + }); + }); + + describe('类型属性', () => { + const types = ['primary', 'success', 'warning', 'danger', 'info', 'default']; + + types.forEach(type => { + it(`应该正确渲染${type}类型按钮`, () => { + const wrapper = mount(Button, { + props: { + type + } + }); + expect(wrapper.classes()).toContain(`k-button--${type}`); + }); + }); + }); + + describe('尺寸属性', () => { + const sizes = ['small', 'medium', 'large']; + + sizes.forEach(size => { + it(`应该正确渲染${size}尺寸按钮`, () => { + const wrapper = mount(Button, { + props: { + size + } + }); + expect(wrapper.classes()).toContain(`k-button--${size}`); + }); + }); + }); + + describe('禁用状态', () => { + it('应该正确应用禁用状态样式', () => { + const wrapper = mount(Button, { + props: { + disabled: true + } + }); + expect(wrapper.classes()).toContain('is-disabled'); + expect(wrapper.attributes('disabled')).toBeDefined(); + }); + + it('禁用状态下不应该触发点击事件', async () => { + const clickFn = vi.fn(); + const wrapper = mount(Button, { + props: { + disabled: true + }, + slots: { + default: '禁用按钮' + }, + attrs: { + onClick: clickFn + } + }); + await wrapper.trigger('click'); + expect(clickFn).not.toHaveBeenCalled(); + }); + }); + + describe('加载状态', () => { + it('应该正确应用加载状态样式', () => { + const wrapper = mount(Button, { + props: { + loading: true + } + }); + expect(wrapper.classes()).toContain('is-loading'); + expect(wrapper.attributes('disabled')).toBeDefined(); + expect(wrapper.find('.k-button__loading').exists()).toBe(true); + }); + + it('加载状态下不应该触发点击事件', async () => { + const clickFn = vi.fn(); + const wrapper = mount(Button, { + props: { + loading: true + }, + slots: { + default: '加载按钮' + }, + attrs: { + onClick: clickFn + } + }); + await wrapper.trigger('click'); + expect(clickFn).not.toHaveBeenCalled(); + }); + + it('加载状态下应该显示加载图标而不是普通图标', () => { + const wrapper = mount(Button, { + props: { + loading: true, + icon: 'k-icon-check' + } + }); + expect(wrapper.find('.k-button__loading').exists()).toBe(true); + expect(wrapper.find('.k-button__icon').exists()).toBe(false); + }); + }); + + describe('圆角和圆形样式', () => { + it('应该正确应用圆角样式', () => { + const wrapper = mount(Button, { + props: { + round: true + } + }); + expect(wrapper.classes()).toContain('is-round'); + }); + + it('应该正确应用圆形样式', () => { + const wrapper = mount(Button, { + props: { + circle: true + } + }); + expect(wrapper.classes()).toContain('is-circle'); + }); + }); + + describe('不同按钮样式', () => { + it('应该正确应用plain样式', () => { + const wrapper = mount(Button, { + props: { + plain: true + } + }); + expect(wrapper.classes()).toContain('is-plain'); + }); + + it('应该正确应用text样式', () => { + const wrapper = mount(Button, { + props: { + text: true + } + }); + expect(wrapper.classes()).toContain('is-text'); + }); + + it('应该正确应用link样式', () => { + const wrapper = mount(Button, { + props: { + link: true + } + }); + expect(wrapper.classes()).toContain('is-link'); + }); + }); + + describe('图标按钮', () => { + it('应该正确显示图标', () => { + const wrapper = mount(Button, { + props: { + icon: 'k-icon-check' + } + }); + expect(wrapper.find('.k-button__icon').exists()).toBe(true); + expect(wrapper.find('.k-button__icon').classes()).toContain('k-icon-check'); + }); + + it('应该正确显示图标和文本', () => { + const wrapper = mount(Button, { + props: { + icon: 'k-icon-check' + }, + slots: { + default: '按钮' + } + }); + expect(wrapper.find('.k-button__icon').exists()).toBe(true); + expect(wrapper.find('.k-button__text').exists()).toBe(true); + }); + + it('应该正确显示纯图标按钮', () => { + const wrapper = mount(Button, { + props: { + icon: 'k-icon-check', + circle: true + } + }); + expect(wrapper.find('.k-button__icon').exists()).toBe(true); + expect(wrapper.find('.k-button__text').exists()).toBe(false); + expect(wrapper.classes()).toContain('is-circle'); + }); + }); + + describe('原生类型', () => { + const nativeTypes = ['button', 'submit', 'reset']; + + nativeTypes.forEach(nativeType => { + it(`应该正确设置原生类型${nativeType}`, () => { + const wrapper = mount(Button, { + props: { + nativeType + } + }); + expect(wrapper.attributes('type')).toBe(nativeType); + }); + }); + + it('应该默认使用button原生类型', () => { + const wrapper = mount(Button); + expect(wrapper.attributes('type')).toBe('button'); + }); + }); + + describe('点击事件', () => { + it('应该正确触发点击事件', async () => { + const clickFn = vi.fn(); + const wrapper = mount(Button, { + slots: { + default: '点击按钮' + }, + attrs: { + onClick: clickFn + } + }); + await wrapper.trigger('click'); + expect(clickFn).toHaveBeenCalled(); + }); + + it('应该正确传递点击事件参数', async () => { + const clickFn = vi.fn(); + const wrapper = mount(Button, { + slots: { + default: '点击按钮' + }, + attrs: { + onClick: clickFn + } + }); + await wrapper.trigger('click'); + expect(clickFn).toHaveBeenCalledWith(expect.any(MouseEvent)); + }); + }); + + describe('组合属性', () => { + it('应该正确组合类型、尺寸和样式', () => { + const wrapper = mount(Button, { + props: { + type: 'primary', + size: 'large', + round: true, + plain: true + }, + slots: { + default: '组合按钮' + } + }); + expect(wrapper.classes()).toContain('k-button--primary'); + expect(wrapper.classes()).toContain('k-button--large'); + expect(wrapper.classes()).toContain('is-round'); + expect(wrapper.classes()).toContain('is-plain'); + }); + + it('应该正确组合图标、禁用和加载状态', () => { + const wrapper = mount(Button, { + props: { + icon: 'k-icon-check', + disabled: true, + loading: false + } + }); + expect(wrapper.classes()).toContain('is-disabled'); + expect(wrapper.classes()).not.toContain('is-loading'); + expect(wrapper.find('.k-button__icon').exists()).toBe(true); + }); + }); +}); \ No newline at end of file diff --git a/src/components/Button/README.md b/src/components/Button/README.md new file mode 100644 index 0000000..3663460 --- /dev/null +++ b/src/components/Button/README.md @@ -0,0 +1,54 @@ +# KButton + +`KButton` 组件是一个可定制的按钮组件,支持多种类型、尺寸、状态和样式。 + +## 使用示例 + +```vue + + + +``` + +## Props + +| 属性名 | 类型 | 默认值 | 描述 | +| ---------- | ----------------------------------------- | --------- | ---------------------------------------- | +| `type` | 'primary' | 'success' | 'warning' | 'danger' | 'info' | 'default' | 'default' | 按钮类型 | +| `size` | 'small' | 'medium' | 'large' | 'medium' | 按钮尺寸 | +| `disabled` | `boolean` | `false` | 是否禁用按钮 | +| `loading` | `boolean` | `false` | 是否显示加载状态 | +| `round` | `boolean` | `false` | 是否为圆角按钮 | +| `circle` | `boolean` | `false` | 是否为圆形按钮(通常与 `icon` 属性一起使用) | +| `plain` | `boolean` | `false` | 是否为朴素按钮 | +| `text` | `boolean` | `false` | 是否为文本按钮 | +| `link` | `boolean` | `false` | 是否为链接按钮 | +| `icon` | `string` | `''` | 按钮图标类名 | +| `autofocus`| `boolean` | `false` | 是否自动获取焦点 | +| `nativeType`| 'button' | 'submit' | 'reset' | 'button' | 按钮的原生 `type` 属性 | + +## Events + +| 事件名 | 参数 | 描述 | +| --------- | ------------ | ------------ | +| `click` | `MouseEvent` | 按钮点击事件 | + +## Slots + +| 插槽名 | 描述 | +| --------- | ------------ | +| `default` | 按钮的默认内容 | \ No newline at end of file diff --git a/src/components/Button/__snapshots__/Button.test.ts.snap b/src/components/Button/__snapshots__/Button.test.ts.snap new file mode 100644 index 0000000..9ac1aeb --- /dev/null +++ b/src/components/Button/__snapshots__/Button.test.ts.snap @@ -0,0 +1,109 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`KButton > should be disabled when disabled prop is true 1`] = ` +"" +`; + +exports[`KButton > should have autofocus attribute 1`] = ` +"" +`; + +exports[`KButton > should have nativeType submit 1`] = ` +"" +`; + +exports[`KButton > should render as circle button 1`] = ` +"" +`; + +exports[`KButton > should render as link button 1`] = ` +"" +`; + +exports[`KButton > should render as plain button 1`] = ` +"" +`; + +exports[`KButton > should render as round button 1`] = ` +"" +`; + +exports[`KButton > should render as text button 1`] = ` +"" +`; + +exports[`KButton > should render with custom icon 1`] = ` +"" +`; + +exports[`KButton > should render with default props 1`] = ` +"" +`; + +exports[`KButton > should render with large size 1`] = ` +"" +`; + +exports[`KButton > should render with primary type 1`] = ` +"" +`; + +exports[`KButton > should render with slot content 1`] = ` +"" +`; + +exports[`KButton > should show loading state when loading prop is true 1`] = ` +"" +`; diff --git a/src/components/Button/index.scss b/src/components/Button/index.scss new file mode 100644 index 0000000..48e9d8b --- /dev/null +++ b/src/components/Button/index.scss @@ -0,0 +1,325 @@ +// Button组件样式 +.k-button { + display: inline-flex; + align-items: center; + justify-content: center; + line-height: 1; + white-space: nowrap; + cursor: pointer; + background-color: #ffffff; + border: 1px solid #dcdfe6; + color: #606266; + text-align: center; + box-sizing: border-box; + outline: none; + transition: 0.1s; + font-weight: 500; + user-select: none; + vertical-align: middle; + padding: 8px 15px; + font-size: 14px; + border-radius: 4px; + + &:hover, + &:focus { + color: #409eff; + border-color: #c6e2ff; + background-color: #ecf5ff; + } + + &:active { + color: #3a8ee6; + border-color: #3a8ee6; + outline: none; + } + + &--primary { + color: #ffffff; + background-color: #409eff; + border-color: #409eff; + + &:hover, + &:focus { + background: #66b1ff; + border-color: #66b1ff; + color: #ffffff; + } + + &:active { + background: #3a8ee6; + border-color: #3a8ee6; + color: #ffffff; + } + } + + &--success { + color: #ffffff; + background-color: #67c23a; + border-color: #67c23a; + + &:hover, + &:focus { + background: #85ce61; + border-color: #85ce61; + color: #ffffff; + } + + &:active { + background: #5daf34; + border-color: #5daf34; + color: #ffffff; + } + } + + &--warning { + color: #ffffff; + background-color: #e6a23c; + border-color: #e6a23c; + + &:hover, + &:focus { + background: #ebb563; + border-color: #ebb563; + color: #ffffff; + } + + &:active { + background: #cf9236; + border-color: #cf9236; + color: #ffffff; + } + } + + &--danger { + color: #ffffff; + background-color: #f56c6c; + border-color: #f56c6c; + + &:hover, + &:focus { + background: #f78989; + border-color: #f78989; + color: #ffffff; + } + + &:active { + background: #dd6161; + border-color: #dd6161; + color: #ffffff; + } + } + + &--info { + color: #ffffff; + background-color: #909399; + border-color: #909399; + + &:hover, + &:focus { + background: #a6a9ad; + border-color: #a6a9ad; + color: #ffffff; + } + + &:active { + background: #82848a; + border-color: #82848a; + color: #ffffff; + } + } + + &--small { + padding: 5px 11px; + font-size: 12px; + border-radius: 3px; + } + + &--large { + padding: 11px 19px; + font-size: 16px; + border-radius: 4px; + } + + &--text { + border-color: transparent; + background: transparent; + padding-left: 0; + padding-right: 0; + + &:hover, + &:focus { + color: #409eff; + background-color: transparent; + border-color: transparent; + } + + &:active { + color: #3a8ee6; + background-color: transparent; + border-color: transparent; + } + } + + &--link { + border-color: transparent; + background: transparent; + padding-left: 0; + padding-right: 0; + height: auto; + text-decoration: underline; + + &:hover, + &:focus { + color: #409eff; + background-color: transparent; + border-color: transparent; + } + + &:active { + color: #3a8ee6; + background-color: transparent; + border-color: transparent; + } + } + + &--plain { + &.k-button--primary { + color: #409eff; + background: #ecf5ff; + border-color: #b3d8ff; + + &:hover, + &:focus { + background: #409eff; + color: #ffffff; + border-color: #409eff; + } + } + + &.k-button--success { + color: #67c23a; + background: #f0f9eb; + border-color: #c2e7b0; + + &:hover, + &:focus { + background: #67c23a; + color: #ffffff; + border-color: #67c23a; + } + } + + &.k-button--warning { + color: #e6a23c; + background: #fdf6ec; + border-color: #f5dab1; + + &:hover, + &:focus { + background: #e6a23c; + color: #ffffff; + border-color: #e6a23c; + } + } + + &.k-button--danger { + color: #f56c6c; + background: #fef0f0; + border-color: #fbc4c4; + + &:hover, + &:focus { + background: #f56c6c; + color: #ffffff; + border-color: #f56c6c; + } + } + + &.k-button--info { + color: #909399; + background: #f4f4f5; + border-color: #d3d4d6; + + &:hover, + &:focus { + background: #909399; + color: #ffffff; + border-color: #909399; + } + } + } + + &--round { + border-radius: 20px; + padding: 8px 23px; + } + + &--circle { + border-radius: 50%; + padding: 8px; + + &.k-button--small { + padding: 5px; + } + + &.k-button--large { + padding: 11px; + } + } + + &--text.is-disabled, + &--link.is-disabled, + &.is-disabled, + &.is-loading { + color: #c0c4cc; + cursor: not-allowed; + background-image: none; + background-color: #ffffff; + border-color: #ebeef5; + + &:hover, + &:focus { + color: #c0c4cc; + cursor: not-allowed; + background-image: none; + background-color: #ffffff; + border-color: #ebeef5; + } + } + + &--text.is-disabled, + &--link.is-disabled { + background-color: transparent !important; + border-color: transparent !important; + } + + &__loading { + pointer-events: none; + margin-right: 8px; + } + + &__icon { + margin-right: 8px; + + &:last-child { + margin-right: 0; + } + } + + &__text { + display: inline-block; + } +} + +.k-icon-loading { + animation: rotating 2s linear infinite; +} + +@keyframes rotating { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} \ No newline at end of file diff --git a/src/components/Button/index.ts b/src/components/Button/index.ts new file mode 100644 index 0000000..15d090e --- /dev/null +++ b/src/components/Button/index.ts @@ -0,0 +1,5 @@ +import Button from './index.vue'; +export * from './types'; + +export default Button; +export { Button }; diff --git a/src/components/Button/index.vue b/src/components/Button/index.vue new file mode 100644 index 0000000..32b4a1d --- /dev/null +++ b/src/components/Button/index.vue @@ -0,0 +1,114 @@ + + + + + diff --git a/src/components/Button/types.ts b/src/components/Button/types.ts new file mode 100644 index 0000000..b371f63 --- /dev/null +++ b/src/components/Button/types.ts @@ -0,0 +1,22 @@ +export type Size = 'small' | 'medium' | 'large'; +export type Type = 'primary' | 'success' | 'warning' | 'danger' | 'info'; +export type Color = string; + +export interface ButtonProps { + type?: Type | 'default'; + size?: Size; + disabled?: boolean; + loading?: boolean; + round?: boolean; + circle?: boolean; + plain?: boolean; + text?: boolean; + link?: boolean; + icon?: string; + autofocus?: boolean; + nativeType?: 'button' | 'submit' | 'reset'; +} + +export interface ButtonEmits { + (event: 'click', e: MouseEvent): void; +} diff --git a/src/components/ChatBubble/ChatBubble.test.ts b/src/components/ChatBubble/ChatBubble.test.ts new file mode 100644 index 0000000..e1a574d --- /dev/null +++ b/src/components/ChatBubble/ChatBubble.test.ts @@ -0,0 +1,289 @@ +import { describe, it, expect, mount, vi } from 'vitest'; +import KChatBubble from './index.vue'; + +// 创建测试消息数据 +const createTestMessage = (overrides = {}) => { + return { + id: '1', + type: 'text', + content: 'Hello, world!', + sender: { + name: 'Test User', + avatar: 'https://example.com/avatar.jpg', + isOnline: true + }, + timestamp: Date.now(), + status: 'read', + ...overrides + }; +}; + +describe('KChatBubble Component', () => { + // 基础渲染测试 + it('should render correctly', () => { + const wrapper = mount(KChatBubble, { + props: { + message: createTestMessage() + } + }); + expect(wrapper.exists()).toBe(true); + expect(wrapper.classes()).toContain('k-chat-bubble'); + }); + + // 他人消息测试 + it('should render other user message correctly', () => { + const wrapper = mount(KChatBubble, { + props: { + message: createTestMessage(), + isSelf: false + } + }); + expect(wrapper.classes()).toContain('k-chat-bubble--other'); + expect(wrapper.find('.k-chat-bubble__bubble').classes()).toContain('k-chat-bubble__bubble--other'); + }); + + // 自己消息测试 + it('should render own message correctly', () => { + const wrapper = mount(KChatBubble, { + props: { + message: createTestMessage(), + isSelf: true + } + }); + expect(wrapper.classes()).toContain('k-chat-bubble--own'); + expect(wrapper.find('.k-chat-bubble__bubble').classes()).toContain('k-chat-bubble__bubble--own'); + }); + + // 消息内容测试 + it('should display correct message content', () => { + const message = createTestMessage({ content: 'Test message content' }); + const wrapper = mount(KChatBubble, { + props: { + message + } + }); + expect(wrapper.find('.k-chat-bubble__text').text()).toBe('Test message content'); + }); + + // 头像显示测试 + it('should display avatar when showAvatar is true', () => { + const wrapper = mount(KChatBubble, { + props: { + message: createTestMessage(), + showAvatar: true + } + }); + expect(wrapper.find('.k-chat-bubble__avatar').exists()).toBe(true); + expect(wrapper.find('.k-avatar').exists()).toBe(true); + }); + + // 头像隐藏测试 + it('should not display avatar when showAvatar is false', () => { + const wrapper = mount(KChatBubble, { + props: { + message: createTestMessage(), + showAvatar: false + } + }); + expect(wrapper.find('.k-chat-bubble__avatar').exists()).toBe(false); + }); + + // 头像大小测试 + it('should set correct avatar size', () => { + const wrapper = mount(KChatBubble, { + props: { + message: createTestMessage(), + avatarSize: 'small' + } + }); + expect(wrapper.find('.k-avatar').classes()).toContain('k-avatar--small'); + }); + + // 发送者名称显示测试 + it('should display sender name when showSenderName is true', () => { + const wrapper = mount(KChatBubble, { + props: { + message: createTestMessage(), + showSenderName: true, + isSelf: false + } + }); + expect(wrapper.find('.k-chat-bubble__sender').exists()).toBe(true); + expect(wrapper.find('.k-chat-bubble__sender').text()).toBe('Test User'); + }); + + // 发送者名称隐藏测试 + it('should not display sender name when showSenderName is false', () => { + const wrapper = mount(KChatBubble, { + props: { + message: createTestMessage(), + showSenderName: false, + isSelf: false + } + }); + expect(wrapper.find('.k-chat-bubble__sender').exists()).toBe(false); + }); + + // 发送者名称不显示在自己消息中测试 + it('should not display sender name for self messages', () => { + const wrapper = mount(KChatBubble, { + props: { + message: createTestMessage(), + showSenderName: true, + isSelf: true + } + }); + expect(wrapper.find('.k-chat-bubble__sender').exists()).toBe(false); + }); + + // 时间显示测试 + it('should display formatted time', () => { + const message = createTestMessage(); + const wrapper = mount(KChatBubble, { + props: { + message + } + }); + expect(wrapper.find('.k-chat-bubble__time').exists()).toBe(true); + }); + + // 消息状态显示测试 - sending + it('should display sending status icon for self messages', () => { + const message = createTestMessage({ status: 'sending' }); + const wrapper = mount(KChatBubble, { + props: { + message, + isSelf: true + } + }); + expect(wrapper.find('.k-chat-bubble__status-icon--sending').exists()).toBe(true); + }); + + // 消息状态显示测试 - sent + it('should display sent status icon for self messages', () => { + const message = createTestMessage({ status: 'sent' }); + const wrapper = mount(KChatBubble, { + props: { + message, + isSelf: true + } + }); + expect(wrapper.find('.k-chat-bubble__status-icon--sent').exists()).toBe(true); + }); + + // 消息状态显示测试 - delivered + it('should display delivered status icon for self messages', () => { + const message = createTestMessage({ status: 'delivered' }); + const wrapper = mount(KChatBubble, { + props: { + message, + isSelf: true + } + }); + expect(wrapper.find('.k-chat-bubble__status-icon--delivered').exists()).toBe(true); + }); + + // 消息状态显示测试 - read + it('should display read status icon for self messages', () => { + const message = createTestMessage({ status: 'read' }); + const wrapper = mount(KChatBubble, { + props: { + message, + isSelf: true + } + }); + expect(wrapper.find('.k-chat-bubble__status-icon--read').exists()).toBe(true); + }); + + // 消息状态显示测试 - failed + it('should display failed status icon for self messages', () => { + const message = createTestMessage({ status: 'failed' }); + const wrapper = mount(KChatBubble, { + props: { + message, + isSelf: true + } + }); + expect(wrapper.find('.k-chat-bubble__status-icon--failed').exists()).toBe(true); + }); + + // 气泡样式测试 - default + it('should use default bubble style', () => { + const wrapper = mount(KChatBubble, { + props: { + message: createTestMessage(), + bubbleStyle: 'default' + } + }); + expect(wrapper.classes()).toContain('k-chat-bubble--style-default'); + }); + + // 气泡样式测试 - rounded + it('should use rounded bubble style', () => { + const wrapper = mount(KChatBubble, { + props: { + message: createTestMessage(), + bubbleStyle: 'rounded' + } + }); + expect(wrapper.classes()).toContain('k-chat-bubble--style-rounded'); + }); + + // 气泡样式测试 - sharp + it('should use sharp bubble style', () => { + const wrapper = mount(KChatBubble, { + props: { + message: createTestMessage(), + bubbleStyle: 'sharp' + } + }); + expect(wrapper.classes()).toContain('k-chat-bubble--style-sharp'); + }); + + // 气泡点击事件测试 + it('should emit bubbleClick event when bubble is clicked', () => { + const message = createTestMessage(); + const wrapper = mount(KChatBubble, { + props: { + message + } + }); + + wrapper.find('.k-chat-bubble__bubble').trigger('click'); + expect(wrapper.emitted('bubbleClick')).toBeTruthy(); + expect(wrapper.emitted('bubbleClick')?.[0]?.[0]).toEqual(message); + }); + + // 没有头像时使用默认图标测试 + it('should use default icon when no avatar is provided', () => { + const message = createTestMessage({ sender: { name: 'Test User', avatar: '', isOnline: true } }); + const wrapper = mount(KChatBubble, { + props: { + message + } + }); + expect(wrapper.find('.k-avatar__icon').exists()).toBe(true); + }); + + // 组合属性测试 + it('should work correctly with multiple props combined', () => { + const message = createTestMessage({ status: 'delivered' }); + const wrapper = mount(KChatBubble, { + props: { + message, + isSelf: true, + showAvatar: true, + avatarSize: 'large', + showSenderName: false, + bubbleStyle: 'rounded' + } + }); + + expect(wrapper.classes()).toContain('k-chat-bubble--own'); + expect(wrapper.classes()).toContain('k-chat-bubble--style-rounded'); + expect(wrapper.find('.k-chat-bubble__avatar').exists()).toBe(true); + expect(wrapper.find('.k-avatar').classes()).toContain('k-avatar--large'); + expect(wrapper.find('.k-chat-bubble__sender').exists()).toBe(false); + expect(wrapper.find('.k-chat-bubble__status-icon--delivered').exists()).toBe(true); + }); +}); \ No newline at end of file diff --git a/src/components/ChatBubble/README.md b/src/components/ChatBubble/README.md new file mode 100644 index 0000000..2ac1528 --- /dev/null +++ b/src/components/ChatBubble/README.md @@ -0,0 +1,293 @@ +# KChatBubble + +`KChatBubble` 组件用于显示聊天气泡,支持多种消息类型(文本、图片、文件、语音、系统消息)以及消息状态和发送者信息。 + +## 使用示例 + +```vue + + + + +## Props + +| 属性名 | 类型 | 默认值 | 描述 | +| ----- | ---- | ----- | ---- | +| `message` | `ChatBubbleMessage` | - | 消息对象,必填 | +| `isSelf` | `boolean` | `false` | 是否为当前用户的消息 | +| `showAvatar` | `boolean` | `true` | 是否显示头像 | +| `avatarSize` | `'small' \| 'middle' \| 'large'` | `'middle'` | 头像大小 | +| `showSenderName` | `boolean` | `true` | 是否显示发送者名称 | +| `bubbleStyle` | `'default' \| 'rounded' \| 'sharp'` | `'default'` | 气泡样式 | + +## Events + +| 事件名 | 参数 | 描述 | +| ----- | ---- | ---- | +| `bubbleClick` | `(message: ChatBubbleMessage)` | 气泡点击事件 | +| `imageClick` | `(message: ChatBubbleMessage)` | 图片点击事件 | +| `downloadClick` | `(message: ChatBubbleMessage)` | 文件下载点击事件 | +| `voicePlay` | `(message: ChatBubbleMessage, isPlaying: boolean)` | 语音播放/暂停事件 | +| `retry` | `(message: ChatBubbleMessage)` | 消息重试点击事件 | + +## 消息类型 + +`ChatBubbleMessage` 接口支持以下消息类型: + +### 文本消息 +```ts +{ + id: string; + type: 'text'; + content: string; + sender: { id: string; name: string; avatar?: string; isOnline?: boolean }; + timestamp: number; + status?: 'sending' | 'sent' | 'delivered' | 'read' | 'failed'; +} +``` + +### 图片消息 +```ts +{ + id: string; + type: 'image'; + content: { url: string; alt?: string }; + sender: { id: string; name: string; avatar?: string; isOnline?: boolean }; + timestamp: number; + status?: 'sending' | 'sent' | 'delivered' | 'read' | 'failed'; +} +``` + +### 文件消息 +```ts +{ + id: string; + type: 'file'; + content: { name: string; size: number; type: string }; + sender: { id: string; name: string; avatar?: string; isOnline?: boolean }; + timestamp: number; + status?: 'sending' | 'sent' | 'delivered' | 'read' | 'failed'; +} +``` + +### 语音消息 +```ts +{ + id: string; + type: 'voice'; + content: { duration: number; waveform?: number[] }; + sender: { id: string; name: string; avatar?: string; isOnline?: boolean }; + timestamp: number; + status?: 'sending' | 'sent' | 'delivered' | 'read' | 'failed'; +} +``` + +### 系统消息 +```ts +{ + id: string; + type: 'system'; + content: string; + sender: { id: string; name: string }; + timestamp: number; +} +``` + +## 消息状态 + +消息状态包括以下几种: +- `sending`: 发送中 +- `sent`: 已发送 +- `delivered`: 已送达 +- `read`: 已读 +- `failed`: 发送失败 + +## 样式定制 + +可以通过覆盖以下CSS变量来自定义组件样式: + +```css +.k-chat-bubble { + /* 自定义聊天气泡样式 */ +} + +.k-chat-bubble--own { + /* 自定义自己的聊天气泡样式 */ +} + +.k-chat-bubble--other { + /* 自定义他人的聊天气泡样式 */ +} + +.k-chat-bubble--style-rounded { + /* 自定义圆角聊天气泡样式 */ +} + +.k-chat-bubble--style-sharp { + /* 自定义锐角聊天气泡样式 */ +} +} +``` \ No newline at end of file diff --git a/src/components/ChatBubble/__snapshots__/ChatBubble.test.ts.snap b/src/components/ChatBubble/__snapshots__/ChatBubble.test.ts.snap new file mode 100644 index 0000000..cef6cf1 --- /dev/null +++ b/src/components/ChatBubble/__snapshots__/ChatBubble.test.ts.snap @@ -0,0 +1,21 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`KChatBubble > should render text message correctly 1`] = ` +"
+ +
+ +
+
+ +
张三
+
+ +
你好,这是一条测试消息。
+
+
05:45 + +
+
+
" +`; diff --git a/src/components/ChatBubble/index.scss b/src/components/ChatBubble/index.scss new file mode 100644 index 0000000..3da116f --- /dev/null +++ b/src/components/ChatBubble/index.scss @@ -0,0 +1,329 @@ +// ChatBubble组件样式 + +@import '../../styles/mixins.scss'; + +.k-chat-bubble { + display: flex; + margin-bottom: $spacing-md; + max-width: 80%; + + // 自己的消息 + &--own { + flex-direction: row-reverse; + align-self: flex-end; + margin-left: auto; + margin-right: 0; + } + + // 其他人的消息 + &--other { + flex-direction: row; + align-self: flex-start; + margin-left: 0; + margin-right: auto; + } +} + +// 头像 +.k-chat-bubble__avatar { + flex-shrink: 0; + margin: 0 $spacing-sm; +} + +// 消息内容 +.k-chat-bubble__content { + display: flex; + flex-direction: column; + min-width: 0; + flex: 1; +} + +// 发送者名称 +.k-chat-bubble__sender { + font-size: $font-size-xs; + color: $text-color-tertiary; + margin-bottom: $spacing-xs; + padding-left: $spacing-xs; +} + +// 消息气泡 +.k-chat-bubble__bubble { + position: relative; + padding: $spacing-sm $spacing-md; + border-radius: $border-radius-lg; + word-wrap: break-word; + max-width: 100%; + + // 自己的消息气泡 + &--own { + background-color: $primary-color; + color: #fff; + border-top-right-radius: $border-radius-sm; + + &::after { + content: ''; + position: absolute; + top: 10px; + right: -6px; + width: 0; + height: 0; + border-style: solid; + border-width: 6px 0 6px 6px; + border-color: transparent transparent transparent $primary-color; + } + } + + // 其他人的消息气泡 + &--other { + background-color: $background-color-light; + color: $text-color; + border-top-left-radius: $border-radius-sm; + + &::after { + content: ''; + position: absolute; + top: 10px; + left: -6px; + width: 0; + height: 0; + border-style: solid; + border-width: 6px 6px 6px 0; + border-color: transparent $background-color-light transparent transparent; + } + } + + // 文本消息 + &--type-text { + line-height: 1.5; + } + + // 图片消息 + &--type-image { + padding: 0; + background-color: transparent; + border-radius: $border-radius-base; + overflow: hidden; + + .k-chat-bubble__bubble--own::after, + .k-chat-bubble__bubble--other::after { + display: none; + } + } + + // 文件消息 + &--type-file { + padding: $spacing-sm; + } + + // 语音消息 + &--type-voice { + padding: $spacing-sm; + } + + // 系统消息 + &--type-system { + background-color: $background-color-base; + color: $text-color-secondary; + text-align: center; + font-size: $font-size-sm; + padding: $spacing-xs $spacing-sm; + margin: 0 auto; + max-width: 60%; + + &::after { + display: none; + } + } +} + +// 文本消息 +.k-chat-bubble__text { + white-space: pre-wrap; +} + +// 图片消息 +.k-chat-bubble__image { + img { + max-width: 200px; + max-height: 200px; + border-radius: $border-radius-base; + cursor: pointer; + transition: transform 0.2s; + + &:hover { + transform: scale(1.02); + } + } +} + +// 文件消息 +.k-chat-bubble__file { + display: flex; + align-items: center; + gap: $spacing-sm; + min-width: 200px; +} + +.k-chat-bubble__file-icon { + display: flex; + align-items: center; + justify-content: center; + width: 40px; + height: 40px; + background-color: $background-color-base; + border-radius: $border-radius-base; + color: $text-color-secondary; + font-size: 20px; +} + +.k-chat-bubble__file-info { + flex: 1; + min-width: 0; +} + +.k-chat-bubble__file-name { + font-weight: 500; + @include text-ellipsis; +} + +.k-chat-bubble__file-size { + font-size: $font-size-xs; + color: $text-color-tertiary; +} + +// 语音消息 +.k-chat-bubble__voice { + display: flex; + align-items: center; + gap: $spacing-sm; + min-width: 120px; +} + +.k-chat-bubble__voice-btn--playing { + color: $primary-color; +} + +.k-chat-bubble__voice-duration { + font-size: $font-size-xs; + color: $text-color-secondary; + white-space: nowrap; +} + +.k-chat-bubble__voice-waveform { + display: flex; + align-items: center; + height: 20px; + gap: 2px; + flex: 1; +} + +.k-chat-bubble__voice-bar { + width: 3px; + background-color: currentColor; + border-radius: 3px; + opacity: 0.6; + transition: opacity 0.2s; + + &:hover { + opacity: 1; + } +} + +// 系统消息 +.k-chat-bubble__system { + padding: $spacing-xs $spacing-sm; + font-size: $font-size-sm; + color: $text-color-secondary; + background-color: $background-color-base; + border-radius: $border-radius-base; + text-align: center; + margin: 0 auto; + max-width: 60%; +} + +// 消息状态和时间 +.k-chat-bubble__meta { + display: flex; + align-items: center; + justify-content: flex-end; + margin-top: $spacing-xs; + padding: 0 $spacing-xs; + font-size: $font-size-xs; + color: $text-color-tertiary; + gap: $spacing-xs; + + .k-chat-bubble--other & { + justify-content: flex-start; + } +} + +.k-chat-bubble__time { + white-space: nowrap; +} + +.k-chat-bubble__status-icon--sending { + color: $text-color-tertiary; + animation: spin 1s linear infinite; +} + +.k-chat-bubble__status-icon--sent { + color: $text-color-tertiary; +} + +.k-chat-bubble__status-icon--delivered { + color: $text-color-tertiary; +} + +.k-chat-bubble__status-icon--read { + color: $primary-color; +} + +.k-chat-bubble__status-icon--failed { + color: $error-color; + cursor: pointer; + + &:hover { + color: darken($error-color, 10%); + } +} + +// 样式变体 +// 默认样式(占位,保持与后续变体结构一致) +.k-chat-bubble--style-default { + // 默认样式已在基础规则中定义,此处无需额外覆盖 +} + + +.k-chat-bubble--style-rounded { + .k-chat-bubble__bubble { + border-radius: 20px; + + &--own { + border-top-right-radius: 8px; + } + + &--other { + border-top-left-radius: 8px; + } + } +} + +.k-chat-bubble--style-sharp { + .k-chat-bubble__bubble { + border-radius: $border-radius-sm; + + &--own { + border-top-right-radius: 0; + } + + &--other { + border-top-left-radius: 0; + } + } +} + +// 动画 +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} \ No newline at end of file diff --git a/src/components/ChatBubble/index.ts b/src/components/ChatBubble/index.ts new file mode 100644 index 0000000..150339d --- /dev/null +++ b/src/components/ChatBubble/index.ts @@ -0,0 +1,5 @@ +import ChatBubble from './index.vue'; +export * from './types'; + +export { ChatBubble }; +export default ChatBubble; diff --git a/src/components/ChatBubble/index.vue b/src/components/ChatBubble/index.vue new file mode 100644 index 0000000..5240f43 --- /dev/null +++ b/src/components/ChatBubble/index.vue @@ -0,0 +1,199 @@ + + + diff --git a/src/components/ChatBubble/types.ts b/src/components/ChatBubble/types.ts new file mode 100644 index 0000000..9c08f97 --- /dev/null +++ b/src/components/ChatBubble/types.ts @@ -0,0 +1,26 @@ +export interface ChatBubbleMessage { + id: string; + type: 'text'; + content: string; + sender: { + id: string; + avatar?: string; + name: string; + isOnline?: boolean; + }; + timestamp: number; + status?: 'sending' | 'sent' | 'delivered' | 'read' | 'failed'; +} + +export interface ChatBubbleProps { + message: ChatBubbleMessage; + isSelf?: boolean; + showAvatar?: boolean; + avatarSize?: 'small' | 'middle' | 'large'; + showSenderName?: boolean; + bubbleStyle?: 'default' | 'rounded' | 'sharp'; +} + +export interface ChatBubbleEmits { + (event: 'bubbleClick', message: ChatBubbleMessage): void; +} diff --git a/src/components/Container/Container.test.ts b/src/components/Container/Container.test.ts new file mode 100644 index 0000000..fea1e88 --- /dev/null +++ b/src/components/Container/Container.test.ts @@ -0,0 +1,361 @@ +import { describe, it, expect } from 'vitest'; +import { mount } from '@vue/test-utils'; +import Container from './index.vue'; + +describe('KContainer Component', () => { + describe('基础渲染', () => { + it('应该正确渲染容器组件', () => { + const wrapper = mount(Container, { + slots: { + default: '容器内容' + } + }); + expect(wrapper.exists()).toBe(true); + expect(wrapper.classes()).toContain('k-container'); + expect(wrapper.text()).toBe('容器内容'); + }); + + it('应该默认居中', () => { + const wrapper = mount(Container); + expect(wrapper.classes()).toContain('k-container--center'); + }); + }); + + describe('流体布局', () => { + it('应该在fluid为true时添加流体布局类', () => { + const wrapper = mount(Container, { + props: { + fluid: true + } + }); + expect(wrapper.classes()).toContain('k-container--fluid'); + }); + + it('应该在fluid为false时不添加流体布局类', () => { + const wrapper = mount(Container, { + props: { + fluid: false + } + }); + expect(wrapper.classes()).not.toContain('k-container--fluid'); + }); + }); + + describe('最大宽度', () => { + it('应该正确应用数字类型的最大宽度', () => { + const maxWidth = 1200; + const wrapper = mount(Container, { + props: { + maxWidth + } + }); + expect(wrapper.attributes('style')).toContain(`max-width: ${maxWidth}px`); + }); + + it('应该正确应用字符串类型的最大宽度', () => { + const maxWidth = '90%'; + const wrapper = mount(Container, { + props: { + maxWidth + } + }); + expect(wrapper.attributes('style')).toContain(`max-width: ${maxWidth}`); + }); + }); + + describe('内边距', () => { + it('应该正确应用数字类型的内边距', () => { + const padding = 20; + const wrapper = mount(Container, { + props: { + padding + } + }); + expect(wrapper.attributes('style')).toContain(`padding: ${padding}px`); + }); + + it('应该正确应用字符串类型的内边距', () => { + const padding = '2rem'; + const wrapper = mount(Container, { + props: { + padding + } + }); + expect(wrapper.attributes('style')).toContain(`padding: ${padding}`); + }); + + it('应该正确应用数组类型的内边距(1个值)', () => { + const padding = [10]; + const wrapper = mount(Container, { + props: { + padding + } + }); + expect(wrapper.attributes('style')).toContain(`padding: ${padding[0]}px`); + }); + + it('应该正确应用数组类型的内边距(2个值)', () => { + const padding = [10, 20]; + const wrapper = mount(Container, { + props: { + padding + } + }); + expect(wrapper.attributes('style')).toContain(`padding: ${padding[0]}px ${padding[1]}px`); + }); + + it('应该正确应用数组类型的内边距(4个值)', () => { + const padding = [10, 20, 30, 40]; + const wrapper = mount(Container, { + props: { + padding + } + }); + expect(wrapper.attributes('style')).toContain(`padding: ${padding[0]}px ${padding[1]}px ${padding[2]}px ${padding[3]}px`); + }); + }); + + describe('外边距', () => { + it('应该正确应用数字类型的外边距', () => { + const margin = 20; + const wrapper = mount(Container, { + props: { + margin + } + }); + expect(wrapper.attributes('style')).toContain(`margin: ${margin}px`); + }); + + it('应该正确应用字符串类型的外边距', () => { + const margin = '2rem'; + const wrapper = mount(Container, { + props: { + margin + } + }); + expect(wrapper.attributes('style')).toContain(`margin: ${margin}`); + }); + + it('应该正确应用数组类型的外边距(1个值)', () => { + const margin = [10]; + const wrapper = mount(Container, { + props: { + margin + } + }); + expect(wrapper.attributes('style')).toContain(`margin: ${margin[0]}px`); + }); + + it('应该正确应用数组类型的外边距(2个值)', () => { + const margin = [10, 20]; + const wrapper = mount(Container, { + props: { + margin + } + }); + expect(wrapper.attributes('style')).toContain(`margin: ${margin[0]}px ${margin[1]}px`); + }); + + it('应该正确应用数组类型的外边距(4个值)', () => { + const margin = [10, 20, 30, 40]; + const wrapper = mount(Container, { + props: { + margin + } + }); + expect(wrapper.attributes('style')).toContain(`margin: ${margin[0]}px ${margin[1]}px ${margin[2]}px ${margin[3]}px`); + }); + }); + + describe('居中属性', () => { + it('应该在center为true时添加居中类', () => { + const wrapper = mount(Container, { + props: { + center: true + } + }); + expect(wrapper.classes()).toContain('k-container--center'); + }); + + it('应该在center为false时不添加居中类', () => { + const wrapper = mount(Container, { + props: { + center: false + } + }); + expect(wrapper.classes()).not.toContain('k-container--center'); + }); + }); + + describe('背景颜色', () => { + it('应该正确应用背景颜色', () => { + const bgColor = '#ff0000'; + const wrapper = mount(Container, { + props: { + bgColor + } + }); + expect(wrapper.attributes('style')).toContain(`background-color: ${bgColor}`); + }); + }); + + describe('圆角', () => { + it('应该正确应用数字类型的圆角', () => { + const borderRadius = 8; + const wrapper = mount(Container, { + props: { + borderRadius + } + }); + expect(wrapper.attributes('style')).toContain(`border-radius: ${borderRadius}px`); + }); + + it('应该正确应用字符串类型的圆角', () => { + const borderRadius = '50%'; + const wrapper = mount(Container, { + props: { + borderRadius + } + }); + expect(wrapper.attributes('style')).toContain(`border-radius: ${borderRadius}`); + }); + }); + + describe('阴影效果', () => { + it('应该正确应用always阴影样式', () => { + const wrapper = mount(Container, { + props: { + shadow: 'always' + } + }); + expect(wrapper.classes()).toContain('k-container--shadow-always'); + }); + + it('应该正确应用hover阴影样式', () => { + const wrapper = mount(Container, { + props: { + shadow: 'hover' + } + }); + expect(wrapper.classes()).toContain('k-container--shadow-hover'); + }); + + it('应该在shadow为never时不添加阴影类', () => { + const wrapper = mount(Container, { + props: { + shadow: 'never' + } + }); + expect(wrapper.classes()).not.toContain('k-container--shadow-always'); + expect(wrapper.classes()).not.toContain('k-container--shadow-hover'); + }); + }); + + describe('组合属性', () => { + it('应该正确组合流体布局、背景颜色和阴影', () => { + const wrapper = mount(Container, { + props: { + fluid: true, + bgColor: '#f0f0f0', + shadow: 'always' + } + }); + expect(wrapper.classes()).toContain('k-container--fluid'); + expect(wrapper.classes()).toContain('k-container--shadow-always'); + expect(wrapper.attributes('style')).toContain('background-color: #f0f0f0'); + }); + + it('应该正确组合内边距、外边距和圆角', () => { + const wrapper = mount(Container, { + props: { + padding: [10, 20], + margin: [20, 0], + borderRadius: 8 + } + }); + expect(wrapper.attributes('style')).toContain('padding: 10px 20px'); + expect(wrapper.attributes('style')).toContain('margin: 20px 0px'); + expect(wrapper.attributes('style')).toContain('border-radius: 8px'); + }); + + it('应该正确组合所有属性', () => { + const wrapper = mount(Container, { + props: { + fluid: false, + maxWidth: 1200, + padding: [20], + margin: [10, 20], + center: true, + bgColor: '#ffffff', + borderRadius: 4, + shadow: 'hover' + }, + slots: { + default: '组合属性测试' + } + }); + expect(wrapper.classes()).toContain('k-container--center'); + expect(wrapper.classes()).toContain('k-container--shadow-hover'); + expect(wrapper.attributes('style')).toContain('max-width: 1200px'); + expect(wrapper.attributes('style')).toContain('padding: 20px'); + expect(wrapper.attributes('style')).toContain('margin: 10px 20px'); + expect(wrapper.attributes('style')).toContain('background-color: #ffffff'); + expect(wrapper.attributes('style')).toContain('border-radius: 4px'); + expect(wrapper.text()).toBe('组合属性测试'); + }); + }); + + describe('动态属性变化', () => { + it('应该响应fluid属性的动态变化', () => { + const wrapper = mount(Container, { + props: { + fluid: false + } + }); + expect(wrapper.classes()).not.toContain('k-container--fluid'); + + wrapper.setProps({ fluid: true }); + expect(wrapper.classes()).toContain('k-container--fluid'); + }); + + it('应该响应maxWidth属性的动态变化', () => { + const wrapper = mount(Container, { + props: { + maxWidth: 1000 + } + }); + expect(wrapper.attributes('style')).toContain('max-width: 1000px'); + + wrapper.setProps({ maxWidth: '90%' }); + expect(wrapper.attributes('style')).toContain('max-width: 90%'); + }); + + it('应该响应bgColor属性的动态变化', () => { + const wrapper = mount(Container, { + props: { + bgColor: '#ff0000' + } + }); + expect(wrapper.attributes('style')).toContain('background-color: #ff0000'); + + wrapper.setProps({ bgColor: '#00ff00' }); + expect(wrapper.attributes('style')).toContain('background-color: #00ff00'); + }); + + it('应该响应shadow属性的动态变化', () => { + const wrapper = mount(Container, { + props: { + shadow: 'never' + } + }); + expect(wrapper.classes()).not.toContain('k-container--shadow-always'); + + wrapper.setProps({ shadow: 'always' }); + expect(wrapper.classes()).toContain('k-container--shadow-always'); + + wrapper.setProps({ shadow: 'hover' }); + expect(wrapper.classes()).toContain('k-container--shadow-hover'); + expect(wrapper.classes()).not.toContain('k-container--shadow-always'); + }); + }); +}); \ No newline at end of file diff --git a/src/components/Container/README.md b/src/components/Container/README.md new file mode 100644 index 0000000..413765b --- /dev/null +++ b/src/components/Container/README.md @@ -0,0 +1,81 @@ +# Container 容器组件 + +Container组件是一个布局容器组件,用于控制内容区域的宽度、内外边距、背景色和阴影等样式,提供灵活的页面布局能力。 + +## 参数接口 (Props) + +| 参数名 | 类型 | 默认值 | 说明 | +|--------|------|--------|------| +| fluid | boolean | false | 是否为流体布局,占据整个父容器宽度 | +| maxWidth | string \| number | '' | 容器最大宽度,支持数字(px)或字符串 | +| padding | string \| number \| number[] | '' | 内边距,支持数字(px)、字符串或数组[上,右,下,左] | +| margin | string \| number \| number[] | '' | 外边距,支持数字(px)、字符串或数组[上,右,下,左] | +| center | boolean | true | 是否水平居中 | +| bgColor | string | '' | 背景颜色 | +| borderRadius | string \| number | '' | 边框圆角,支持数字(px)或字符串 | +| shadow | 'always' \| 'hover' \| 'never' | 'never' | 阴影效果 | + +## 插槽 (Slots) + +| 插槽名 | 说明 | +|--------|------| +| default | 容器内容区域 | + +## 事件 (Events) + +Container组件没有提供事件。 + +## 使用示例 + +```vue + \ No newline at end of file diff --git a/src/components/Container/__snapshots__/Container.test.ts.snap b/src/components/Container/__snapshots__/Container.test.ts.snap new file mode 100644 index 0000000..d32609e --- /dev/null +++ b/src/components/Container/__snapshots__/Container.test.ts.snap @@ -0,0 +1,79 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`KContainer > should apply always shadow correctly 1`] = ` +"
+
始终有阴影
+
" +`; + +exports[`KContainer > should apply array padding correctly 1`] = ` +"
+
数组内边距
+
" +`; + +exports[`KContainer > should apply border radius correctly 1`] = ` +"
+
有圆角
+
" +`; + +exports[`KContainer > should apply full array padding correctly 1`] = ` +"
+
完整数组内边距
+
" +`; + +exports[`KContainer > should apply hover shadow correctly 1`] = ` +"
+
悬停有阴影
+
" +`; + +exports[`KContainer > should apply margin style correctly 1`] = ` +"
+
有外边距
+
" +`; + +exports[`KContainer > should apply maxWidth style correctly 1`] = ` +"
+
有最大宽度
+
" +`; + +exports[`KContainer > should apply padding style correctly 1`] = ` +"
+
有内边距
+
" +`; + +exports[`KContainer > should apply string maxWidth correctly 1`] = ` +"
+
百分比最大宽度
+
" +`; + +exports[`KContainer > should not apply shadow when shadow is never 1`] = ` +"
+
无阴影
+
" +`; + +exports[`KContainer > should not center content when center prop is false 1`] = ` +"
+
不居中内容
+
" +`; + +exports[`KContainer > should render container correctly by default 1`] = ` +"
+
测试内容
+
" +`; + +exports[`KContainer > should render fluid container when fluid prop is true 1`] = ` +"
+
流体容器
+
" +`; diff --git a/src/components/Container/index.scss b/src/components/Container/index.scss new file mode 100644 index 0000000..7ee603e --- /dev/null +++ b/src/components/Container/index.scss @@ -0,0 +1,35 @@ +// Container组件样式 + + +.k-container { + width: 100%; + box-sizing: border-box; + + // 流体布局 + &--fluid { + max-width: none !important; + } + + // 居中 + &--center { + margin-left: auto; + margin-right: auto; + } + + // 阴影样式 + &--shadow-always { + box-shadow: $box-shadow-light; + } + + &--shadow-hover { + transition: box-shadow $transition-duration $transition-timing-function; + + &:hover { + box-shadow: $box-shadow-light; + } + } + + &--shadow-never { + box-shadow: none; + } +} \ No newline at end of file diff --git a/src/components/Container/index.ts b/src/components/Container/index.ts new file mode 100644 index 0000000..f218cd5 --- /dev/null +++ b/src/components/Container/index.ts @@ -0,0 +1,5 @@ +import Container from './index.vue'; +export * from './types'; + +export { Container }; +export default Container; diff --git a/src/components/Container/index.vue b/src/components/Container/index.vue new file mode 100644 index 0000000..12212d5 --- /dev/null +++ b/src/components/Container/index.vue @@ -0,0 +1,131 @@ + + + diff --git a/src/components/Container/types.ts b/src/components/Container/types.ts new file mode 100644 index 0000000..cbf84e0 --- /dev/null +++ b/src/components/Container/types.ts @@ -0,0 +1,41 @@ +export interface ContainerProps { + /** + * 是否为流体布局,占据整个父容器宽度 + */ + fluid?: boolean; + + /** + * 容器最大宽度,支持数字(px)或字符串 + */ + maxWidth?: string | number; + + /** + * 内边距,支持数字(px)、字符串或数组[上,右,下,左] + */ + padding?: string | number | number[]; + + /** + * 外边距,支持数字(px)、字符串或数组[上,右,下,左] + */ + margin?: string | number | number[]; + + /** + * 是否水平居中 + */ + center?: boolean; + + /** + * 背景颜色 + */ + bgColor?: string; + + /** + * 边框圆角,支持数字(px)或字符串 + */ + borderRadius?: string | number; + + /** + * 阴影效果 + */ + shadow?: 'always' | 'hover' | 'never'; +} diff --git a/src/components/Divider/Divider.test.ts b/src/components/Divider/Divider.test.ts new file mode 100644 index 0000000..2764916 --- /dev/null +++ b/src/components/Divider/Divider.test.ts @@ -0,0 +1,180 @@ +import { describe, it, expect } from 'vitest'; +import { mount } from '@vue/test-utils'; +import KDivider from './index.vue'; + +describe('Divider组件测试', () => { + // 基础渲染测试 + it('组件能够正常渲染', () => { + const wrapper = mount(KDivider); + expect(wrapper.exists()).toBe(true); + expect(wrapper.classes('k-divider')).toBe(true); + }); + + // 方向测试 + it('默认方向为horizontal', () => { + const wrapper = mount(KDivider); + expect(wrapper.classes('k-divider--horizontal')).toBe(true); + }); + + it('可以设置为vertical方向', () => { + const wrapper = mount(KDivider, { + props: { + direction: 'vertical' + } + }); + expect(wrapper.classes('k-divider--vertical')).toBe(true); + }); + + // 内容位置测试 + it('默认内容位置为center', () => { + const wrapper = mount(KDivider, { + slots: { + default: '测试文本' + } + }); + const textElement = wrapper.find('.k-divider__text'); + expect(textElement.classes('k-divider__text--center')).toBe(true); + }); + + it('可以设置内容位置为left', () => { + const wrapper = mount(KDivider, { + props: { + contentPosition: 'left' + }, + slots: { + default: '测试文本' + } + }); + const textElement = wrapper.find('.k-divider__text'); + expect(textElement.classes('k-divider__text--left')).toBe(true); + }); + + it('可以设置内容位置为right', () => { + const wrapper = mount(KDivider, { + props: { + contentPosition: 'right' + }, + slots: { + default: '测试文本' + } + }); + const textElement = wrapper.find('.k-divider__text'); + expect(textElement.classes('k-divider__text--right')).toBe(true); + }); + + // 虚线样式测试 + it('默认不是虚线', () => { + const wrapper = mount(KDivider); + expect(wrapper.classes('k-divider--dashed')).toBe(false); + }); + + it('可以设置为虚线样式', () => { + const wrapper = mount(KDivider, { + props: { + dashed: true + } + }); + expect(wrapper.classes('k-divider--dashed')).toBe(true); + }); + + // 边框样式测试 + it('可以设置边框样式', () => { + const wrapper = mount(KDivider, { + props: { + borderStyle: 'dotted' + } + }); + expect(wrapper.attributes('style')).toContain('border-top-style: dotted'); + }); + + // 颜色测试 + it('可以设置边框颜色', () => { + const color = '#ff0000'; + const wrapper = mount(KDivider, { + props: { + color + } + }); + expect(wrapper.attributes('style')).toContain(`border-color: ${color}`); + }); + + // 边距测试 + it('可以设置数字类型的边距', () => { + const margin = 20; + const wrapper = mount(KDivider, { + props: { + margin + } + }); + expect(wrapper.attributes('style')).toContain(`margin: ${margin}px`); + }); + + it('可以设置字符串类型的边距', () => { + const margin = '10px 20px'; + const wrapper = mount(KDivider, { + props: { + margin + } + }); + expect(wrapper.attributes('style')).toContain(`margin: ${margin}`); + }); + + // 尺寸测试 + it('默认尺寸为medium', () => { + const wrapper = mount(KDivider); + expect(wrapper.classes('k-divider--medium')).toBe(true); + }); + + it('可以设置尺寸为small', () => { + const wrapper = mount(KDivider, { + props: { + size: 'small' + } + }); + expect(wrapper.classes('k-divider--small')).toBe(true); + }); + + it('可以设置尺寸为large', () => { + const wrapper = mount(KDivider, { + props: { + size: 'large' + } + }); + expect(wrapper.classes('k-divider--large')).toBe(true); + }); + + // 插槽内容测试 + it('可以显示插槽内容', () => { + const text = '测试文本'; + const wrapper = mount(KDivider, { + slots: { + default: text + } + }); + const textElement = wrapper.find('.k-divider__text'); + expect(textElement.exists()).toBe(true); + expect(textElement.text()).toBe(text); + }); + + it('没有插槽内容时不显示文本元素', () => { + const wrapper = mount(KDivider); + const textElement = wrapper.find('.k-divider__text'); + expect(textElement.exists()).toBe(false); + }); + + // 组合属性测试 + it('可以同时设置多个属性', () => { + const wrapper = mount(KDivider, { + props: { + direction: 'vertical', + dashed: true, + color: '#00ff00', + margin: '8px' + } + }); + expect(wrapper.classes('k-divider--vertical')).toBe(true); + expect(wrapper.classes('k-divider--dashed')).toBe(true); + expect(wrapper.attributes('style')).toContain('border-color: #00ff00'); + expect(wrapper.attributes('style')).toContain('margin: 8px'); + }); +}); \ No newline at end of file diff --git a/src/components/Divider/README.md b/src/components/Divider/README.md new file mode 100644 index 0000000..ae2d592 --- /dev/null +++ b/src/components/Divider/README.md @@ -0,0 +1,153 @@ +# Divider 分割线 + +用于分隔内容区域的分割线组件。 + +## 基本用法 + +### 水平分割线 + +最基础的水平分割线。 + +```vue + +``` + +### 带文本的分割线 + +可以在分割线中间显示文本内容。 + +```vue +分割线标题 +左侧标题 +右侧标题 +``` + +### 虚线分割线 + +通过设置 `dashed` 属性可以创建虚线分割线。 + +```vue +虚线分割线 +``` + +### 垂直分割线 + +设置 `direction` 属性为 `vertical` 可以创建垂直分割线。 + +```vue +
+ 内容 1 + + 内容 2 + + 内容 3 +
+``` + +### 自定义样式 + +可以自定义分割线的颜色、边框样式和边距。 + +```vue +红色点状分割线 +带外边距的分割线 +``` + +### 不同尺寸 + +通过 `size` 属性可以设置分割线的尺寸。 + +```vue +小号分割线 +中号分割线 +大号分割线 +``` + +## API + +### Props + +| 属性名 | 类型 | 说明 | 默认值 | 可选值 | +|-------|-----|------|-------|-------| +| direction | string | 分割线方向 | horizontal | horizontal, vertical | +| contentPosition | string | 文本位置 | center | left, center, right | +| dashed | boolean | 是否为虚线 | false | true, false | +| borderStyle | string | 边框样式 | - | solid, dashed, dotted 等 CSS 边框样式值 | +| color | string | 边框颜色 | - | 任意 CSS 颜色值 | +| margin | string \| number | 外边距 | - | CSS margin 值或数字 | +| size | string | 分割线尺寸 | medium | small, medium, large | + +### Slots + +| 插槽名 | 说明 | +|-------|-----| +| default | 分割线中显示的文本内容 | + +## 类型定义 + +组件导出以下类型定义,可以在 TypeScript 项目中使用: + +```typescript +import { DividerProps, DividerDirection, DividerContentPosition, DividerSize } from '@/components/Divider'; +``` + +## 示例代码 + +以下是一个更复杂的使用示例: + +```vue + + + +``` \ No newline at end of file diff --git a/src/components/Divider/index.scss b/src/components/Divider/index.scss new file mode 100644 index 0000000..da7605c --- /dev/null +++ b/src/components/Divider/index.scss @@ -0,0 +1,73 @@ +// Divider组件样式 + + +.k-divider { + width: 100%; + margin: 16px 0; + border-top: 1px solid #e4e7ed; + box-sizing: border-box; + + // 方向样式 + &--horizontal { + display: block; + height: 1px; + width: 100%; + margin: 16px 0; + } + + &--vertical { + display: inline-block; + width: 1px; + height: 1em; + margin: 0 8px; + vertical-align: middle; + position: relative; + border-top: none; + border-left: 1px solid #e4e7ed; + } + + // 尺寸样式 + &--small { + margin: 12px 0; + } + + &--large { + margin: 20px 0; + } + + // 虚线样式 + &--dashed { + border-top-style: dashed; + } + + &--dashed.k-divider--vertical { + border-left-style: dashed; + } + + // 文本样式 + &__text { + position: absolute; + background-color: #fff; + padding: 0 16px; + color: #303133; + font-size: 14px; + font-weight: 500; + transform: translateY(-50%); + top: 50%; + + &--left { + left: 20px; + transform: translateY(-50%); + } + + &--center { + left: 50%; + transform: translateX(-50%) translateY(-50%); + } + + &--right { + right: 20px; + transform: translateY(-50%); + } + } +} \ No newline at end of file diff --git a/src/components/Divider/index.ts b/src/components/Divider/index.ts new file mode 100644 index 0000000..d0d94cc --- /dev/null +++ b/src/components/Divider/index.ts @@ -0,0 +1,4 @@ +import Divider from './index.vue'; + +export { Divider }; +export default Divider; diff --git a/src/components/Divider/index.vue b/src/components/Divider/index.vue new file mode 100644 index 0000000..a613144 --- /dev/null +++ b/src/components/Divider/index.vue @@ -0,0 +1,99 @@ + + + diff --git a/src/components/Divider/types.ts b/src/components/Divider/types.ts new file mode 100644 index 0000000..7b4450b --- /dev/null +++ b/src/components/Divider/types.ts @@ -0,0 +1,55 @@ +// Divider组件的方向类型 +export type DividerDirection = 'horizontal' | 'vertical'; + +// Divider组件的内容位置类型 +export type DividerContentPosition = 'left' | 'center' | 'right'; + +// Divider组件的尺寸类型 +export type DividerSize = 'small' | 'medium' | 'large'; + +// Divider组件的Props接口 +export interface DividerProps { + /** + * 分割线方向 + * @default horizontal + */ + direction?: DividerDirection; + + /** + * 分割线内容位置 + * @default center + */ + contentPosition?: DividerContentPosition; + + /** + * 是否使用虚线 + * @default false + */ + dashed?: boolean; + + /** + * 边框样式 + */ + borderStyle?: string; + + /** + * 分割线颜色 + */ + color?: string; + + /** + * 外边距 + */ + margin?: string | number; + + /** + * 分割线尺寸 + * @default medium + */ + size?: DividerSize; +} + +// Divider组件的Emits接口 +export interface DividerEmits { + // 当前组件暂无事件定义,保留接口以备扩展 +} diff --git a/src/components/Empty/Empty.test.ts b/src/components/Empty/Empty.test.ts new file mode 100644 index 0000000..9ff36bc --- /dev/null +++ b/src/components/Empty/Empty.test.ts @@ -0,0 +1,140 @@ +import { describe, it, expect } from 'vitest'; +import { mount } from '@vue/test-utils'; +import KEmpty from './index.vue'; + +describe('Empty组件测试', () => { + // 基础渲染测试 + it('组件能够正常渲染', () => { + const wrapper = mount(KEmpty); + expect(wrapper.exists()).toBe(true); + expect(wrapper.classes('k-empty')).toBe(true); + }); + + // 默认属性测试 + it('默认尺寸为medium', () => { + const wrapper = mount(KEmpty); + expect(wrapper.classes('k-empty--size-medium')).toBe(true); + }); + + it('默认描述为"暂无数据"', () => { + const wrapper = mount(KEmpty); + const descriptionElement = wrapper.find('.k-empty__description'); + expect(descriptionElement.text()).toBe('暂无数据'); + }); + + // 尺寸测试 + it('可以设置尺寸为small', () => { + const wrapper = mount(KEmpty, { + props: { + size: 'small' + } + }); + expect(wrapper.classes('k-empty--size-small')).toBe(true); + }); + + it('可以设置尺寸为large', () => { + const wrapper = mount(KEmpty, { + props: { + size: 'large' + } + }); + expect(wrapper.classes('k-empty--size-large')).toBe(true); + }); + + // 自定义图片测试 + it('可以自定义图片图标', () => { + const customIcon = 'custom-empty'; + const wrapper = mount(KEmpty, { + props: { + image: customIcon + } + }); + // 查找Icon组件并检查name属性 + const iconComponent = wrapper.findComponent({ name: 'KIcon' }); + expect(iconComponent.exists()).toBe(true); + expect(iconComponent.props('name')).toBe(customIcon); + }); + + // 自定义描述测试 + it('可以自定义描述文本', () => { + const customDescription = '没有找到任何内容'; + const wrapper = mount(KEmpty, { + props: { + description: customDescription + } + }); + const descriptionElement = wrapper.find('.k-empty__description'); + expect(descriptionElement.text()).toBe(customDescription); + }); + + // 图片插槽测试 + it('可以使用图片插槽自定义图标', () => { + const customImage = '
自定义图片
'; + const wrapper = mount(KEmpty, { + slots: { + image: customImage + } + }); + // 检查是否渲染了自定义图片而不是默认Icon组件 + const customImageElement = wrapper.find('.custom-image'); + expect(customImageElement.exists()).toBe(true); + }); + + // 描述插槽测试 + it('可以使用描述插槽自定义描述', () => { + const customDescription = '自定义描述'; + const wrapper = mount(KEmpty, { + slots: { + description: customDescription + } + }); + // 检查是否渲染了自定义描述 + const customDescriptionElement = wrapper.find('.custom-description'); + expect(customDescriptionElement.exists()).toBe(true); + }); + + // 内容插槽测试 + it('可以使用内容插槽添加额外内容', () => { + const customContent = ''; + const wrapper = mount(KEmpty, { + slots: { + default: customContent + } + }); + // 检查是否渲染了自定义内容 + const customContentElement = wrapper.find('.custom-button'); + expect(customContentElement.exists()).toBe(true); + // 检查自定义内容是否在正确的容器中 + const contentContainer = wrapper.find('.k-empty__content'); + expect(contentContainer.exists()).toBe(true); + // 使用DOM节点进行contains检查 + expect(contentContainer.element.contains(customContentElement.element)).toBe(true); + }); + + // 没有内容插槽时不显示内容容器测试 + it('没有内容插槽时不显示内容容器', () => { + const wrapper = mount(KEmpty); + const contentContainer = wrapper.find('.k-empty__content'); + expect(contentContainer.exists()).toBe(false); + }); + + // 组合使用插槽测试 + it('可以同时使用多个插槽', () => { + const customImage = '
自定义图片
'; + const customDescription = '自定义描述'; + const customContent = ''; + + const wrapper = mount(KEmpty, { + slots: { + image: customImage, + description: customDescription, + default: customContent + } + }); + + // 检查所有自定义内容是否都被渲染 + expect(wrapper.find('.custom-image').exists()).toBe(true); + expect(wrapper.find('.custom-description').exists()).toBe(true); + expect(wrapper.find('.custom-button').exists()).toBe(true); + }); +}); \ No newline at end of file diff --git a/src/components/Empty/README.md b/src/components/Empty/README.md new file mode 100644 index 0000000..b097c7a --- /dev/null +++ b/src/components/Empty/README.md @@ -0,0 +1,206 @@ +# Empty 空状态 + +用于空状态显示,如暂无数据、暂无内容等场景。 + +## 基本用法 + +### 默认空状态 + +显示默认的空状态,包含默认图标和默认文本。 + +```vue + +``` + +### 自定义描述 + +可以自定义空状态的描述文本。 + +```vue + +``` + +### 自定义图标 + +通过设置 `image` 属性可以自定义空状态的图标。 + +```vue + +``` + +### 不同尺寸 + +通过 `size` 属性可以设置空状态的尺寸。 + +```vue + + + +``` + +### 底部操作区 + +可以通过默认插槽添加底部操作区域。 + +```vue + + 添加数据 + +``` + +### 完全自定义内容 + +可以通过命名插槽完全自定义空状态的各个部分。 + +```vue + + + +
+ 返回 + 刷新 +
+
+``` + +## API + +### Props + +| 属性名 | 类型 | 说明 | 默认值 | 可选值 | +|-------|-----|------|-------|-------| +| image | string | 图标名称 | 'empty' | 任意有效的图标名称 | +| description | string | 描述文本 | '暂无数据' | - | +| size | string | 空状态尺寸 | 'medium' | 'small', 'medium', 'large' | +| imageSize | number \| string | 图标尺寸(可选) | - | CSS 尺寸值或数字 | + +### Slots + +| 插槽名 | 说明 | +|-------|-----| +| default | 底部内容区域,通常用于放置操作按钮 | +| image | 自定义图像/图标区域 | +| description | 自定义描述文本区域 | + +## 类型定义 + +组件导出以下类型定义,可以在 TypeScript 项目中使用: + +```typescript +import { EmptyProps } from '@/components/Empty'; +``` + +## 示例代码 + +以下是一个更复杂的使用示例: + +```vue + + + +``` \ No newline at end of file diff --git a/src/components/Empty/index.scss b/src/components/Empty/index.scss new file mode 100644 index 0000000..d801ff8 --- /dev/null +++ b/src/components/Empty/index.scss @@ -0,0 +1,48 @@ +// Empty组件样式 + + +.k-empty { + box-sizing: border-box; + margin: 0; + padding: 0; + color: $color-text-primary; + font-size: $font-size-md; + line-height: 1.5715; + text-align: center; + + // 尺寸样式 + &--size-small { + .k-empty__icon { + font-size: 48px; + } + } + + &--size-medium { + .k-empty__icon { + font-size: 64px; + } + } + + &--size-large { + .k-empty__icon { + font-size: 80px; + } + } + + &__image { + margin-bottom: 16px; + } + + &__icon { + color: $color-text-disabled; + } + + &__description { + margin-bottom: 16px; + color: $color-text-secondary; + } + + &__content { + margin-top: 16px; + } +} \ No newline at end of file diff --git a/src/components/Empty/index.ts b/src/components/Empty/index.ts new file mode 100644 index 0000000..0e30595 --- /dev/null +++ b/src/components/Empty/index.ts @@ -0,0 +1,5 @@ +import Empty from './index.vue'; +export * from './types'; + +export { Empty }; +export default Empty; diff --git a/src/components/Empty/index.vue b/src/components/Empty/index.vue new file mode 100644 index 0000000..90cde51 --- /dev/null +++ b/src/components/Empty/index.vue @@ -0,0 +1,75 @@ + + + diff --git a/src/components/Empty/types.ts b/src/components/Empty/types.ts new file mode 100644 index 0000000..3e4c0d9 --- /dev/null +++ b/src/components/Empty/types.ts @@ -0,0 +1,6 @@ +export interface EmptyProps { + image?: string; + imageSize?: number | string; + description?: string; + size?: 'small' | 'medium' | 'large'; +} diff --git a/src/components/Grid/Grid.test.ts b/src/components/Grid/Grid.test.ts new file mode 100644 index 0000000..148f1e3 --- /dev/null +++ b/src/components/Grid/Grid.test.ts @@ -0,0 +1,217 @@ +import { describe, it, expect } from 'vitest'; +import { mount } from '@vue/test-utils'; +import KGrid from './index.vue'; + +describe('KGrid Component', () => { + // 基础渲染测试 + it('should render correctly', () => { + const wrapper = mount(KGrid); + expect(wrapper.exists()).toBe(true); + expect(wrapper.classes()).toContain('k-grid'); + }); + + // 列数设置测试 - 数字类型 + it('should set correct columns when columns is a number', () => { + const wrapper = mount(KGrid, { + props: { + columns: 3 + } + }); + expect(wrapper.attributes('style')).toContain('grid-template-columns: repeat(3, 1fr)'); + }); + + // 列数设置测试 - 数组类型 + it('should set correct columns when columns is an array', () => { + const wrapper = mount(KGrid, { + props: { + columns: ['100px', '1fr', '2fr'] + } + }); + expect(wrapper.attributes('style')).toContain('grid-template-columns: 100px 1fr 2fr'); + }); + + // 间距测试 - 数字类型 + it('should set correct gap when gap is a number', () => { + const wrapper = mount(KGrid, { + props: { + gap: 20 + } + }); + expect(wrapper.attributes('style')).toContain('gap: 20px'); + }); + + // 间距测试 - 字符串类型 + it('should set correct gap when gap is a string', () => { + const wrapper = mount(KGrid, { + props: { + gap: '2rem' + } + }); + expect(wrapper.attributes('style')).toContain('gap: 2rem'); + }); + + // 行间距测试 + it('should set correct rowGap when rowGap is provided', () => { + const wrapper = mount(KGrid, { + props: { + rowGap: 10 + } + }); + expect(wrapper.attributes('style')).toContain('row-gap: 10px'); + }); + + // 列间距测试 + it('should set correct columnGap when columnGap is provided', () => { + const wrapper = mount(KGrid, { + props: { + columnGap: 15 + } + }); + expect(wrapper.attributes('style')).toContain('column-gap: 15px'); + }); + + // 对齐方式测试 - align + it('should set correct align when align is provided', () => { + const wrapper = mount(KGrid, { + props: { + align: 'center' + } + }); + expect(wrapper.attributes('style')).toContain('align-items: center'); + }); + + // 对齐方式测试 - justify + it('should set correct justify when justify is provided', () => { + const wrapper = mount(KGrid, { + props: { + justify: 'space-between' + } + }); + expect(wrapper.attributes('style')).toContain('justify-items: space-between'); + }); + + // 响应式布局测试 - xs + it('should add responsive xs class when responsive is true and xs is provided', () => { + const wrapper = mount(KGrid, { + props: { + responsive: true, + xs: 2 + } + }); + expect(wrapper.classes()).toContain('k-grid--xs-2'); + }); + + // 响应式布局测试 - sm + it('should add responsive sm class when responsive is true and sm is provided', () => { + const wrapper = mount(KGrid, { + props: { + responsive: true, + sm: 4 + } + }); + expect(wrapper.classes()).toContain('k-grid--sm-4'); + }); + + // 响应式布局测试 - md + it('should add responsive md class when responsive is true and md is provided', () => { + const wrapper = mount(KGrid, { + props: { + responsive: true, + md: 6 + } + }); + expect(wrapper.classes()).toContain('k-grid--md-6'); + }); + + // 响应式布局测试 - lg + it('should add responsive lg class when responsive is true and lg is provided', () => { + const wrapper = mount(KGrid, { + props: { + responsive: true, + lg: 8 + } + }); + expect(wrapper.classes()).toContain('k-grid--lg-8'); + }); + + // 响应式布局测试 - xl + it('should add responsive xl class when responsive is true and xl is provided', () => { + const wrapper = mount(KGrid, { + props: { + responsive: true, + xl: 12 + } + }); + expect(wrapper.classes()).toContain('k-grid--xl-12'); + }); + + // 响应式布局测试 - 多个断点 + it('should add multiple responsive classes when multiple breakpoints are provided', () => { + const wrapper = mount(KGrid, { + props: { + responsive: true, + xs: 2, + sm: 4, + md: 6 + } + }); + expect(wrapper.classes()).toContain('k-grid--xs-2'); + expect(wrapper.classes()).toContain('k-grid--sm-4'); + expect(wrapper.classes()).toContain('k-grid--md-6'); + }); + + // 组合属性测试 + it('should work correctly with multiple props combined', () => { + const wrapper = mount(KGrid, { + props: { + columns: 4, + gap: 16, + align: 'center', + justify: 'space-around', + responsive: true, + xs: 2, + sm: 3 + } + }); + + // 检查样式 + expect(wrapper.attributes('style')).toContain('grid-template-columns: repeat(4, 1fr)'); + expect(wrapper.attributes('style')).toContain('gap: 16px'); + expect(wrapper.attributes('style')).toContain('align-items: center'); + expect(wrapper.attributes('style')).toContain('justify-items: space-around'); + + // 检查响应式类 + expect(wrapper.classes()).toContain('k-grid--xs-2'); + expect(wrapper.classes()).toContain('k-grid--sm-3'); + }); + + // 默认值测试 + it('should use default values when no props are provided', () => { + const wrapper = mount(KGrid); + + // 检查默认列数 + expect(wrapper.attributes('style')).toContain('grid-template-columns: repeat(12, 1fr)'); + + // 检查默认对齐方式 + expect(wrapper.attributes('style')).toContain('align-items: stretch'); + expect(wrapper.attributes('style')).toContain('justify-items: start'); + + // 检查默认没有响应式类 + expect(wrapper.classes()).not.toContain('k-grid--xs-'); + expect(wrapper.classes()).not.toContain('k-grid--sm-'); + expect(wrapper.classes()).not.toContain('k-grid--md-'); + expect(wrapper.classes()).not.toContain('k-grid--lg-'); + expect(wrapper.classes()).not.toContain('k-grid--xl-'); + }); + + // 插槽内容测试 + it('should render slot content correctly', () => { + const wrapper = mount(KGrid, { + slots: { + default: '
Test Item
' + } + }); + expect(wrapper.find('.grid-item').exists()).toBe(true); + expect(wrapper.text()).toContain('Test Item'); + }); +}); \ No newline at end of file diff --git a/src/components/Grid/README.md b/src/components/Grid/README.md new file mode 100644 index 0000000..270e5a0 --- /dev/null +++ b/src/components/Grid/README.md @@ -0,0 +1,133 @@ +# KGrid + +KGrid 组件是一个基于 CSS Grid 的网格布局组件,用于创建响应式、灵活的页面布局。 + +## 使用示例 + +```vue + + + +``` + +## Props + +| 属性名 | 类型 | 默认值 | 描述 | +|-------|------|-------|------| +| columns | `number \| string[]` | `12` | 列数,支持数字或数组形式,数组形式可自定义每列宽度 | +| gap | `number \| string` | `''` | 网格间距,支持数字(像素)或字符串(CSS单位) | +| rowGap | `number \| string` | `''` | 行间距,支持数字(像素)或字符串(CSS单位) | +| columnGap | `number \| string` | `''` | 列间距,支持数字(像素)或字符串(CSS单位) | +| align | `'start' \| 'center' \| 'end' \| 'stretch'` | `'stretch'` | 垂直对齐方式 | +| justify | `'start' \| 'center' \| 'end' \| 'space-between' \| 'space-around' \| 'space-evenly'` | `'start'` | 水平对齐方式 | +| responsive | `boolean` | `false` | 是否启用响应式布局 | +| xs | `number` | `0` | 超小屏幕(< 576px)下的列数 | +| sm | `number` | `0` | 小屏幕(≥ 576px)下的列数 | +| md | `number` | `0` | 中等屏幕(≥ 768px)下的列数 | +| lg | `number` | `0` | 大屏幕(≥ 992px)下的列数 | +| xl | `number` | `0` | 超大屏幕(≥ 1200px)下的列数 | + +## Slots + +| 插槽名 | 描述 | +|-------|------| +| default | 网格内容区域,放置网格项目 | + +## Events + +KGrid 组件没有提供事件。 + +## 高级用法 + +### 自定义列宽 + +```vue + + + +``` + +### 复杂布局示例 + +```vue + + + + + +``` \ No newline at end of file diff --git a/src/components/Grid/index.scss b/src/components/Grid/index.scss new file mode 100644 index 0000000..40df8f9 --- /dev/null +++ b/src/components/Grid/index.scss @@ -0,0 +1,84 @@ +// Grid组件样式 + + +.k-grid { + display: grid; + width: 100%; + + // 响应式断点 + @media (max-width: 576px) { + // fraction unit等宽列 + &--xs-1 { grid-template-columns: repeat(1, 1fr); } + &--xs-2 { grid-template-columns: repeat(2, 1fr); } + &--xs-3 { grid-template-columns: repeat(3, 1fr); } + &--xs-4 { grid-template-columns: repeat(4, 1fr); } + &--xs-5 { grid-template-columns: repeat(5, 1fr); } + &--xs-6 { grid-template-columns: repeat(6, 1fr); } + &--xs-7 { grid-template-columns: repeat(7, 1fr); } + &--xs-8 { grid-template-columns: repeat(8, 1fr); } + &--xs-9 { grid-template-columns: repeat(9, 1fr); } + &--xs-10 { grid-template-columns: repeat(10, 1fr); } + &--xs-11 { grid-template-columns: repeat(11, 1fr); } + &--xs-12 { grid-template-columns: repeat(12, 1fr); } + } + + @media (min-width: 576px) and (max-width: 768px) { + &--sm-1 { grid-template-columns: repeat(1, 1fr); } + &--sm-2 { grid-template-columns: repeat(2, 1fr); } + &--sm-3 { grid-template-columns: repeat(3, 1fr); } + &--sm-4 { grid-template-columns: repeat(4, 1fr); } + &--sm-5 { grid-template-columns: repeat(5, 1fr); } + &--sm-6 { grid-template-columns: repeat(6, 1fr); } + &--sm-7 { grid-template-columns: repeat(7, 1fr); } + &--sm-8 { grid-template-columns: repeat(8, 1fr); } + &--sm-9 { grid-template-columns: repeat(9, 1fr); } + &--sm-10 { grid-template-columns: repeat(10, 1fr); } + &--sm-11 { grid-template-columns: repeat(11, 1fr); } + &--sm-12 { grid-template-columns: repeat(12, 1fr); } + } + + @media (min-width: 768px) and (max-width: 992px) { + &--md-1 { grid-template-columns: repeat(1, 1fr); } + &--md-2 { grid-template-columns: repeat(2, 1fr); } + &--md-3 { grid-template-columns: repeat(3, 1fr); } + &--md-4 { grid-template-columns: repeat(4, 1fr); } + &--md-5 { grid-template-columns: repeat(5, 1fr); } + &--md-6 { grid-template-columns: repeat(6, 1fr); } + &--md-7 { grid-template-columns: repeat(7, 1fr); } + &--md-8 { grid-template-columns: repeat(8, 1fr); } + &--md-9 { grid-template-columns: repeat(9, 1fr); } + &--md-10 { grid-template-columns: repeat(10, 1fr); } + &--md-11 { grid-template-columns: repeat(11, 1fr); } + &--md-12 { grid-template-columns: repeat(12, 1fr); } + } + + @media (min-width: 992px) and (max-width: 1200px) { + &--lg-1 { grid-template-columns: repeat(1, 1fr); } + &--lg-2 { grid-template-columns: repeat(2, 1fr); } + &--lg-3 { grid-template-columns: repeat(3, 1fr); } + &--lg-4 { grid-template-columns: repeat(4, 1fr); } + &--lg-5 { grid-template-columns: repeat(5, 1fr); } + &--lg-6 { grid-template-columns: repeat(6, 1fr); } + &--lg-7 { grid-template-columns: repeat(7, 1fr); } + &--lg-8 { grid-template-columns: repeat(8, 1fr); } + &--lg-9 { grid-template-columns: repeat(9, 1fr); } + &--lg-10 { grid-template-columns: repeat(10, 1fr); } + &--lg-11 { grid-template-columns: repeat(11, 1fr); } + &--lg-12 { grid-template-columns: repeat(12, 1fr); } + } + + @media (min-width: 1200px) { + &--xl-1 { grid-template-columns: repeat(1, 1fr); } + &--xl-2 { grid-template-columns: repeat(2, 1fr); } + &--xl-3 { grid-template-columns: repeat(3, 1fr); } + &--xl-4 { grid-template-columns: repeat(4, 1fr); } + &--xl-5 { grid-template-columns: repeat(5, 1fr); } + &--xl-6 { grid-template-columns: repeat(6, 1fr); } + &--xl-7 { grid-template-columns: repeat(7, 1fr); } + &--xl-8 { grid-template-columns: repeat(8, 1fr); } + &--xl-9 { grid-template-columns: repeat(9, 1fr); } + &--xl-10 { grid-template-columns: repeat(10, 1fr); } + &--xl-11 { grid-template-columns: repeat(11, 1fr); } + &--xl-12 { grid-template-columns: repeat(12, 1fr); } + } +} \ No newline at end of file diff --git a/src/components/Grid/index.ts b/src/components/Grid/index.ts new file mode 100644 index 0000000..60e3277 --- /dev/null +++ b/src/components/Grid/index.ts @@ -0,0 +1,5 @@ +import Grid from './index.vue'; +export * from './types'; + +export { Grid }; +export default Grid; diff --git a/src/components/Grid/index.vue b/src/components/Grid/index.vue new file mode 100644 index 0000000..7296d6c --- /dev/null +++ b/src/components/Grid/index.vue @@ -0,0 +1,130 @@ + + + diff --git a/src/components/Grid/types.ts b/src/components/Grid/types.ts new file mode 100644 index 0000000..a4de1a6 --- /dev/null +++ b/src/components/Grid/types.ts @@ -0,0 +1,61 @@ +export interface GridProps { + /** + * 列数,支持数字或数组形式 + */ + columns?: number | string[]; + + /** + * 网格间距 + */ + gap?: number | string; + + /** + * 行间距 + */ + rowGap?: number | string; + + /** + * 列间距 + */ + columnGap?: number | string; + + /** + * 垂直对齐方式 + */ + align?: 'start' | 'center' | 'end' | 'stretch'; + + /** + * 水平对齐方式 + */ + justify?: 'start' | 'center' | 'end' | 'space-between' | 'space-around' | 'space-evenly'; + + /** + * 是否启用响应式布局 + */ + responsive?: boolean; + + /** + * 超小屏幕下的列数 + */ + xs?: number; + + /** + * 小屏幕下的列数 + */ + sm?: number; + + /** + * 中等屏幕下的列数 + */ + md?: number; + + /** + * 大屏幕下的列数 + */ + lg?: number; + + /** + * 超大屏幕下的列数 + */ + xl?: number; +} diff --git a/src/components/Icon/Icon.test.ts b/src/components/Icon/Icon.test.ts new file mode 100644 index 0000000..f2119db --- /dev/null +++ b/src/components/Icon/Icon.test.ts @@ -0,0 +1,209 @@ +import { describe, it, expect, vi } from 'vitest'; +import { mount } from '@vue/test-utils'; +import KIcon from './index.vue'; + +describe('Icon组件测试', () => { + // 基础渲染测试 + it('组件能够正常渲染', () => { + const wrapper = mount(KIcon, { + props: { + name: 'check' + } + }); + expect(wrapper.exists()).toBe(true); + // 检查el-icon元素是否有k-icon类 + expect(wrapper.find('el-icon').classes('k-icon')).toBe(true); + }); + + // 图标名称测试 + it('可以通过name属性设置图标', () => { + const wrapper = mount(KIcon, { + props: { + name: 'check' + } + }); + // 检查是否渲染了Element Plus的el-icon组件 + expect(wrapper.find('el-icon').exists()).toBe(true); + }); + + // 尺寸测试 + it('默认尺寸为inherit', () => { + const wrapper = mount(KIcon, { + props: { + name: 'check' + } + }); + expect(wrapper.find('el-icon').attributes('size')).toBeUndefined(); + }); + + it('可以设置数字类型的尺寸', () => { + const size = 24; + const wrapper = mount(KIcon, { + props: { + name: 'check', + size + } + }); + expect(wrapper.find('el-icon').attributes('size')).toBe(String(size)); + }); + + it('可以设置字符串类型的尺寸', () => { + const size = '24px'; + const wrapper = mount(KIcon, { + props: { + name: 'check', + size + } + }); + expect(wrapper.find('el-icon').attributes('size')).toBe('24'); + }); + + // 颜色测试 + it('默认颜色为inherit', () => { + const wrapper = mount(KIcon, { + props: { + name: 'check' + } + }); + expect(wrapper.find('el-icon').attributes('color')).toBe('inherit'); + }); + + it('可以设置颜色', () => { + const color = '#ff0000'; + const wrapper = mount(KIcon, { + props: { + name: 'check', + color + } + }); + expect(wrapper.find('el-icon').attributes('color')).toBe(color); + }); + + // 旋转动画测试 + it('默认不旋转', () => { + const wrapper = mount(KIcon, { + props: { + name: 'check' + } + }); + expect(wrapper.classes('k-icon--spin')).toBe(false); + }); + + it('可以设置旋转动画', () => { + const wrapper = mount(KIcon, { + props: { + name: 'check', + spin: true + } + }); + // 检查el-icon元素是否有k-icon--spin类 + expect(wrapper.find('el-icon').classes('k-icon--spin')).toBe(true); + }); + + // 动态图标加载测试 + it('可以动态切换图标', async () => { + const wrapper = mount(KIcon, { + props: { + name: 'check' + } + }); + + // 切换图标 + await wrapper.setProps({ + name: 'close' + }); + + // 检查是否重新渲染了(这里主要测试watch是否触发) + expect(wrapper.exists()).toBe(true); + }); + + // 默认图标测试 + it('当图标不存在时可以使用默认图标', () => { + const errorHandler = vi.fn(); + const wrapper = mount(KIcon, { + props: { + name: 'non-existent-icon', + defaultIcon: 'check' + }, + attrs: { + onError: errorHandler + } + }); + + // 应该触发错误事件 + expect(errorHandler).toHaveBeenCalled(); + // 但组件应该仍然存在 + expect(wrapper.exists()).toBe(true); + }); + + // 错误处理测试 + it('当图标不存在且没有默认图标时触发错误事件', () => { + const errorHandler = vi.fn(); + const wrapper = mount(KIcon, { + props: { + name: 'non-existent-icon' + }, + attrs: { + onError: errorHandler + } + }); + + // 应该触发错误事件 + expect(errorHandler).toHaveBeenCalled(); + // 检查错误信息 + expect(errorHandler.mock.calls[0][0].message).toContain('non-existent-icon'); + }); + + // 插槽测试 + it('可以使用插槽自定义图标内容', () => { + const customContent = '...'; + const wrapper = mount(KIcon, { + props: { + name: 'non-existent-icon' // 不存在的图标,会使用插槽 + }, + slots: { + default: customContent + } + }); + + // 检查是否渲染了自定义内容 + const customIcon = wrapper.find('.custom-icon'); + expect(customIcon.exists()).toBe(true); + }); + + // 组合属性测试 + it('可以同时设置多个属性', () => { + const wrapper = mount(KIcon, { + props: { + name: 'loading', + size: 32, + color: '#00ff00', + spin: true + } + }); + + expect(wrapper.find('el-icon').attributes('size')).toBe('32'); + expect(wrapper.find('el-icon').attributes('color')).toBe('#00ff00'); + // 检查el-icon元素是否有k-icon--spin类 + expect(wrapper.find('el-icon').classes('k-icon--spin')).toBe(true); + }); + + // 图标名称格式测试 + it('支持连字符格式的图标名称', () => { + const wrapper = mount(KIcon, { + props: { + name: 'check-circle' + } + }); + expect(wrapper.exists()).toBe(true); + }); + + it('支持驼峰式格式的图标名称', () => { + const wrapper = mount(KIcon, { + props: { + name: 'CheckCircle' + } + }); + expect(wrapper.exists()).toBe(true); + }); +}); \ No newline at end of file diff --git a/src/components/Icon/README.md b/src/components/Icon/README.md new file mode 100644 index 0000000..a956e46 --- /dev/null +++ b/src/components/Icon/README.md @@ -0,0 +1,203 @@ +# Icon 图标组件 + +## 介绍 + +Icon 组件是一个基于 Element Plus 图标的封装组件,提供了统一的图标使用方式。它支持多种图标尺寸、颜色设置以及旋转动画效果。 + +## 特性 + +- 📦 按需加载 Element Plus 图标,减小打包体积 +- 🎨 支持自定义尺寸和颜色 +- 🔄 内置旋转动画效果 +- 🔧 支持短横线命名和驼峰命名两种方式 +- 📱 响应式设计,适应不同屏幕尺寸 +- 🧩 与其他组件无缝集成 + +## 安装 + +Icon 组件已包含在 KnowAI UI 组件库中,您可以通过以下方式导入使用: + +```typescript +// 完整导入组件库 +import KnowAIUI from 'knowai-ui'; +import 'knowai-ui/dist/style.css'; + +// 按需导入 Icon 组件 +import { KIcon } from 'knowai-ui'; +``` + +## 基础用法 + +```vue + + + +``` + +## API + +### 属性 + +| 属性名 | 类型 | 说明 | 默认值 | 可选值 | +|-------|-----|------|-------|-------| +| name | string | 图标名称 | - | 支持短横线命名(如 'check-circle')和驼峰命名(如 'CircleCheck') | +| size | number \| string | 图标尺寸 | 'inherit' | CSS 尺寸值或数字 | +| color | string | 图标颜色 | 'inherit' | 任意有效的 CSS 颜色值 | +| spin | boolean | 是否显示旋转动画 | false | true / false | + +### 插槽 + +| 插槽名 | 说明 | +|-------|-----| +| default | 当图标不存在时显示的默认内容 | + +## 内置图标列表 + +Icon 组件目前内置了以下常用图标: + +### 基础图标 +- `check` - 选中 +- `close` - 关闭 +- `search` - 搜索 + +### 方向性图标 +- `arrow-left` - 左箭头 +- `arrow-right` - 右箭头 +- `arrow-up` - 上箭头 +- `arrow-down` - 下箭头 + +### 状态图标 +- `check-circle` - 选中圆 +- `close-circle` - 关闭圆 +- `info-circle` - 信息圆 +- `exclamation-circle` - 警告圆 + +### 功能图标 +- `image` - 图片 +- `image-error` - 图片错误 +- `preview` - 预览 +- `loading` - 加载中 +- `empty` - 空状态 + +> 注意:您也可以直接使用 Element Plus 图标的原始名称(驼峰命名)。 + +## 图标名称映射 + +为了提供更好的使用体验,Icon 组件支持短横线命名方式,并自动映射到 Element Plus 的图标名称: + +| 短横线命名 | Element Plus 图标名称 | +|-----------|---------------------| +| check-circle | CircleCheck | +| close-circle | CircleClose | +| info-circle | CircleInfoFilled | +| exclamation-circle | CircleWarningFilled | +| image | Picture | +| image-error | PictureRounded | +| preview | ZoomIn | +| loading | Loading | +| empty | Document | +| search | Search | +| close | Close | +| check | Check | +| arrow-left | ArrowLeft | +| arrow-right | ArrowRight | +| arrow-up | ArrowUp | +| arrow-down | ArrowDown | + +## 高级用法 + +### 动态切换图标 + +```vue + + + +``` + +### 结合其他组件使用 + +```vue + + + + + +``` + +## 类型定义 + +组件导出以下类型定义,可以在 TypeScript 项目中使用: + +```typescript +import { IconProps } from '@/components/Icon'; +``` + +## 注意事项 + +1. 图标组件目前只包含了常用的图标,如果需要使用其他 Element Plus 图标,请参考迁移文档扩展图标支持 +2. 当指定的图标名称不存在时,组件会显示默认插槽内容 +3. 旋转动画仅对某些图标(如 loading)有意义 +4. 图标尺寸和颜色会继承父元素的样式,也可以通过属性直接设置 + +## 性能优化 + +- 组件使用按需静态导入方式加载图标,避免了导入整个图标库 +- 图标组件实现了高效的缓存机制,避免重复加载 +- 支持 Vue 3 的响应式优化,确保图标更新时的性能 \ No newline at end of file diff --git a/src/components/Icon/__snapshots__/index.test.ts.snap b/src/components/Icon/__snapshots__/index.test.ts.snap new file mode 100644 index 0000000..a48778d --- /dev/null +++ b/src/components/Icon/__snapshots__/index.test.ts.snap @@ -0,0 +1,13 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`KIcon Component > should match snapshot with all props 1`] = ` +" + + " +`; + +exports[`KIcon Component > should match snapshot with default props 1`] = ` +" + + " +`; diff --git a/src/components/Icon/index.scss b/src/components/Icon/index.scss new file mode 100644 index 0000000..1a7428f --- /dev/null +++ b/src/components/Icon/index.scss @@ -0,0 +1,3529 @@ +// Icon组件样式 + + +.k-icon { + display: inline-flex; + align-items: center; + justify-content: center; + line-height: 1; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + vertical-align: middle; + + &--spin { + animation: rotating 2s linear infinite; + } +} + +@keyframes rotating { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} + +// 常用图标样式 +.k-icon--close { + &::before { + content: "×"; + } +} + +.k-icon--check { + &::before { + content: "✓"; + } +} + +.k-icon--arrow-left { + &::before { + content: "←"; + } +} + +.k-icon--arrow-right { + &::before { + content: "→"; + } +} + +.k-icon--arrow-up { + &::before { + content: "↑"; + } +} + +.k-icon--arrow-down { + &::before { + content: "↓"; + } +} + +.k-icon--loading { + &::before { + content: "⏳"; + } +} + +.k-icon--search { + &::before { + content: "🔍"; + } +} + +.k-icon--plus { + &::before { + content: "+"; + } +} + +.k-icon--minus { + &::before { + content: "-"; + } +} + +.k-icon--edit { + &::before { + content: "✏️"; + } +} + +.k-icon--delete { + &::before { + content: "🗑️"; + } +} + +.k-icon--download { + &::before { + content: "⬇️"; + } +} + +.k-icon--upload { + &::before { + content: "⬆️"; + } +} + +.k-icon--refresh { + &::before { + content: "🔄"; + } +} + +.k-icon--warning { + &::before { + content: "⚠️"; + } +} + +.k-icon--error { + &::before { + content: "❌"; + } +} + +.k-icon--success { + &::before { + content: "✅"; + } +} + +.k-icon--info { + &::before { + content: "ℹ️"; + } +} + +.k-icon--question { + &::before { + content: "❓"; + } +} + +.k-icon--eye { + &::before { + content: "👁️"; + } +} + +.k-icon--eye-off { + &::before { + content: "👁️‍🗨️"; + } +} + +.k-icon--heart { + &::before { + content: "❤️"; + } +} + +.k-icon--heart-filled { + &::before { + content: "💖"; + } +} + +.k-icon--star { + &::before { + content: "⭐"; + } +} + +.k-icon--star-filled { + &::before { + content: "⭐"; + } +} + +.k-icon--menu { + &::before { + content: "☰"; + } +} + +.k-icon--more { + &::before { + content: "⋯"; + } +} + +.k-icon--setting { + &::before { + content: "⚙️"; + } +} + +.k-icon--user { + &::before { + content: "👤"; + } +} + +.k-icon--users { + &::before { + content: "👥"; + } +} + +.k-icon--home { + &::before { + content: "🏠"; + } +} + +.k-icon--folder { + &::before { + content: "📁"; + } +} + +.k-icon--file { + &::before { + content: "📄"; + } +} + +.k-icon--image { + &::before { + content: "🖼️"; + } +} + +.k-icon--video { + &::before { + content: "🎥"; + } +} + +.k-icon--audio { + &::before { + content: "🎵"; + } +} + +.k-icon--link { + &::before { + content: "🔗"; + } +} + +.k-icon--share { + &::before { + content: "🔗"; + } +} + +.k-icon--copy { + &::before { + content: "📋"; + } +} + +.k-icon--paste { + &::before { + content: "📋"; + } +} + +.k-icon--cut { + &::before { + content: "✂️"; + } +} + +.k-icon--calendar { + &::before { + content: "📅"; + } +} + +.k-icon--clock { + &::before { + content: "🕐"; + } +} + +.k-icon--bell { + &::before { + content: "🔔"; + } +} + +.k-icon--mail { + &::before { + content: "✉️"; + } +} + +.k-icon--phone { + &::before { + content: "📞"; + } +} + +.k-icon--location { + &::before { + content: "📍"; + } +} + +.k-icon--map { + &::before { + content: "🗺️"; + } +} + +.k-icon--cart { + &::before { + content: "🛒"; + } +} + +.k-icon--tag { + &::before { + content: "🏷️"; + } +} + +.k-icon--bookmark { + &::before { + content: "🔖"; + } +} + +.k-icon--filter { + &::before { + content: "🔍"; + } +} + +.k-icon--sort { + &::before { + content: "↕️"; + } +} + +.k-icon--expand { + &::before { + content: "⬇️"; + } +} + +.k-icon--collapse { + &::before { + content: "⬆️"; + } +} + +.k-icon--fullscreen { + &::before { + content: "⛶"; + } +} + +.k-icon--exit-fullscreen { + &::before { + content: "⛶"; + } +} + +.k-icon--lock { + &::before { + content: "🔒"; + } +} + +.k-icon--unlock { + &::before { + content: "🔓"; + } +} + +.k-icon--shield { + &::before { + content: "🛡️"; + } +} + +.k-icon--key { + &::before { + content: "🔑"; + } +} + +.k-icon--database { + &::before { + content: "🗄️"; + } +} + +.k-icon--server { + &::before { + content: "🖥️"; + } +} + +.k-icon--cloud { + &::before { + content: "☁️"; + } +} + +.k-icon--wifi { + &::before { + content: "📶"; + } +} + +.k-icon--battery { + &::before { + content: "🔋"; + } +} + +.k-icon--signal { + &::before { + content: "📶"; + } +} + +.k-icon--bluetooth { + &::before { + content: "📶"; + } +} + +.k-icon--usb { + &::before { + content: "🔌"; + } +} + +.k-icon--print { + &::before { + content: "🖨️"; + } +} + +.k-icon--scan { + &::before { + content: "📷"; + } +} + +.k-icon--camera { + &::before { + content: "📷"; + } +} + +.k-icon--microphone { + &::before { + content: "🎤"; + } +} + +.k-icon--volume { + &::before { + content: "🔊"; + } +} + +.k-icon--mute { + &::before { + content: "🔇"; + } +} + +.k-icon--play { + &::before { + content: "▶️"; + } +} + +.k-icon--pause { + &::before { + content: "⏸️"; + } +} + +.k-icon--stop { + &::before { + content: "⏹️"; + } +} + +.k-icon--next { + &::before { + content: "⏭️"; + } +} + +.k-icon--previous { + &::before { + content: "⏮️"; + } +} + +.k-icon--forward { + &::before { + content: "⏩"; + } +} + +.k-icon--backward { + &::before { + content: "⏪"; + } +} + +.k-icon--record { + &::before { + content: "⏺️"; + } +} + +.k-icon--replay { + &::before { + content: "🔄"; + } +} + +.k-icon--shuffle { + &::before { + content: "🔀"; + } +} + +.k-icon--repeat { + &::before { + content: "🔁"; + } +} + +.k-icon--loop { + &::before { + content: "🔁"; + } +} + +.k-icon--zoom-in { + &::before { + content: "🔍"; + } +} + +.k-icon--zoom-out { + &::before { + content: "🔍"; + } +} + +.k-icon--fit { + &::before { + content: "🔍"; + } +} + +.k-icon--fullscreen-exit { + &::before { + content: "⛶"; + } +} + +.k-icon--grid { + &::before { + content: "⊞"; + } +} + +.k-icon--list { + &::before { + content: "☰"; + } +} + +.k-icon--th-large { + &::before { + content: "⊞"; + } +} + +.k-icon--th { + &::before { + content: "⊞"; + } +} + +.k-icon--th-list { + &::before { + content: "☰"; + } +} + +.k-icon--align-left { + &::before { + content: "⬅️"; + } +} + +.k-icon--align-center { + &::before { + content: "↔️"; + } +} + +.k-icon--align-right { + &::before { + content: "➡️"; + } +} + +.k-icon--align-justify { + &::before { + content: "↔️"; + } +} + +.k-icon--bold { + &::before { + content: "B"; + } +} + +.k-icon--italic { + &::before { + content: "I"; + } +} + +.k-icon--underline { + &::before { + content: "U"; + } +} + +.k-icon--strikethrough { + &::before { + content: "S"; + } +} + +.k-icon--subscript { + &::before { + content: "ₓ"; + } +} + +.k-icon--superscript { + &::before { + content: "ˣ"; + } +} + +.k-icon--quote-left { + &::before { + content: """; + } +} + +.k-icon--quote-right { + &::before { + content: """; + } +} + +.k-icon--code { + &::before { + content: ""; + } +} + +.k-icon--terminal { + &::before { + content: ">"; + } +} + +.k-icon--terminal-square { + &::before { + content: "⧉"; + } +} + +.k-icon--file-code { + &::before { + content: ""; + } +} + +.k-icon--file-excel { + &::before { + content: "📊"; + } +} + +.k-icon--file-pdf { + &::before { + content: "📄"; + } +} + +.k-icon--file-word { + &::before { + content: "📄"; + } +} + +.k-icon--file-powerpoint { + &::before { + content: "📊"; + } +} + +.k-icon--file-archive { + &::before { + content: "📦"; + } +} + +.k-icon--file-audio { + &::before { + content: "🎵"; + } +} + +.k-icon--file-video { + &::before { + content: "🎥"; + } +} + +.k-icon--file-image { + &::before { + content: "🖼️"; + } +} + +.k-icon--file-text { + &::before { + content: "📄"; + } +} + +.k-icon--folder-open { + &::before { + content: "📂"; + } +} + +.k-icon--folder-plus { + &::before { + content: "📁"; + } +} + +.k-icon--folder-minus { + &::before { + content: "📁"; + } +} + +.k-icon--smile { + &::before { + content: "😊"; + } +} + +.k-icon--meh { + &::before { + content: "😐"; + } +} + +.k-icon--frown { + &::before { + content: "😞"; + } +} + +.k-icon--angry { + &::before { + content: "😠"; + } +} + +.k-icon--tired { + &::before { + content: "😫"; + } +} + +.k-icon--dizzy { + &::before { + content: "😵"; + } +} + +.k-icon--sad-tear { + &::before { + content: "😢"; + } +} + +.k-icon--grin { + &::before { + content: "😃"; + } +} + +.k-icon--grin-hearts { + &::before { + content: "😍"; + } +} + +.k-icon--grin-beam { + &::before { + content: "😄"; + } +} + +.k-icon--grin-stars { + &::before { + content: "🤩"; + } +} + +.k-icon--grin-tongue { + &::before { + content: "😛"; + } +} + +.k-icon--grin-wink { + &::before { + content: "😉"; + } +} + +.k-icon--laugh { + &::before { + content: "😆"; + } +} + +.k-icon--laugh-beam { + &::before { + content: "😄"; + } +} + +.k-icon--laugh-squint { + &::before { + content: "😆"; + } +} + +.k-icon--laugh-wink { + &::before { + content: "😉"; + } +} + +.k-icon--kiss { + &::before { + content: "😗"; + } +} + +.k-icon--kiss-beam { + &::before { + content: "😗"; + } +} + +.k-icon--kiss-wink-heart { + &::before { + content: "😘"; + } +} + +.k-icon--surprise { + &::before { + content: "😮"; + } +} + +.k-icon--tongue { + &::before { + content: "😛"; + } +} + +.k-icon--wink { + &::before { + content: "😉"; + } +} + +.k-icon--thumbs-up { + &::before { + content: "👍"; + } +} + +.k-icon--thumbs-down { + &::before { + content: "👎"; + } +} + +.k-icon--hand-peace { + &::before { + content: "✌️"; + } +} + +.k-icon--hand-rock { + &::before { + content: "✊"; + } +} + +.k-icon--hand-paper { + &::before { + content: "✋"; + } +} + +.k-icon--hand-scissors { + &::before { + content: "✌️"; + } +} + +.k-icon--hand-spock { + &::before { + content: "🖖"; + } +} + +.k-icon--hand-point-left { + &::before { + content: "👈"; + } +} + +.k-icon--hand-point-right { + &::before { + content: "👉"; + } +} + +.k-icon--hand-point-up { + &::before { + content: "👆"; + } +} + +.k-icon--hand-point-down { + &::before { + content: "👇"; + } +} + +.k-icon--hand-holding { + &::before { + content: "🤲"; + } +} + +.k-icon--hand-holding-heart { + &::before { + content: "🫶"; + } +} + +.k-icon--hand-holding-usd { + &::before { + content: "💵"; + } +} + +.k-icon--hand-holding-water { + &::before { + content: "💧"; + } +} + +.k-icon--hands { + &::before { + content: "🤝"; + } +} + +.k-icon--hands-helping { + &::before { + content: "🤝"; + } +} + +.k-icon--handshake { + &::before { + content: "🤝"; + } +} + +.k-icon--handshake-alt { + &::before { + content: "🤝"; + } +} + +.k-icon--handshake-simple { + &::before { + content: "🤝"; + } +} + +.k-icon--handshake-simple-slash { + &::before { + content: "🤝"; + } +} + +.k-icon--handshake-slash { + &::before { + content: "🤝"; + } +} + +.k-icon--hamsa { + &::before { + content: "🤚"; + } +} + +.k-icon--pray { + &::before { + content: "🙏"; + } +} + +.k-icon--pray-hands { + &::before { + content: "🙏"; + } +} + +.k-icon--sign { + &::before { + content: "📜"; + } +} + +.k-icon--sign-language { + &::before { + content: "🤟"; + } +} + +.k-icon--asl-interpreting { + &::before { + content: "🤟"; + } +} + +.k-icon--deaf { + &::before { + content: "🧏"; + } +} + +.k-icon--deafness { + &::before { + content: "🧏"; + } +} + +.k-icon--hard-of-hearing { + &::before { + content: "🧏"; + } +} + +.k-icon--assistive-listening-systems { + &::before { + content: "🦻"; + } +} + +.k-icon--american-sign-language-interpreting { + &::before { + content: "🤟"; + } +} + +.k-icon--audio-description { + &::before { + content: "🔊"; + } +} + +.k-icon--braille { + &::before { + content: "🧱"; + } +} + +.k-icon--closed-captioning { + &::before { + content: "🔇"; + } +} + +.k-icon--closed-captioning-sign { + &::before { + content: "🔇"; + } +} + +.k-icon--low-vision { + &::before { + content: "👁️"; + } +} + +.k-icon--sign-language { + &::before { + content: "🤟"; + } +} + +.k-icon--universal-access { + &::before { + content: "♿"; + } +} + +.k-icon--wheelchair { + &::before { + content: "♿"; + } +} + +.k-icon--wheelchair-alt { + &::before { + content: "♿"; + } +} + +.k-icon--blind { + &::before { + content: "👁️"; + } +} + +.k-icon--tty { + &::before { + content: "📞"; + } +} + +.k-icon--volume-low { + &::before { + content: "🔉"; + } +} + +.k-icon--volume-high { + &::before { + content: "🔊"; + } +} + +.k-icon--volume-mute { + &::before { + content: "🔇"; + } +} + +.k-icon--volume-off { + &::before { + content: "🔇"; + } +} + +.k-icon--volume-up { + &::before { + content: "🔊"; + } +} + +.k-icon--volume-down { + &::before { + content: "🔉"; + } +} + +.k-icon--bell-slash { + &::before { + content: "🔕"; + } +} + +.k-icon--comment { + &::before { + content: "💬"; + } +} + +.k-icon--comment-alt { + &::before { + content: "💬"; + } +} + +.k-icon--comment-dots { + &::before { + content: "💬"; + } +} + +.k-icon--comment-slash { + &::before { + content: "💬"; + } +} + +.k-icon--comments { + &::before { + content: "💬"; + } +} + +.k-icon--comments-alt { + &::before { + content: "💬"; + } +} + +.k-icon--envelope { + &::before { + content: "✉️"; + } +} + +.k-icon--envelope-open { + &::before { + content: "📧"; + } +} + +.k-icon--envelope-open-text { + &::before { + content: "📧"; + } +} + +.k-icon--envelope-square { + &::before { + content: "📧"; + } +} + +.k-icon--paper-plane { + &::before { + content: "✈️"; + } +} + +.k-icon--paper-plane-alt { + &::before { + content: "✈️"; + } +} + +.k-icon--reply { + &::before { + content: "↩️"; + } +} + +.k-icon--reply-all { + &::before { + content: "↩️"; + } +} + +.k-icon--share-alt { + &::before { + content: "🔗"; + } +} + +.k-icon--share-alt-square { + &::before { + content: "🔗"; + } +} + +.k-icon--share-square { + &::before { + content: "🔗"; + } +} + +.k-icon--share { + &::before { + content: "🔗"; + } +} + +.k-icon--bookmark { + &::before { + content: "🔖"; + } +} + +.k-icon--bookmark-alt { + &::before { + content: "🔖"; + } +} + +.k-icon--flag { + &::before { + content: "🚩"; + } +} + +.k-icon--flag-usa { + &::before { + content: "🇺🇸"; + } +} + +.k-icon--flag-checkered { + &::before { + content: "🏁"; + } +} + +.k-icon--tag { + &::before { + content: "🏷️"; + } +} + +.k-icon--tags { + &::before { + content: "🏷️"; + } +} + +.k-icon--certificate { + &::before { + content: "🏆"; + } +} + +.k-icon--award { + &::before { + content: "🏆"; + } +} + +.k-icon--medal { + &::before { + content: "🏅"; + } +} + +.k-icon--trophy { + &::before { + content: "🏆"; + } +} + +.k-icon--gift { + &::before { + content: "🎁"; + } +} + +.k-icon--birthday-cake { + &::before { + content: "🎂"; + } +} + +.k-icon--glass-cheers { + &::before { + content: "🥂"; + } +} + +.k-icon--guitar { + &::before { + content: "🎸"; + } +} + +.k-icon--headphones { + &::before { + content: "🎧"; + } +} + +.k-icon--music { + &::before { + content: "🎵"; + } +} + +.k-icon--record-vinyl { + &::before { + content: "💿"; + } +} + +.k-icon--compact-disc { + &::before { + content: "💿"; + } +} + +.k-icon--drum { + &::before { + content: "🥁"; + } +} + +.k-icon--drum-steelpan { + &::before { + content: "🥁"; + } +} + +.k-icon--microphone { + &::before { + content: "🎤"; + } +} + +.k-icon--microphone-alt { + &::before { + content: "🎤"; + } +} + +.k-icon--microphone-alt-slash { + &::before { + content: "🎤"; + } +} + +.k-icon--microphone-slash { + &::before { + content: "🎤"; + } +} + +.k-icon--video { + &::before { + content: "🎥"; + } +} + +.k-icon--video-slash { + &::before { + content: "🎥"; + } +} + +.k-icon--camera { + &::before { + content: "📷"; + } +} + +.k-icon--camera-alt { + &::before { + content: "📷"; + } +} + +.k-icon--camera-retro { + &::before { + content: "📷"; + } +} + +.k-icon--film { + &::before { + content: "🎬"; + } +} + +.k-icon--video-camera { + &::before { + content: "🎥"; + } +} + +.k-icon--binoculars { + &::before { + content: "🔭"; + } +} + +.k-icon--telescope { + &::before { + content: "🔭"; + } +} + +.k-icon--microscope { + &::before { + content: "🔬"; + } +} + +.k-icon--dna { + &::before { + content: "🧬"; + } +} + +.k-icon--atom { + &::before { + content: "⚛️"; + } +} + +.k-icon--flask { + &::before { + content: "🧪"; + } +} + +.k-icon--magnet { + &::before { + content: "🧲"; + } +} + +.k-icon--vial { + &::before { + content: "🧪"; + } +} + +.k-icon--vials { + &::before { + content: "🧪"; + } +} + +.k-icon--bacteria { + &::before { + content: "🦠"; + } +} + +.k-icon--virus { + &::before { + content: "🦠"; + } +} + +.k-icon--virus-slash { + &::before { + content: "🦠"; + } +} + +.k-icon--syringe { + &::before { + content: "💉"; + } +} + +.k-icon--pills { + &::before { + content: "💊"; + } +} + +.k-icon--prescription-bottle { + &::before { + content: "💊"; + } +} + +.k-icon--prescription-bottle-alt { + &::before { + content: "💊"; + } +} + +.k-icon--stethoscope { + &::before { + content: "🩺"; + } +} + +.k-icon--tooth { + &::before { + content: "🦷"; + } +} + +.k-icon--teeth { + &::before { + content: "🦷"; + } +} + +.k-icon--teeth-open { + &::before { + content: "🦷"; + } +} + +.k-icon--user-md { + &::before { + content: "👨‍⚕️"; + } +} + +.k-icon--user-nurse { + &::before { + content: "👩‍⚕️"; + } +} + +.k-icon--crutch { + &::before { + content: "🦯"; + } +} + +.k-icon--wheelchair { + &::before { + content: "♿"; + } +} + +.k-icon--procedures { + &::before { + content: "🏥"; + } +} + +.k-icon--hospital { + &::before { + content: "🏥"; + } +} + +.k-icon--hospital-alt { + &::before { + content: "🏥"; + } +} + +.k-icon--hospital-symbol { + &::before { + content: "🏥"; + } +} + +.k-icon--ambulance { + &::before { + content: "🚑"; + } +} + +.k-icon--helicopter { + &::before { + content: "🚁"; + } +} + +.k-icon--plane { + &::before { + content: "✈️"; + } +} + +.k-icon--plane-alt { + &::before { + content: "✈️"; + } +} + +.k-icon--fighter-jet { + &::before { + content: "✈️"; + } +} + +.k-icon--rocket { + &::before { + content: "🚀"; + } +} + +.k-icon--space-shuttle { + &::before { + content: "🚀"; + } +} + +.k-icon--satellite { + &::before { + content: "🛰️"; + } +} + +.k-icon--satellite-dish { + &::before { + content: "📡"; + } +} + +.k-icon--space-station { + &::before { + content: "🛰️"; + } +} + +.k-icon--moon { + &::before { + content: "🌙"; + } +} + +.k-icon--planet-ringed { + &::before { + content: "🪐"; + } +} + +.k-icon--globe { + &::before { + content: "🌍"; + } +} + +.k-icon--globe-africa { + &::before { + content: "🌍"; + } +} + +.k-icon--globe-americas { + &::before { + content: "🌎"; + } +} + +.k-icon--globe-asia { + &::before { + content: "🌏"; + } +} + +.k-icon--globe-europe { + &::before { + content: "🌍"; + } +} + +.k-icon--earth { + &::before { + content: "🌍"; + } +} + +.k-icon--earth-africa { + &::before { + content: "🌍"; + } +} + +.k-icon--earth-americas { + &::before { + content: "🌎"; + } +} + +.k-icon--earth-asia { + &::before { + content: "🌏"; + } +} + +.k-icon--earth-europe { + &::before { + content: "🌍"; + } +} + +.k-icon--map { + &::before { + content: "🗺️"; + } +} + +.k-icon--map-marked { + &::before { + content: "🗺️"; + } +} + +.k-icon--map-marked-alt { + &::before { + content: "🗺️"; + } +} + +.k-icon--map-pin { + &::before { + content: "📍"; + } +} + +.k-icon--map-marker { + &::before { + content: "📍"; + } +} + +.k-icon--map-marker-alt { + &::before { + content: "📍"; + } +} + +.k-icon--location-arrow { + &::before { + content: "➡️"; + } +} + +.k-icon--compass { + &::before { + content: "🧭"; + } +} + +.k-icon--route { + &::before { + content: "🛣️"; + } +} + +.k-icon--directions { + &::before { + content: "🛣️"; + } +} + +.k-icon--street-view { + &::before { + content: "🗺️"; + } +} + +.k-icon--traffic-light { + &::before { + content: "🚦"; + } +} + +.k-icon--road { + &::before { + content: "🛣️"; + } +} + +.k-icon--bridge { + &::before { + content: "🌉"; + } +} + +.k-icon--train { + &::before { + content: "🚂"; + } +} + +.k-icon--subway { + &::before { + content: "🚇"; + } +} + +.k-icon--bus { + &::before { + content: "🚌"; + } +} + +.k-icon--bus-alt { + &::before { + content: "🚌"; + } +} + +.k-icon--tram { + &::before { + content: "🚊"; + } +} + +.k-icon--car { + &::before { + content: "🚗"; + } +} + +.k-icon--car-alt { + &::before { + content: "🚗"; + } +} + +.k-icon--taxi { + &::before { + content: "🚕"; + } +} + +.k-icon--truck { + &::before { + content: "🚚"; + } +} + +.k-icon--truck-monster { + &::before { + content: "🚚"; + } +} + +.k-icon--truck-pickup { + &::before { + content: "🚚"; + } +} + +.k-icon--shipping-fast { + &::before { + content: "🚚"; + } +} + +.k-icon--bicycle { + &::before { + content: "🚲"; + } +} + +.k-icon--motorcycle { + &::before { + content: "🏍️"; + } +} + +.k-icon--ship { + &::before { + content: "🚢"; + } +} + +.k-icon--anchor { + &::before { + content: "⚓"; + } +} + +.k-icon--life-ring { + &::before { + content: "⛑️"; + } +} + +.k-icon--life-buoy { + &::before { + content: "⛑️"; + } +} + +.k-icon--life-preserver { + &::before { + content: "⛑️"; + } +} + +.k-icon--buoy { + &::before { + content: "⛑️"; + } +} + +.k-icon--swimmer { + &::before { + content: "🏊"; + } +} + +.k-icon--swimming-pool { + &::before { + content: "🏊"; + } +} + +.k-icon--hot-tub { + &::before { + content: "🛁"; + } +} + +.k-icon--bath { + &::before { + content: "🛁"; + } +} + +.k-icon--bathtub { + &::before { + content: "🛁"; + } +} + +.k-icon--shower { + &::before { + content: "🚿"; + } +} + +.k-icon--sink { + &::before { + content: "🚿"; + } +} + +.k-icon--toilet { + &::before { + content: "🚽"; + } +} + +.k-icon--toilet-paper { + &::before { + content: "🧻"; + } +} + +.k-icon--toilet-paper-alt { + &::before { + content: "🧻"; + } +} + +.k-icon--restroom { + &::before { + content: "🚻"; + } +} + +.k-icon--men { + &::before { + content: "🚹"; + } +} + +.k-icon--women { + &::before { + content: "🚺"; + } +} + +.k-icon--baby { + &::before { + content: "👶"; + } +} + +.k-icon--baby-carriage { + &::before { + content: "🍼"; + } +} + +.k-icon--bed { + &::before { + content: "🛏️"; + } +} + +.k-icon--couch { + &::before { + content: "🛋️"; + } +} + +.k-icon--chair { + &::before { + content: "🪑"; + } +} + +.k-icon--tv { + &::before { + content: "📺"; + } +} + +.k-icon--tv-alt { + &::before { + content: "📺"; + } +} + +.k-icon--desktop { + &::before { + content: "🖥️"; + } +} + +.k-icon--desktop-alt { + &::before { + content: "🖥️"; + } +} + +.k-icon--laptop { + &::before { + content: "💻"; + } +} + +.k-icon--laptop-code { + &::before { + content: "💻"; + } +} + +.k-icon--laptop-medical { + &::before { + content: "💻"; + } +} + +.k-icon--tablet { + &::before { + content: "📱"; + } +} + +.k-icon--tablet-alt { + &::before { + content: "📱"; + } +} + +.k-icon--mobile { + &::before { + content: "📱"; + } +} + +.k-icon--mobile-alt { + &::before { + content: "📱"; + } +} + +.k-icon--phone { + &::before { + content: "📞"; + } +} + +.k-icon--phone-alt { + &::before { + content: "📞"; + } +} + +.k-icon--phone-slash { + &::before { + content: "📞"; + } +} + +.k-icon--phone-volume { + &::before { + content: "📞"; + } +} + +.k-icon--fax { + &::before { + content: "📠"; + } +} + +.k-icon--bell { + &::before { + content: "🔔"; + } +} + +.k-icon--bell-slash { + &::before { + content: "🔕"; + } +} + +.k-icon--door-open { + &::before { + content: "🚪"; + } +} + +.k-icon--door-closed { + &::before { + content: "🚪"; + } +} + +.k-icon--window-maximize { + &::before { + content: "🔳"; + } +} + +.k-icon--window-minimize { + &::before { + content: "🔳"; + } +} + +.k-icon--window-restore { + &::before { + content: "🔳"; + } +} + +.k-icon--window-close { + &::before { + content: "🔳"; + } +} + +.k-icon--window-alt { + &::before { + content: "🔳"; + } +} + +.k-icon--archway { + &::before { + content: "🏛️"; + } +} + +.k-icon--church { + &::before { + content: "⛪"; + } +} + +.k-icon--mosque { + &::before { + content: "🕌"; + } +} + +.k-icon--synagogue { + &::before { + content: "🕍"; + } +} + +.k-icon--place-of-worship { + &::before { + content: "🛐"; + } +} + +.k-icon--home { + &::before { + content: "🏠"; + } +} + +.k-icon--home-alt { + &::before { + content: "🏠"; + } +} + +.k-icon--home-lg { + &::before { + content: "🏠"; + } +} + +.k-icon--home-lg-alt { + &::before { + content: "🏠"; + } +} + +.k-icon--building { + &::before { + content: "🏢"; + } +} + +.k-icon--warehouse { + &::before { + content: "🏭"; + } +} + +.k-icon--industry { + &::before { + content: "🏭"; + } +} + +.k-icon--store { + &::before { + content: "🏪"; + } +} + +.k-icon--store-alt { + &::before { + content: "🏪"; + } +} + +.k-icon--shopping-bag { + &::before { + content: "🛍️"; + } +} + +.k-icon--shopping-basket { + &::before { + content: "🛒"; + } +} + +.k-icon--shopping-cart { + &::before { + content: "🛒"; + } +} + +.k-icon--shopping-cart-arrow-down { + &::before { + content: "🛒"; + } +} + +.k-icon--shopping-cart-plus { + &::before { + content: "🛒"; + } +} + +.k-icon--box { + &::before { + content: "📦"; + } +} + +.k-icon--box-open { + &::before { + content: "📦"; + } +} + +.k-icon--box-tissue { + &::before { + content: "📦"; + } +} + +.k-icon--boxes { + &::before { + content: "📦"; + } +} + +.k-icon--archive { + &::before { + content: "🗄️"; + } +} + +.k-icon--file { + &::before { + content: "📄"; + } +} + +.k-icon--file-alt { + &::before { + content: "📄"; + } +} + +.k-icon--file-archive { + &::before { + content: "📦"; + } +} + +.k-icon--file-audio { + &::before { + content: "🎵"; + } +} + +.k-icon--file-code { + &::before { + content: ""; + } +} + +.k-icon--file-contract { + &::before { + content: "📄"; + } +} + +.k-icon--file-csv { + &::before { + content: "📊"; + } +} + +.k-icon--file-download { + &::before { + content: "📥"; + } +} + +.k-icon--file-excel { + &::before { + content: "📊"; + } +} + +.k-icon--file-export { + &::before { + content: "📤"; + } +} + +.k-icon--file-image { + &::before { + content: "🖼️"; + } +} + +.k-icon--file-import { + &::before { + content: "📥"; + } +} + +.k-icon--file-invoice { + &::before { + content: "📄"; + } +} + +.k-icon--file-invoice-dollar { + &::before { + content: "💵"; + } +} + +.k-icon--file-medical { + &::before { + content: "📄"; + } +} + +.k-icon--file-medical-alt { + &::before { + content: "📄"; + } +} + +.k-icon--file-pdf { + &::before { + content: "📄"; + } +} + +.k-icon--file-powerpoint { + &::before { + content: "📊"; + } +} + +.k-icon--file-prescription { + &::before { + content: "📄"; + } +} + +.k-icon--file-signature { + &::before { + content: "📄"; + } +} + +.k-icon--file-upload { + &::before { + content: "📤"; + } +} + +.k-icon--file-video { + &::before { + content: "🎥"; + } +} + +.k-icon--file-word { + &::before { + content: "📄"; + } +} + +.k-icon--folder { + &::before { + content: "📁"; + } +} + +.k-icon--folder-minus { + &::before { + content: "📁"; + } +} + +.k-icon--folder-open { + &::before { + content: "📂"; + } +} + +.k-icon--folder-plus { + &::before { + content: "📁"; + } +} + +.k-icon--folder-tree { + &::before { + content: "📁"; + } +} + +.k-icon--newspaper { + &::before { + content: "📰"; + } +} + +.k-icon--address-book { + &::before { + content: "📇"; + } +} + +.k-icon--address-card { + &::before { + content: "🪪"; + } +} + +.k-icon--id-badge { + &::before { + content: "🪪"; + } +} + +.k-icon--id-card { + &::before { + content: "🪪"; + } +} + +.k-icon--id-card-alt { + &::before { + content: "🪪"; + } +} + +.k-icon--passport { + &::before { + content: "🪪"; + } +} + +.k-icon--portrait { + &::before { + content: "🖼️"; + } +} + +.k-icon--user { + &::before { + content: "👤"; + } +} + +.k-icon--user-alt { + &::before { + content: "👤"; + } +} + +.k-icon--user-astronaut { + &::before { + content: "👨‍🚀"; + } +} + +.k-icon--user-check { + &::before { + content: "👤"; + } +} + +.k-icon--user-circle { + &::before { + content: "👤"; + } +} + +.k-icon--user-clock { + &::before { + content: "👤"; + } +} + +.k-icon--user-cog { + &::before { + content: "👤"; + } +} + +.k-icon--user-edit { + &::before { + content: "👤"; + } +} + +.k-icon--user-friends { + &::before { + content: "👥"; + } +} + +.k-icon--user-graduate { + &::before { + content: "👨‍🎓"; + } +} + +.k-icon--user-injured { + &::before { + content: "👤"; + } +} + +.k-icon--user-lock { + &::before { + content: "👤"; + } +} + +.k-icon--user-md { + &::before { + content: "👨‍⚕️"; + } +} + +.k-icon--user-minus { + &::before { + content: "👤"; + } +} + +.k-icon--user-ninja { + &::before { + content: "🥷"; + } +} + +.k-icon--user-nurse { + &::before { + content: "👩‍⚕️"; + } +} + +.k-icon--user-plus { + &::before { + content: "👤"; + } +} + +.k-icon--user-secret { + &::before { + content: "🕵️"; + } +} + +.k-icon--user-shield { + &::before { + content: "👤"; + } +} + +.k-icon--user-slash { + &::before { + content: "👤"; + } +} + +.k-icon--user-tag { + &::before { + content: "👤"; + } +} + +.k-icon--user-tie { + &::before { + content: "👔"; + } +} + +.k-icon--user-times { + &::before { + content: "👤"; + } +} + +.k-icon--users { + &::before { + content: "👥"; + } +} + +.k-icon--users-cog { + &::before { + content: "👥"; + } +} + +.k-icon--users-slash { + &::before { + content: "👥"; + } +} + +.k-icon--hdd { + &::before { + content: "💾"; + } +} + +.k-icon--hdd-alt { + &::before { + content: "💾"; + } +} + +.k-icon--memory { + &::before { + content: "💾"; + } +} + +.k-icon--microchip { + &::before { + content: "🔲"; + } +} + +.k-icon--microchip-alt { + &::before { + content: "🔲"; + } +} + +.k-icon--sd-card { + &::before { + content: "💾"; + } +} + +.k-icon--sim-card { + &::before { + content: "📱"; + } +} + +.k-icon--usb { + &::before { + content: "🔌"; + } +} + +.k-icon--bluetooth { + &::before { + content: "📶"; + } +} + +.k-icon--bluetooth-b { + &::before { + content: "📶"; + } +} + +.k-icon--wifi { + &::before { + content: "📶"; + } +} + +.k-icon--broadcast-tower { + &::before { + content: "📡"; + } +} + +.k-icon--satellite { + &::before { + content: "🛰️"; + } +} + +.k-icon--satellite-dish { + &::before { + content: "📡"; + } +} + +.k-icon--signal { + &::before { + content: "📶"; + } +} + +.k-icon--signal-alt { + &::before { + content: "📶"; + } +} + +.k-icon--signal-alt-slash { + &::before { + content: "📶"; + } +} + +.k-icon--signal-slash { + &::before { + content: "📶"; + } +} + +.k-icon--battery-full { + &::before { + content: "🔋"; + } +} + +.k-icon--battery-three-quarters { + &::before { + content: "🔋"; + } +} + +.k-icon--battery-half { + &::before { + content: "🔋"; + } +} + +.k-icon--battery-quarter { + &::before { + content: "🔋"; + } +} + +.k-icon--battery-empty { + &::before { + content: "🔋"; + } +} + +.k-icon--battery-alt { + &::before { + content: "🔋"; + } +} + +.k-icon--battery-slash { + &::before { + content: "🔋"; + } +} + +.k-icon--plug { + &::before { + content: "🔌"; + } +} + +.k-icon--power-off { + &::before { + content: "🔌"; + } +} + +.k-icon--cog { + &::before { + content: "⚙️"; + } +} + +.k-icon--cogs { + &::before { + content: "⚙️"; + } +} + +.k-icon--gear { + &::before { + content: "⚙️"; + } +} + +.k-icon--wrench { + &::before { + content: "🔧"; + } +} + +.k-icon--tools { + &::before { + content: "🔧"; + } +} + +.k-icon--hammer { + &::before { + content: "🔨"; + } +} + +.k-icon--screwdriver { + &::before { + content: "🔧"; + } +} + +.k-icon--pencil-alt { + &::before { + content: "✏️"; + } +} + +.k-icon--pencil-ruler { + &::before { + content: "📏"; + } +} + +.k-icon--ruler { + &::before { + content: "📏"; + } +} + +.k-icon--ruler-combined { + &::before { + content: "📏"; + } +} + +.k-icon--ruler-horizontal { + &::before { + content: "📏"; + } +} + +.k-icon--ruler-vertical { + &::before { + content: "📏"; + } +} + +.k-icon--eraser { + &::before { + content: "🧹"; + } +} + +.k-icon--pen { + &::before { + content: "🖊️"; + } +} + +.k-icon--pen-alt { + &::before { + content: "🖊️"; + } +} + +.k-icon--pen-fancy { + &::before { + content: "🖊️"; + } +} + +.k-icon--pen-nib { + &::before { + content: "🖊️"; + } +} + +.k-icon--pen-square { + &::before { + content: "📝"; + } +} + +.k-icon--pencil-square { + &::before { + content: "📝"; + } +} + +.k-icon--paint-brush { + &::before { + content: "🖌️"; + } +} + +.k-icon--paint-roller { + &::before { + content: "🖌️"; + } +} + +.k-icon--palette { + &::before { + content: "🎨"; + } +} + +.k-icon--eye { + &::before { + content: "👁️"; + } +} + +.k-icon--eye-dropper { + &::before { + content: "💧"; + } +} + +.k-icon--eye-slash { + &::before { + content: "👁️‍🗨️"; + } +} + +.k-icon--glasses { + &::before { + content: "👓"; + } +} + +.k-icon--binoculars { + &::before { + content: "🔭"; + } +} + +.k-icon--microscope { + &::before { + content: "🔬"; + } +} + +.k-icon--search { + &::before { + content: "🔍"; + } +} + +.k-icon--search-dollar { + &::before { + content: "🔍"; + } +} + +.k-icon--search-location { + &::before { + content: "🔍"; + } +} + +.k-icon--search-minus { + &::before { + content: "🔍"; + } +} + +.k-icon--search-plus { + &::before { + content: "🔍"; + } +} + +.k-icon--filter { + &::before { + content: "🔍"; + } +} + +.k-icon--sort { + &::before { + content: "↕️"; + } +} + +.k-icon--sort-alpha-down { + &::before { + content: "🔤"; + } +} + +.k-icon--sort-alpha-down-alt { + &::before { + content: "🔤"; + } +} + +.k-icon--sort-alpha-up { + &::before { + content: "🔤"; + } +} + +.k-icon--sort-alpha-up-alt { + &::before { + content: "🔤"; + } +} + +.k-icon--sort-amount-down { + &::before { + content: "🔢"; + } +} + +.k-icon--sort-amount-down-alt { + &::before { + content: "🔢"; + } +} + +.k-icon--sort-amount-up { + &::before { + content: "🔢"; + } +} + +.k-icon--sort-amount-up-alt { + &::before { + content: "🔢"; + } +} + +.k-icon--sort-down { + &::before { + content: "⬇️"; + } +} + +.k-icon--sort-numeric-down { + &::before { + content: "🔢"; + } +} + +.k-icon--sort-numeric-down-alt { + &::before { + content: "🔢"; + } +} + +.k-icon--sort-numeric-up { + &::before { + content: "🔢"; + } +} + +.k-icon--sort-numeric-up-alt { + &::before { + content: "🔢"; + } +} + +.k-icon--sort-up { + &::before { + content: "⬆️"; + } +} + +.k-icon--tint { + &::before { + content: "💧"; + } +} + +.k-icon--tint-slash { + &::before { + content: "💧"; + } +} + +.k-icon--thermometer { + &::before { + content: "🌡️"; + } +} + +.k-icon--thermometer-empty { + &::before { + content: "🌡️"; + } +} + +.k-icon--thermometer-full { + &::before { + content: "🌡️"; + } +} + +.k-icon--thermometer-half { + &::before { + content: "🌡️"; + } +} + +.k-icon--thermometer-quarter { + &::before { + content: "🌡️"; + } +} + +.k-icon--thermometer-three-quarters { + &::before { + content: "🌡️"; + } +} + +.k-icon--cloud { + &::before { + content: "☁️"; + } +} + +.k-icon--cloud-download-alt { + &::before { + content: "☁️"; + } +} + +.k-icon--cloud-upload-alt { + &::before { + content: "☁️"; + } +} + +.k-icon--cloud-sun { + &::before { + content: "⛅"; + } +} + +.k-icon--cloud-moon { + &::before { + content: "☁️"; + } +} + +.k-icon--cloud-rain { + &::before { + content: "🌧️"; + } +} + +.k-icon--cloud-showers-heavy { + &::before { + content: "🌧️"; + } +} + +.k-icon--cloud-sun-rain { + &::before { + content: "🌦️"; + } +} + +.k-icon--cloud-moon-rain { + &::before { + content: "🌧️"; + } +} + +.k-icon--cloud-bolt { + &::before { + content: "⛈️"; + } +} + +.k-icon--cloud-meatball { + &::before { + content: "☁️"; + } +} + +.k-icon--cloud-music { + &::before { + content: "☁️"; + } +} + +.k-icon--smog { + &::before { + content: "🌫️"; + } +} + +.k-icon--sun { + &::before { + content: "☀️"; + } +} + +.k-icon--moon { + &::before { + content: "🌙"; + } +} + +.k-icon--star { + &::before { + content: "⭐"; + } +} + +.k-icon--star-half { + &::before { + content: "⭐"; + } +} + +.k-icon--star-half-alt { + &::before { + content: "⭐"; + } +} + +.k-icon--meteor { + &::before { + content: "☄️"; + } +} + +.k-icon--snowflake { + &::before { + content: "❄️"; + } +} + +.k-icon--icicles { + &::before { + content: "🧊"; + } +} + +.k-icon--fire { + &::before { + content: "🔥"; + } +} + +.k-icon--fire-alt { + &::before { + content: "🔥"; + } +} + +.k-icon--bolt { + &::before { + content: "⚡"; + } +} + +.k-icon--water { + &::before { + content: "💧"; + } +} + +.k-icon--leaf { + &::before { + content: "🍃"; + } +} + +.k-icon--seedling { + &::before { + content: "🌱"; + } +} + +.k-icon--tree { + &::before { + content: "🌳"; + } +} + +.k-icon--palm-tree { + &::before { + content: "🌴"; + } +} + +.k-icon--mountain { + &::before { + content: "⛰️"; + } +} + +.k-icon--umbrella { + &::before { + content: "☂️"; + } +} + +.k-icon--umbrella-beach { + &::before { + content: "🏖️"; + } +} + +.k-icon--umbrella-alt { + &::before { + content: "☂️"; + } +} + +.k-icon--wind { + &::before { + content: "💨"; + } +} + +.k-icon--temperature-high { + &::before { + content: "🌡️"; + } +} + +.k-icon--temperature-low { + &::before { + content: "🌡️"; + } +} \ No newline at end of file diff --git a/src/components/Icon/index.ts b/src/components/Icon/index.ts new file mode 100644 index 0000000..1d059e3 --- /dev/null +++ b/src/components/Icon/index.ts @@ -0,0 +1,5 @@ +import Icon from './index.vue'; +export * from './types'; + +export { Icon }; +export default Icon; diff --git a/src/components/Icon/index.vue b/src/components/Icon/index.vue new file mode 100644 index 0000000..2bb258b --- /dev/null +++ b/src/components/Icon/index.vue @@ -0,0 +1,204 @@ + + + + + diff --git a/src/components/Icon/types.ts b/src/components/Icon/types.ts new file mode 100644 index 0000000..945c8fa --- /dev/null +++ b/src/components/Icon/types.ts @@ -0,0 +1,11 @@ +export interface IconProps { + name: string; + size?: number | string; + color?: string; + spin?: boolean; + defaultIcon?: string; +} + +export interface IconEmits { + (event: 'error', data: { name: string; message: string }): void; +} diff --git a/src/components/Image/Image.test.ts b/src/components/Image/Image.test.ts new file mode 100644 index 0000000..dfa9b13 --- /dev/null +++ b/src/components/Image/Image.test.ts @@ -0,0 +1,295 @@ +import { describe, it, expect } from 'vitest'; +import { mount } from '@vue/test-utils'; +import KImage from './index.vue'; + +describe('KImage Component', () => { + // 基础渲染测试 + it('should render correctly with src prop', () => { + const wrapper = mount(KImage, { + props: { + src: 'https://example.com/image.jpg' + } + }); + expect(wrapper.exists()).toBe(true); + expect(wrapper.classes()).toContain('k-image'); + expect(wrapper.find('img').exists()).toBe(true); + expect(wrapper.find('img').attributes('src')).toBe('https://example.com/image.jpg'); + }); + + // alt属性测试 + it('should set correct alt attribute', () => { + const wrapper = mount(KImage, { + props: { + src: 'https://example.com/image.jpg', + alt: 'Test Image' + } + }); + expect(wrapper.find('img').attributes('alt')).toBe('Test Image'); + }); + + // width属性测试 - 数字类型 + it('should set correct width when width is a number', () => { + const wrapper = mount(KImage, { + props: { + src: 'https://example.com/image.jpg', + width: 200 + } + }); + expect(wrapper.attributes('style')).toContain('width: 200px'); + }); + + // width属性测试 - 字符串类型 + it('should set correct width when width is a string', () => { + const wrapper = mount(KImage, { + props: { + src: 'https://example.com/image.jpg', + width: '20rem' + } + }); + expect(wrapper.attributes('style')).toContain('width: 20rem'); + }); + + // height属性测试 - 数字类型 + it('should set correct height when height is a number', () => { + const wrapper = mount(KImage, { + props: { + src: 'https://example.com/image.jpg', + height: 150 + } + }); + expect(wrapper.attributes('style')).toContain('height: 150px'); + }); + + // height属性测试 - 字符串类型 + it('should set correct height when height is a string', () => { + const wrapper = mount(KImage, { + props: { + src: 'https://example.com/image.jpg', + height: '15rem' + } + }); + expect(wrapper.attributes('style')).toContain('height: 15rem'); + }); + + // fit属性测试 + it('should set correct object-fit style', () => { + const wrapper = mount(KImage, { + props: { + src: 'https://example.com/image.jpg', + fit: 'contain' + } + }); + expect(wrapper.find('img').attributes('style')).toContain('object-fit: contain'); + }); + + // borderRadius属性测试 - 数字类型 + it('should set correct border-radius when borderRadius is a number', () => { + const wrapper = mount(KImage, { + props: { + src: 'https://example.com/image.jpg', + borderRadius: 8 + } + }); + expect(wrapper.find('img').attributes('style')).toContain('border-radius: 8px'); + }); + + // borderRadius属性测试 - 字符串类型 + it('should set correct border-radius when borderRadius is a string', () => { + const wrapper = mount(KImage, { + props: { + src: 'https://example.com/image.jpg', + borderRadius: '10%' + } + }); + expect(wrapper.find('img').attributes('style')).toContain('border-radius: 10%'); + }); + + // 加载状态测试 + it('should show loading placeholder initially', () => { + const wrapper = mount(KImage, { + props: { + src: 'https://example.com/image.jpg' + } + }); + expect(wrapper.find('.k-image__img').classes()).toContain('k-image__img--loading'); + expect(wrapper.find('.k-image__placeholder').exists()).toBe(true); + }); + + // 加载成功测试 + it('should hide loading placeholder when image is loaded', () => { + const wrapper = mount(KImage, { + props: { + src: 'https://example.com/image.jpg' + } + }); + + // 模拟图片加载成功 + wrapper.find('img').trigger('load'); + + expect(wrapper.find('.k-image__img').classes()).not.toContain('k-image__img--loading'); + expect(wrapper.find('.k-image__placeholder').exists()).toBe(false); + }); + + // 加载失败测试 + it('should show error state when image fails to load', () => { + const wrapper = mount(KImage, { + props: { + src: 'https://example.com/image.jpg' + } + }); + + // 模拟图片加载失败 + wrapper.find('img').trigger('error'); + + expect(wrapper.find('.k-image__img').classes()).not.toContain('k-image__img--loading'); + expect(wrapper.find('.k-image__placeholder').exists()).toBe(false); + expect(wrapper.find('.k-image__error').exists()).toBe(true); + }); + + // 预览功能测试 + it('should add preview class when preview is true', () => { + const wrapper = mount(KImage, { + props: { + src: 'https://example.com/image.jpg', + preview: true + } + }); + expect(wrapper.classes()).toContain('k-image--preview'); + expect(wrapper.find('.k-image__preview').exists()).toBe(true); + }); + + // 预览点击事件测试 + it('should emit preview event when preview is clicked', () => { + const wrapper = mount(KImage, { + props: { + src: 'https://example.com/image.jpg', + preview: true + } + }); + + // 模拟图片加载成功 + wrapper.find('img').trigger('load'); + + // 点击预览按钮 + wrapper.find('.k-image__preview').trigger('click'); + + expect(wrapper.emitted('preview')).toBeTruthy(); + expect(wrapper.emitted('preview')?.[0]?.[0]).toEqual({ + src: 'https://example.com/image.jpg', + srcList: ['https://example.com/image.jpg'], + initialIndex: 0 + }); + }); + + // 预览图片列表测试 + it('should use previewSrcList when provided', () => { + const srcList = ['image1.jpg', 'image2.jpg', 'image3.jpg']; + const wrapper = mount(KImage, { + props: { + src: 'image1.jpg', + preview: true, + previewSrcList: srcList + } + }); + + // 模拟图片加载成功 + wrapper.find('img').trigger('load'); + + // 点击预览按钮 + wrapper.find('.k-image__preview').trigger('click'); + + expect(wrapper.emitted('preview')?.[0]?.[0].srcList).toEqual(srcList); + }); + + // 初始预览索引测试 + it('should use initialIndex when provided', () => { + const srcList = ['image1.jpg', 'image2.jpg', 'image3.jpg']; + const wrapper = mount(KImage, { + props: { + src: 'image1.jpg', + preview: true, + previewSrcList: srcList, + initialIndex: 1 + } + }); + + // 模拟图片加载成功 + wrapper.find('img').trigger('load'); + + // 点击预览按钮 + wrapper.find('.k-image__preview').trigger('click'); + + expect(wrapper.emitted('preview')?.[0]?.[0].initialIndex).toBe(1); + }); + + // 错误事件测试 + it('should emit error event when image fails to load', () => { + const wrapper = mount(KImage, { + props: { + src: 'https://example.com/image.jpg' + } + }); + + // 模拟图片加载失败 + wrapper.find('img').trigger('error'); + + expect(wrapper.emitted('error')).toBeTruthy(); + expect(wrapper.emitted('error')?.[0]?.[0]).toEqual({ + src: 'https://example.com/image.jpg', + message: 'Failed to load image: https://example.com/image.jpg' + }); + }); + + // 占位符插槽测试 + it('should render custom placeholder slot', () => { + const wrapper = mount(KImage, { + props: { + src: 'https://example.com/image.jpg' + }, + slots: { + placeholder: '
Custom Loading...
' + } + }); + expect(wrapper.find('.custom-placeholder').exists()).toBe(true); + }); + + // 错误插槽测试 + it('should render custom error slot', () => { + const wrapper = mount(KImage, { + props: { + src: 'https://example.com/image.jpg' + }, + slots: { + error: '
Custom Error!
' + } + }); + + // 模拟图片加载失败 + wrapper.find('img').trigger('error'); + + expect(wrapper.find('.custom-error').exists()).toBe(true); + }); + + // 组合属性测试 + it('should work correctly with multiple props combined', () => { + const wrapper = mount(KImage, { + props: { + src: 'https://example.com/image.jpg', + alt: 'Test Image', + width: 300, + height: 200, + fit: 'cover', + borderRadius: 12, + preview: true + } + }); + + expect(wrapper.find('img').attributes('src')).toBe('https://example.com/image.jpg'); + expect(wrapper.find('img').attributes('alt')).toBe('Test Image'); + expect(wrapper.attributes('style')).toContain('width: 300px'); + expect(wrapper.attributes('style')).toContain('height: 200px'); + expect(wrapper.find('img').attributes('style')).toContain('object-fit: cover'); + expect(wrapper.find('img').attributes('style')).toContain('border-radius: 12px'); + expect(wrapper.classes()).toContain('k-image--preview'); + }); +}); \ No newline at end of file diff --git a/src/components/Image/README.md b/src/components/Image/README.md new file mode 100644 index 0000000..ffafc1d --- /dev/null +++ b/src/components/Image/README.md @@ -0,0 +1,248 @@ +# Image 图片组件 + +用于展示图片,支持加载中状态、错误处理和图片预览功能。 + +## 特性 + +- 支持自定义尺寸和裁剪方式 +- 提供加载中状态和错误状态的默认展示 +- 支持图片预览功能 +- 可自定义占位图和错误提示 +- 响应式设计,适应不同屏幕尺寸 + +## 安装 + +```bash +# 安装组件库 +npm install @knowai/knowai-ui +# 或 +yarn add @knowai/knowai-ui +# 或 +pnpm add @knowai/knowai-ui +``` + +## 基础用法 + +### 基本图片展示 + +最简单的使用方式,只需要提供图片地址: + +```vue + + + +``` + +### 自定义尺寸 + +设置图片的宽度和高度: + +```vue + + + +``` + +### 不同的裁剪方式 + +使用`fit`属性控制图片如何适应容器: + +```vue + + + + + +``` + +### 图片预览 + +启用图片预览功能: + +```vue + + + +``` + +### 自定义占位图和错误提示 + +使用插槽自定义加载中和加载错误时的显示内容: + +```vue + + + + + +``` + +## API + +### 属性 + +| 属性名 | 类型 | 默认值 | 说明 | +|-------|------|-------|------| +| src | String | 必填 | 图片的 URL 地址 | +| alt | String | '' | 图片的替代文本 | +| fit | String | 'cover' | 图片的裁剪方式,可选值:'contain'、'cover'、'fill'、'none'、'scale-down' | +| width | String / Number | '' | 图片容器的宽度 | +| height | String / Number | '' | 图片容器的高度 | +| borderRadius | String / Number | '' | 图片的圆角大小 | +| lazy | Boolean | false | 是否开启懒加载 | +| preview | Boolean | false | 是否启用图片预览功能 | +| previewSrcList | Array | [] | 预览图片列表 | +| initialIndex | Number | 0 | 预览图片时的初始索引 | +| hideOnClickModal | Boolean | false | 点击遮罩层是否隐藏预览 | + +### 插槽 + +| 插槽名 | 说明 | +|-------|------| +| placeholder | 加载中状态的自定义内容 | +| error | 加载错误状态的自定义内容 | + +### 事件 + +| 事件名 | 说明 | 参数 | +|-------|------|------| +| load | 图片加载成功时触发 | 无 | +| error | 图片加载失败时触发 | 无 | +| preview | 图片预览时触发 | 当前预览的图片索引 | + +## 类型定义 + +```typescript +// ImageProps 接口定义 +export interface ImageProps { + src: string; + alt?: string; + fit?: 'fill' | 'contain' | 'cover' | 'none' | 'scale-down'; + width?: string | number; + height?: string | number; + borderRadius?: string | number; + lazy?: boolean; + placeholder?: string; + preview?: boolean; + previewSrcList?: string[]; + initialIndex?: number; + hideOnClickModal?: boolean; +} +``` + +## 注意事项 + +1. 当设置了 `preview` 属性为 `true` 时,建议同时提供 `previewSrcList` 以获得更好的预览体验 +2. 使用 `lazy` 属性时,需要确保图片在可视区域内才会触发加载 +3. `fit` 属性的默认值为 `cover`,这意味着图片会被裁剪以填满容器 +4. 当图片加载失败时,组件会显示默认的错误图标和提示文本,可以通过 `error` 插槽自定义 + +## 浏览器支持 + +- Chrome >= 60 +- Firefox >= 55 +- Safari >= 10.1 +- Edge >= 16 + +## 相关链接 + +- [MDN: img 元素](https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/img) +- [CSS object-fit 属性](https://developer.mozilla.org/zh-CN/docs/Web/CSS/object-fit) \ No newline at end of file diff --git a/src/components/Image/__snapshots__/index.test.ts.snap b/src/components/Image/__snapshots__/index.test.ts.snap new file mode 100644 index 0000000..58b010b --- /dev/null +++ b/src/components/Image/__snapshots__/index.test.ts.snap @@ -0,0 +1,23 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`KImage Component > should match snapshot with all props 1`] = ` +"
\\"Test +
+
+
+ +
+
+
+
" +`; + +exports[`KImage Component > should match snapshot with default props 1`] = ` +"
\\"\\" +
+
+
+ + +
" +`; diff --git a/src/components/Image/index.scss b/src/components/Image/index.scss new file mode 100644 index 0000000..a2dea36 --- /dev/null +++ b/src/components/Image/index.scss @@ -0,0 +1,74 @@ +// Image组件样式 + + +.k-image { + position: relative; + display: inline-block; + overflow: hidden; + + &__img { + display: block; + width: 100%; + height: 100%; + object-fit: cover; + transition: transform $transition-duration $transition-timing-function; + + &--loading { + opacity: 0; + } + } + + &__placeholder, + &__error { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + background-color: $color-bg-container; + color: $color-text-tertiary; + } + + &__placeholder-icon, + &__error-icon { + font-size: 24px; + margin-bottom: 8px; + } + + &__error-text { + font-size: 14px; + } + + &__preview { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center; + background-color: rgba(0, 0, 0, 0.5); + color: white; + opacity: 0; + transition: opacity $transition-duration $transition-timing-function; + cursor: pointer; + + &:hover { + opacity: 1; + } + } + + &__preview-icon { + font-size: 24px; + } + + // 预览模式 + &--preview { + cursor: pointer; + } +} \ No newline at end of file diff --git a/src/components/Image/index.test.ts b/src/components/Image/index.test.ts new file mode 100644 index 0000000..53a21ad --- /dev/null +++ b/src/components/Image/index.test.ts @@ -0,0 +1,273 @@ +import { mount } from '@vue/test-utils'; +import { describe, it, expect, vi } from 'vitest'; +import KImage from './index.vue'; + +// 模拟Icon组件 +vi.mock('../Icon/index.vue', () => ({ + default: { + name: 'KIcon', + props: ['name', 'class:iconClass'], + template: '' + } +})); + +describe('KImage Component', () => { + // 测试基础渲染 + it('should render correctly with basic props', () => { + const wrapper = mount(KImage, { + props: { + src: 'https://example.com/image.jpg', + alt: 'Test Image' + } + }); + + expect(wrapper.exists()).toBe(true); + expect(wrapper.classes()).toContain('k-image'); + expect(wrapper.find('img').exists()).toBe(true); + expect(wrapper.find('img').attributes('src')).toBe('https://example.com/image.jpg'); + expect(wrapper.find('img').attributes('alt')).toBe('Test Image'); + }); + + // 测试图片加载状态 + it('should show loading state initially', () => { + const wrapper = mount(KImage, { + props: { + src: 'https://example.com/image.jpg' + } + }); + + expect(wrapper.vm.loading).toBe(true); + expect(wrapper.find('.k-image__img--loading').exists()).toBe(true); + expect(wrapper.find('.k-image__placeholder').exists()).toBe(true); + }); + + // 测试图片加载成功 + it('should handle image load success', async () => { + const wrapper = mount(KImage, { + props: { + src: 'https://example.com/image.jpg' + } + }); + + // 触发图片加载成功事件 + await wrapper.find('img').trigger('load'); + + expect(wrapper.vm.loading).toBe(false); + expect(wrapper.vm.error).toBe(false); + expect(wrapper.find('.k-image__img--loading').exists()).toBe(false); + expect(wrapper.find('.k-image__placeholder').exists()).toBe(false); + }); + + // 测试图片加载失败 + it('should handle image load error', async () => { + const wrapper = mount(KImage, { + props: { + src: 'https://example.com/non-existent.jpg' + } + }); + + // 触发图片加载失败事件 + await wrapper.find('img').trigger('error'); + + expect(wrapper.vm.loading).toBe(false); + expect(wrapper.vm.error).toBe(true); + expect(wrapper.find('.k-image__placeholder').exists()).toBe(false); + expect(wrapper.find('.k-image__error').exists()).toBe(true); + }); + + // 测试预览功能 + it('should show preview button when preview prop is true', () => { + const wrapper = mount(KImage, { + props: { + src: 'https://example.com/image.jpg', + preview: true + } + }); + + expect(wrapper.classes()).toContain('k-image--preview'); + expect(wrapper.find('.k-image__preview').exists()).toBe(true); + }); + + // 测试预览点击事件 + it('should trigger preview event when preview button is clicked', async () => { + const previewSpy = vi.fn(); + + const wrapper = mount(KImage, { + props: { + src: 'https://example.com/image.jpg', + preview: true + }, + onPreview: previewSpy + }); + + await wrapper.find('.k-image__preview').trigger('click'); + + expect(previewSpy).toHaveBeenCalledWith({ + url: 'https://example.com/image.jpg', + index: 0, + srcList: ['https://example.com/image.jpg'] + }); + }); + + // 测试fit属性 + it('should apply correct object-fit style based on fit prop', () => { + const wrapper = mount(KImage, { + props: { + src: 'https://example.com/image.jpg', + fit: 'contain' + } + }); + + expect(wrapper.find('img').attributes('style')).toContain('object-fit: contain'); + }); + + // 测试默认fit值为cover + it('should use cover as default fit value', () => { + const wrapper = mount(KImage, { + props: { + src: 'https://example.com/image.jpg' + } + }); + + // 由于组件内部设置了默认值为cover,所以应该看到这个值 + expect(wrapper.vm.imgStyle.objectFit).toBe('cover'); + }); + + // 测试width和height属性 + it('should apply correct width and height styles', () => { + const wrapper = mount(KImage, { + props: { + src: 'https://example.com/image.jpg', + width: 200, + height: 150 + } + }); + + const imageStyle = wrapper.vm.imageStyle; + expect(imageStyle.width).toBe('200px'); + expect(imageStyle.height).toBe('150px'); + }); + + // 测试width和height为字符串的情况 + it('should handle string width and height correctly', () => { + const wrapper = mount(KImage, { + props: { + src: 'https://example.com/image.jpg', + width: '300px', + height: '200px' + } + }); + + const imageStyle = wrapper.vm.imageStyle; + expect(imageStyle.width).toBe('300px'); + expect(imageStyle.height).toBe('200px'); + }); + + // 测试borderRadius属性 + it('should apply correct border-radius style', () => { + const wrapper = mount(KImage, { + props: { + src: 'https://example.com/image.jpg', + borderRadius: 8 + } + }); + + const imgStyle = wrapper.vm.imgStyle; + expect(imgStyle.borderRadius).toBe('8px'); + }); + + // 测试borderRadius为字符串的情况 + it('should handle string border-radius correctly', () => { + const wrapper = mount(KImage, { + props: { + src: 'https://example.com/image.jpg', + borderRadius: '12px' + } + }); + + const imgStyle = wrapper.vm.imgStyle; + expect(imgStyle.borderRadius).toBe('12px'); + }); + + // 测试placeholder插槽 + it('should render custom placeholder content', () => { + const wrapper = mount(KImage, { + props: { + src: 'https://example.com/image.jpg' + }, + slots: { + placeholder: '
自定义加载中...
' + } + }); + + expect(wrapper.find('.custom-placeholder').exists()).toBe(true); + expect(wrapper.find('.custom-placeholder').text()).toBe('自定义加载中...'); + }); + + // 测试error插槽 + it('should render custom error content', async () => { + const wrapper = mount(KImage, { + props: { + src: 'https://example.com/image.jpg' + }, + slots: { + error: '
自定义错误信息
' + } + }); + + // 触发错误状态 + await wrapper.find('img').trigger('error'); + + expect(wrapper.find('.custom-error').exists()).toBe(true); + expect(wrapper.find('.custom-error').text()).toBe('自定义错误信息'); + }); + + // 测试组合属性 + it('should handle all props together correctly', () => { + const wrapper = mount(KImage, { + props: { + src: 'https://example.com/image.jpg', + alt: 'Test Image', + fit: 'contain', + width: 400, + height: 300, + borderRadius: 16, + preview: true + } + }); + + expect(wrapper.classes()).toContain('k-image--preview'); + expect(wrapper.find('img').attributes('alt')).toBe('Test Image'); + expect(wrapper.vm.imageStyle.width).toBe('400px'); + expect(wrapper.vm.imageStyle.height).toBe('300px'); + expect(wrapper.vm.imgStyle.objectFit).toBe('contain'); + expect(wrapper.vm.imgStyle.borderRadius).toBe('16px'); + }); + + // 快照测试 + it('should match snapshot with default props', () => { + const wrapper = mount(KImage, { + props: { + src: 'https://example.com/image.jpg' + } + }); + + expect(wrapper.html()).toMatchSnapshot(); + }); + + it('should match snapshot with all props', () => { + const wrapper = mount(KImage, { + props: { + src: 'https://example.com/image.jpg', + alt: 'Test Image', + fit: 'cover', + width: 200, + height: 150, + borderRadius: 8, + preview: true + } + }); + + expect(wrapper.html()).toMatchSnapshot(); + }); +}); \ No newline at end of file diff --git a/src/components/Image/index.ts b/src/components/Image/index.ts new file mode 100644 index 0000000..6aaf632 --- /dev/null +++ b/src/components/Image/index.ts @@ -0,0 +1,5 @@ +import Image from './index.vue'; +export * from './types'; + +export { Image }; +export default Image; diff --git a/src/components/Image/index.vue b/src/components/Image/index.vue new file mode 100644 index 0000000..579c426 --- /dev/null +++ b/src/components/Image/index.vue @@ -0,0 +1,201 @@ + + + diff --git a/src/components/Image/types.ts b/src/components/Image/types.ts new file mode 100644 index 0000000..b649558 --- /dev/null +++ b/src/components/Image/types.ts @@ -0,0 +1,19 @@ +export interface ImageProps { + src: string; + alt?: string; + fit?: 'fill' | 'contain' | 'cover' | 'none' | 'scale-down'; + width?: string | number; + height?: string | number; + borderRadius?: string | number; + lazy?: boolean; + placeholder?: string; + preview?: boolean; + previewSrcList?: string[]; + initialIndex?: number; + hideOnClickModal?: boolean; +} + +export interface ImageEmits { + (event: 'preview', data: { src: string; srcList: string[]; initialIndex: number }): void; + (event: 'error', data: { src: string; message: string }): void; +} diff --git a/src/components/Loading/Loading.test.ts b/src/components/Loading/Loading.test.ts new file mode 100644 index 0000000..0083822 --- /dev/null +++ b/src/components/Loading/Loading.test.ts @@ -0,0 +1,213 @@ +import { describe, it, expect } from 'vitest'; +import { mount } from '@vue/test-utils'; +import KLoading from './index.vue'; + +describe('Loading组件测试', () => { + // 基础渲染测试 + it('组件能够正常渲染', () => { + const wrapper = mount(KLoading); + expect(wrapper.exists()).toBe(true); + expect(wrapper.classes('k-loading')).toBe(true); + }); + + // loading状态测试 + it('默认显示loading', () => { + const wrapper = mount(KLoading); + expect(wrapper.find('.k-loading').isVisible()).toBe(true); + }); + + it('可以通过loading属性控制显示/隐藏', async () => { + const wrapper = mount(KLoading); + + // 隐藏loading + await wrapper.setProps({ loading: false }); + // 当loading为false时,元素会被完全移除,所以检查是否存在 + expect(wrapper.find('.k-loading').exists()).toBe(false); + + // 显示loading + await wrapper.setProps({ loading: true }); + expect(wrapper.find('.k-loading').exists()).toBe(true); + expect(wrapper.find('.k-loading').isVisible()).toBe(true); + }); + + // 尺寸测试 + it('默认尺寸为medium', () => { + const wrapper = mount(KLoading); + expect(wrapper.classes('k-loading--size-medium')).toBe(true); + }); + + it('可以设置尺寸为small', () => { + const wrapper = mount(KLoading, { + props: { + size: 'small' + } + }); + expect(wrapper.classes('k-loading--size-small')).toBe(true); + }); + + it('可以设置尺寸为large', () => { + const wrapper = mount(KLoading, { + props: { + size: 'large' + } + }); + expect(wrapper.classes('k-loading--size-large')).toBe(true); + }); + + // 文本测试 + it('默认不显示文本', () => { + const wrapper = mount(KLoading); + const textElement = wrapper.find('.k-loading__text'); + expect(textElement.exists()).toBe(false); + }); + + it('可以设置文本内容', () => { + const text = '加载中...'; + const wrapper = mount(KLoading, { + props: { + text + } + }); + const textElement = wrapper.find('.k-loading__text'); + expect(textElement.exists()).toBe(true); + expect(textElement.text()).toBe(text); + }); + + // 全屏模式测试 + it('默认不是全屏模式', () => { + const wrapper = mount(KLoading); + expect(wrapper.classes('k-loading--fullscreen')).toBe(false); + }); + + it('可以设置为全屏模式', () => { + const wrapper = mount(KLoading, { + props: { + fullscreen: true + } + }); + expect(wrapper.classes('k-loading--fullscreen')).toBe(true); + }); + + // 背景色测试 + it('可以设置背景色', () => { + const background = 'rgba(255, 255, 255, 0.8)'; + const wrapper = mount(KLoading, { + props: { + background + } + }); + expect(wrapper.attributes('style')).toContain(`background-color: ${background}`); + }); + + // 自定义spinner图标测试 + it('默认使用loading图标', () => { + const wrapper = mount(KLoading); + const iconComponent = wrapper.findComponent({ name: 'KIcon' }); + expect(iconComponent.props('name')).toBe('loading'); + }); + + it('可以自定义spinner图标', () => { + const customIcon = 'custom-spinner'; + const wrapper = mount(KLoading, { + props: { + spinner: customIcon + } + }); + const iconComponent = wrapper.findComponent({ name: 'KIcon' }); + expect(iconComponent.props('name')).toBe(customIcon); + }); + + // 垂直布局测试 + it('默认不是垂直布局', () => { + const wrapper = mount(KLoading, { + props: { + text: '加载中' + } + }); + expect(wrapper.classes('k-loading--vertical')).toBe(false); + }); + + it('可以设置为垂直布局', () => { + const wrapper = mount(KLoading, { + props: { + text: '加载中', + vertical: true + } + }); + expect(wrapper.classes('k-loading--vertical')).toBe(true); + }); + + // 默认插槽测试 + it('当loading为false时显示默认插槽内容', async () => { + const slotContent = '
加载完成
'; + const wrapper = mount(KLoading, { + props: { + loading: false + }, + slots: { + default: slotContent + } + }); + const slotElement = wrapper.find('.slot-content'); + expect(slotElement.exists()).toBe(true); + }); + + it('当loading为true时不显示默认插槽内容', () => { + const slotContent = '
加载完成
'; + const wrapper = mount(KLoading, { + props: { + loading: true + }, + slots: { + default: slotContent + } + }); + const slotElement = wrapper.find('.slot-content'); + expect(slotElement.exists()).toBe(false); + }); + + // spinner插槽测试 + it('可以使用spinner插槽自定义加载图标', () => { + const spinnerContent = '
自定义加载动画
'; + const wrapper = mount(KLoading, { + slots: { + spinner: spinnerContent + } + }); + const customSpinner = wrapper.find('.custom-spinner'); + expect(customSpinner.exists()).toBe(true); + }); + + // text插槽测试 + it('可以使用text插槽自定义文本', () => { + const textContent = '自定义加载文本'; + const wrapper = mount(KLoading, { + slots: { + text: textContent + } + }); + const customText = wrapper.find('.custom-text'); + expect(customText.exists()).toBe(true); + }); + + // 组合属性测试 + it('可以同时设置多个属性', () => { + const text = '正在加载数据...'; + const background = 'rgba(0, 0, 0, 0.5)'; + const wrapper = mount(KLoading, { + props: { + size: 'large', + text, + fullscreen: true, + background, + vertical: true + } + }); + + expect(wrapper.classes('k-loading--size-large')).toBe(true); + expect(wrapper.classes('k-loading--fullscreen')).toBe(true); + expect(wrapper.classes('k-loading--vertical')).toBe(true); + expect(wrapper.find('.k-loading__text').text()).toBe(text); + expect(wrapper.attributes('style')).toContain(`background-color: ${background}`); + }); +}); \ No newline at end of file diff --git a/src/components/Loading/README.md b/src/components/Loading/README.md new file mode 100644 index 0000000..e579b25 --- /dev/null +++ b/src/components/Loading/README.md @@ -0,0 +1,75 @@ +# KLoading + +`KLoading` 组件用于展示加载状态,支持多种尺寸、全屏模式和自定义内容。 + +## 使用示例 + +```vue + + + +``` + +## Props + +| 属性名 | 类型 | 默认值 | 描述 | +|--------|------|--------|------| +| `loading` | `boolean` | `true` | 是否显示加载状态 | +| `size` | 'small' \| 'medium' \| 'large' | 'medium' | 加载组件尺寸 | +| `text` | `string` | `''` | 加载文字提示 | +| `delay` | `number` | `0` | 延迟显示时间(毫秒) | +| `fullscreen` | `boolean` | `false` | 是否全屏显示 | +| `background` | `string` | `''` | 背景颜色 | +| `spinner` | `string` | 'loading' | 加载图标名称 | +| `color` | `string` | `''` | 图标颜色 | +| `vertical` | `boolean` | `false` | 是否垂直排列图标和文字 | +| `lock` | `boolean` | - | 是否锁定背景滚动 | +| `customClass` | `string` | - | 自定义类名 | + +## Slots + +| 插槽名 | 描述 | +|--------|------| +| `default` | 未加载状态下显示的内容 | +| `spinner` | 自定义加载图标 | +| `text` | 自定义加载文字内容 | + +## Events + +KLoading组件没有提供事件。 \ No newline at end of file diff --git a/src/components/Loading/__snapshots__/index.test.ts.snap b/src/components/Loading/__snapshots__/index.test.ts.snap new file mode 100644 index 0000000..c758df2 --- /dev/null +++ b/src/components/Loading/__snapshots__/index.test.ts.snap @@ -0,0 +1,15 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`KLoading Component > should match snapshot with all props 1`] = ` +"
+
+
Loading...
+
" +`; + +exports[`KLoading Component > should match snapshot with default props 1`] = ` +"
+
+ +
" +`; diff --git a/src/components/Loading/index.scss b/src/components/Loading/index.scss new file mode 100644 index 0000000..2adae40 --- /dev/null +++ b/src/components/Loading/index.scss @@ -0,0 +1,76 @@ +// Loading组件样式 + + +.k-loading { + display: flex; + align-items: center; + justify-content: center; + color: $color-primary; + + // 尺寸样式 + &--size-small { + .k-loading__icon { + font-size: 16px; + } + } + + &--size-medium { + .k-loading__icon { + font-size: 24px; + } + } + + &--size-large { + .k-loading__icon { + font-size: 32px; + } + } + + // 全屏模式 + &--fullscreen { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(255, 255, 255, 0.7); + z-index: 9999; + } + + // 垂直布局 + &--vertical { + flex-direction: column; + } + + &__spinner { + display: flex; + align-items: center; + justify-content: center; + } + + &__icon { + animation: spin 1s linear infinite; + } + + &__text { + margin-left: 8px; + color: $color-text-secondary; + } + + // 垂直布局下的文本样式 + &--vertical { + .k-loading__text { + margin-left: 0; + margin-top: 8px; + } + } +} + +@keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} \ No newline at end of file diff --git a/src/components/Loading/index.test.ts b/src/components/Loading/index.test.ts new file mode 100644 index 0000000..5dcd115 --- /dev/null +++ b/src/components/Loading/index.test.ts @@ -0,0 +1,245 @@ +import { mount } from '@vue/test-utils'; +import { describe, it, expect, vi } from 'vitest'; +import KLoading from './index.vue'; +import type { LoadingProps } from './types'; + +// 模拟Icon组件 +vi.mock('../Icon/index.vue', () => ({ + default: { + name: 'KIcon', + props: ['name', 'class:iconClass'], + template: '' + } +})); + +describe('KLoading Component', () => { + // 测试基础渲染 + it('should render correctly with default props', () => { + const wrapper = mount(KLoading); + + expect(wrapper.exists()).toBe(true); + expect(wrapper.classes()).toContain('k-loading'); + expect(wrapper.classes()).toContain('k-loading--size-medium'); + expect(wrapper.find('.k-loading__spinner').exists()).toBe(true); + expect(wrapper.find('.k-loading__icon').exists()).toBe(true); + expect(wrapper.findComponent({ name: 'KIcon' }).props('name')).toBe('loading'); + }); + + // 测试loading状态控制 + it('should show loading content when loading is true', () => { + const wrapper = mount(KLoading, { + props: { + loading: true + }, + slots: { + default: '
Default content
' + } + }); + + expect(wrapper.find('.k-loading__spinner').exists()).toBe(true); + expect(wrapper.find('.default-content').exists()).toBe(false); + }); + + it('should show default slot when loading is false', () => { + const wrapper = mount(KLoading, { + props: { + loading: false + }, + slots: { + default: '
Default content
' + } + }); + + expect(wrapper.find('.k-loading__spinner').exists()).toBe(false); + expect(wrapper.find('.default-content').exists()).toBe(true); + expect(wrapper.find('.default-content').text()).toBe('Default content'); + }); + + // 测试尺寸属性 + it('should apply correct size class', () => { + const sizeCases = [ + { size: 'small', expectedClass: 'k-loading--size-small' }, + { size: 'medium', expectedClass: 'k-loading--size-medium' }, + { size: 'large', expectedClass: 'k-loading--size-large' } + ]; + + sizeCases.forEach(({ size, expectedClass }) => { + const wrapper = mount(KLoading, { + props: { size } as LoadingProps + }); + + expect(wrapper.classes()).toContain(expectedClass); + expect(wrapper.find('.k-loading__icon').classes()).toContain(`k-loading__icon--${size}`); + }); + }); + + // 测试文字提示 + it('should display loading text when provided', () => { + const wrapper = mount(KLoading, { + props: { + text: 'Loading...' + } + }); + + expect(wrapper.find('.k-loading__text').exists()).toBe(true); + expect(wrapper.find('.k-loading__text').text()).toBe('Loading...'); + }); + + it('should not display text element when text is empty', () => { + const wrapper = mount(KLoading, { + props: { + text: '' + } + }); + + expect(wrapper.find('.k-loading__text').exists()).toBe(false); + }); + + // 测试垂直排列 + it('should apply vertical class when vertical is true', () => { + const wrapper = mount(KLoading, { + props: { + vertical: true, + text: 'Loading...' + } + }); + + expect(wrapper.classes()).toContain('k-loading--vertical'); + }); + + // 测试全屏模式 + it('should apply fullscreen class when fullscreen is true', () => { + const wrapper = mount(KLoading, { + props: { + fullscreen: true + } + }); + + expect(wrapper.classes()).toContain('k-loading--fullscreen'); + }); + + // 测试背景色 + it('should apply background color when provided', () => { + const wrapper = mount(KLoading, { + props: { + background: 'rgba(255, 255, 255, 0.8)' + } + }); + + expect(wrapper.attributes('style')).toContain('background-color: rgba(255, 255, 255, 0.8)'); + }); + + // 测试自定义图标 + it('should use custom spinner icon when provided', () => { + const wrapper = mount(KLoading, { + props: { + spinner: 'loading-ring' + } + }); + + expect(wrapper.findComponent({ name: 'KIcon' }).props('name')).toBe('loading-ring'); + }); + + // 测试spinner插槽 + it('should render custom spinner from slot', () => { + const wrapper = mount(KLoading, { + slots: { + spinner: '
Custom Spinner
' + } + }); + + expect(wrapper.find('.custom-spinner').exists()).toBe(true); + expect(wrapper.find('.custom-spinner').text()).toBe('Custom Spinner'); + expect(wrapper.findComponent({ name: 'KIcon' }).exists()).toBe(false); + }); + + // 测试text插槽 + it('should render custom text from slot', () => { + const wrapper = mount(KLoading, { + slots: { + text: 'Custom Text' + } + }); + + expect(wrapper.find('.k-loading__text').exists()).toBe(true); + expect(wrapper.find('.custom-text').exists()).toBe(true); + expect(wrapper.find('.custom-text').text()).toBe('Custom Text'); + }); + + // 测试组合属性 + it('should handle all props together correctly', () => { + const wrapper = mount(KLoading, { + props: { + size: 'large', + text: 'Processing...', + vertical: true, + background: '#f0f0f0', + spinner: 'refresh' + } + }); + + expect(wrapper.classes()).toContain('k-loading--size-large'); + expect(wrapper.classes()).toContain('k-loading--vertical'); + expect(wrapper.find('.k-loading__text').text()).toBe('Processing...'); + expect(wrapper.attributes('style')).toContain('background-color: #f0f0f0'); + expect(wrapper.findComponent({ name: 'KIcon' }).props('name')).toBe('refresh'); + }); + + // 快照测试 + it('should match snapshot with default props', () => { + const wrapper = mount(KLoading); + + expect(wrapper.html()).toMatchSnapshot(); + }); + + it('should match snapshot with all props', () => { + const wrapper = mount(KLoading, { + props: { + size: 'small', + text: 'Loading...', + vertical: true, + background: '#fff', + spinner: 'loading-dots' + } + }); + + expect(wrapper.html()).toMatchSnapshot(); + }); + + // 测试类型定义中存在但组件未直接使用的属性 + it('should accept lock and customClass props (from type definition)', () => { + const wrapper = mount(KLoading, { + props: { + lock: true, + customClass: 'test-custom-class' + } + }); + + // 验证组件不会因为这些属性而报错 + expect(wrapper.exists()).toBe(true); + }); + + // 测试delay属性(虽然组件中未直接实现延迟逻辑) + it('should accept delay prop', () => { + const wrapper = mount(KLoading, { + props: { + delay: 300 + } + }); + + // 验证组件不会因为delay属性而报错 + expect(wrapper.exists()).toBe(true); + }); + + // 测试color属性(虽然组件中未直接实现颜色逻辑) + it('should accept color prop', () => { + const wrapper = mount(KLoading, { + props: { + color: '#ff0000' + } + }); + + // 验证组件不会因为color属性而报错 + expect(wrapper.exists()).toBe(true); + }); +}); \ No newline at end of file diff --git a/src/components/Loading/index.ts b/src/components/Loading/index.ts new file mode 100644 index 0000000..e9f88aa --- /dev/null +++ b/src/components/Loading/index.ts @@ -0,0 +1,5 @@ +import Loading from './index.vue'; +export * from './types'; + +export { Loading }; +export default Loading; diff --git a/src/components/Loading/index.vue b/src/components/Loading/index.vue new file mode 100644 index 0000000..5e23ec3 --- /dev/null +++ b/src/components/Loading/index.vue @@ -0,0 +1,109 @@ + + + diff --git a/src/components/Loading/types.ts b/src/components/Loading/types.ts new file mode 100644 index 0000000..5e227fd --- /dev/null +++ b/src/components/Loading/types.ts @@ -0,0 +1,13 @@ +export interface LoadingProps { + loading?: boolean; + size?: 'small' | 'medium' | 'large'; + text?: string; + delay?: number; + fullscreen?: boolean; + background?: string; + spinner?: string; + color?: string; + vertical?: boolean; + lock?: boolean; + customClass?: string; +} diff --git a/src/components/Message/Message.test.ts b/src/components/Message/Message.test.ts new file mode 100644 index 0000000..c375cb2 --- /dev/null +++ b/src/components/Message/Message.test.ts @@ -0,0 +1,273 @@ +import { describe, it, expect, mount, vi } from 'vitest'; +import KMessage from './index.vue'; + +describe('KMessage Component', () => { + // 基础渲染测试 + it('should render correctly', () => { + const wrapper = mount(KMessage, { + props: { + content: 'Test Message' + } + }); + expect(wrapper.exists()).toBe(true); + expect(wrapper.classes()).toContain('k-message'); + }); + + // 类型属性测试 - success + it('should render success type message', () => { + const wrapper = mount(KMessage, { + props: { + content: 'Success Message', + type: 'success' + } + }); + expect(wrapper.classes()).toContain('k-message--type-success'); + expect(wrapper.find('.k-message__icon').exists()).toBe(true); + }); + + // 类型属性测试 - info + it('should render info type message', () => { + const wrapper = mount(KMessage, { + props: { + content: 'Info Message', + type: 'info' + } + }); + expect(wrapper.classes()).toContain('k-message--type-info'); + }); + + // 类型属性测试 - warning + it('should render warning type message', () => { + const wrapper = mount(KMessage, { + props: { + content: 'Warning Message', + type: 'warning' + } + }); + expect(wrapper.classes()).toContain('k-message--type-warning'); + }); + + // 类型属性测试 - error + it('should render error type message', () => { + const wrapper = mount(KMessage, { + props: { + content: 'Error Message', + type: 'error' + } + }); + expect(wrapper.classes()).toContain('k-message--type-error'); + }); + + // 类型属性测试 - loading + it('should render loading type message', () => { + const wrapper = mount(KMessage, { + props: { + content: 'Loading Message', + type: 'loading' + } + }); + expect(wrapper.classes()).toContain('k-message--type-loading'); + }); + + // 内容属性测试 + it('should display correct content', () => { + const wrapper = mount(KMessage, { + props: { + content: 'Test Content' + } + }); + expect(wrapper.find('.k-message__content').text()).toBe('Test Content'); + }); + + // 插槽内容测试 + it('should render slot content correctly', () => { + const wrapper = mount(KMessage, { + slots: { + default: 'Slot Content' + } + }); + expect(wrapper.find('.k-message__content').html()).toContain('Slot Content'); + }); + + // 隐藏图标测试 + it('should not display icon when icon is false', () => { + const wrapper = mount(KMessage, { + props: { + content: 'No Icon Message', + icon: false + } + }); + expect(wrapper.find('.k-message__icon').exists()).toBe(false); + }); + + // 可关闭属性测试 + it('should display close button when closable is true', () => { + const wrapper = mount(KMessage, { + props: { + content: 'Closable Message', + closable: true + } + }); + expect(wrapper.find('.k-message__close').exists()).toBe(true); + }); + + // 关闭按钮点击测试 + it('should call onClose when close button is clicked', () => { + const onClose = vi.fn(); + const wrapper = mount(KMessage, { + props: { + content: 'Closable Message', + closable: true, + onClose: onClose + } + }); + + wrapper.find('.k-message__close').trigger('click'); + expect(onClose).toHaveBeenCalled(); + }); + + // 位置属性测试 + it('should set correct top position', () => { + const wrapper = mount(KMessage, { + props: { + content: 'Positioned Message', + top: 50 + } + }); + expect(wrapper.attributes('style')).toContain('top: 50px'); + }); + + // 点击事件测试 + it('should call onClick when message is clicked', () => { + const onClick = vi.fn(); + const wrapper = mount(KMessage, { + props: { + content: 'Clickable Message', + onClick: onClick + } + }); + + wrapper.trigger('click'); + expect(onClick).toHaveBeenCalled(); + }); + + // 自定义类名测试 + it('should add custom className', () => { + const wrapper = mount(KMessage, { + props: { + content: 'Custom Class Message', + className: 'custom-message' + } + }); + expect(wrapper.classes()).toContain('custom-message'); + }); + + // 自定义样式测试 + it('should add custom style', () => { + const wrapper = mount(KMessage, { + props: { + content: 'Custom Style Message', + style: { zIndex: 2000 } + } + }); + expect(wrapper.attributes('style')).toContain('z-index: 2000'); + }); + + // 鼠标悬停暂停自动关闭测试 + it('should pause auto close on mouse enter when pauseOnHover is true', () => { + const onClose = vi.fn(); + const wrapper = mount(KMessage, { + props: { + content: 'Pause On Hover Message', + duration: 1, + pauseOnHover: true, + onClose: onClose + } + }); + + // 模拟鼠标悬停 + wrapper.trigger('mouseenter'); + + // 等待足够长时间 + return new Promise(resolve => { + setTimeout(() => { + // 验证onClose没有被调用 + expect(onClose).not.toHaveBeenCalled(); + resolve(null); + }, 1500); + }); + }); + + // 鼠标离开恢复自动关闭测试 + it('should resume auto close on mouse leave when pauseOnHover is true', () => { + const onClose = vi.fn(); + const wrapper = mount(KMessage, { + props: { + content: 'Resume On Mouse Leave Message', + duration: 1, + pauseOnHover: true, + onClose: onClose + } + }); + + // 模拟鼠标悬停 + wrapper.trigger('mouseenter'); + + // 等待一段时间 + return new Promise(resolve => { + setTimeout(() => { + // 模拟鼠标离开 + wrapper.trigger('mouseleave'); + + // 再次等待足够长时间 + setTimeout(() => { + // 验证onClose被调用 + expect(onClose).toHaveBeenCalled(); + resolve(null); + }, 1500); + }, 500); + }); + }); + + // 持续时间为0的测试 + it('should not auto close when duration is 0', () => { + const onClose = vi.fn(); + const wrapper = mount(KMessage, { + props: { + content: 'No Auto Close Message', + duration: 0, + onClose: onClose + } + }); + + // 等待足够长时间 + return new Promise(resolve => { + setTimeout(() => { + // 验证onClose没有被调用 + expect(onClose).not.toHaveBeenCalled(); + resolve(null); + }, 1500); + }); + }); + + // 组合属性测试 + it('should work correctly with multiple props combined', () => { + const wrapper = mount(KMessage, { + props: { + content: 'Combined Props Message', + type: 'success', + closable: true, + top: 100, + duration: 2, + className: 'custom-message', + style: { width: '300px' } + } + }); + + expect(wrapper.classes()).toContain('k-message--type-success'); + expect(wrapper.classes()).toContain('custom-message'); + expect(wrapper.find('.k-message__close').exists()).toBe(true); + expect(wrapper.attributes('style')).toContain('top: 100px'); + expect(wrapper.attributes('style')).toContain('width: 300px'); + }); +}); \ No newline at end of file diff --git a/src/components/Message/README.md b/src/components/Message/README.md new file mode 100644 index 0000000..5e727c0 --- /dev/null +++ b/src/components/Message/README.md @@ -0,0 +1,132 @@ +# Message 消息提示 + +轻量级的提示组件,用于向用户反馈操作结果或展示临时信息。 + +## 基础用法 + +直接在模板中使用组件。 + +```vue + + + +``` + +## 不同类型的消息 + +Message 组件支持多种类型的消息,可以通过 `type` 属性来设置。 + +```vue + + + +``` + +## 自定义配置 + +可以通过各种属性来自定义消息的行为。 + +```vue + + + +``` + +## Props + +| 参数 | 说明 | 类型 | 可选值 | 默认值 | +| --- | --- | --- | --- | --- | +| content | 消息内容 | `string` | - | '' | +| type | 消息类型 | `string` | success / info / warning / error / loading | info | +| icon | 是否显示图标 | `boolean` | - | true | +| duration | 显示时长(秒),0 表示永久显示 | `number` | - | 3 | +| closable | 是否显示关闭按钮 | `boolean` | - | false | +| onClose | 关闭时的回调函数 | `Function` | - | null | +| onClick | 点击消息时的回调函数 | `Function` | - | null | +| className | 自定义类名 | `string` | - | '' | +| style | 自定义样式 | `Object` | - | {} | +| top | 消息距离顶部的距离(px) | `number` | - | 24 | +| pauseOnHover | 鼠标悬停时是否暂停计时 | `boolean` | - | true | + diff --git a/src/components/Message/index.scss b/src/components/Message/index.scss new file mode 100644 index 0000000..d93cd9a --- /dev/null +++ b/src/components/Message/index.scss @@ -0,0 +1,144 @@ +// Message组件样式 + + +.k-message { + position: fixed; + top: 24px; + left: 50%; + transform: translateX(-50%); + z-index: 1010; + display: flex; + align-items: center; + padding: 10px 16px; + background-color: #fff; + border-radius: $border-radius-md; + box-shadow: 0 3px 6px -4px rgba(0, 0, 0, 0.12), 0 6px 16px 0 rgba(0, 0, 0, 0.08), 0 9px 28px 8px rgba(0, 0, 0, 0.05); + transition: all 0.3s; + pointer-events: auto; + max-width: calc(100vw - 32px); + + &__icon { + margin-right: 8px; + font-size: $font-size-md; + flex-shrink: 0; + } + + &__content { + color: $color-text-primary; + font-size: $font-size-md; + line-height: 1.5715; + word-wrap: break-word; + } + + &__close { + margin-left: 8px; + color: $color-text-secondary; + background: transparent; + border: none; + outline: none; + cursor: pointer; + font-size: $font-size-md; + padding: 0; + line-height: 1; + transition: color 0.2s; + + &:hover { + color: $color-text-primary; + } + } + + // 类型样式 + &--type-success { + background-color: #f6ffed; + border: 1px solid #b7eb8f; + + .k-message__icon { + color: $color-success; + } + + .k-message__content { + color: #52c41a; + } + } + + &--type-info { + background-color: #e6f7ff; + border: 1px solid #91d5ff; + + .k-message__icon { + color: $color-info; + } + + .k-message__content { + color: #1890ff; + } + } + + &--type-warning { + background-color: #fffbe6; + border: 1px solid #ffe58f; + + .k-message__icon { + color: $color-warning; + } + + .k-message__content { + color: #faad14; + } + } + + &--type-error { + background-color: #fff2f0; + border: 1px solid #ffccc7; + + .k-message__icon { + color: $color-danger; + } + + .k-message__content { + color: #ff4d4f; + } + } + + &--type-loading { + background-color: #e6f7ff; + border: 1px solid #91d5ff; + + .k-message__icon { + color: $color-info; + animation: spin 1s linear infinite; + } + + .k-message__content { + color: #1890ff; + } + } +} + +// 动画效果 +@keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} + +.k-message-fade-enter-active { + transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275); +} + +.k-message-fade-leave-active { + transition: all 0.3s cubic-bezier(0.55, 0, 0.55, 0.2); +} + +.k-message-fade-enter-from { + opacity: 0; + transform: translate(-50%, -100%); +} + +.k-message-fade-leave-to { + opacity: 0; + transform: translate(-50%, -100%); +} \ No newline at end of file diff --git a/src/components/Message/index.ts b/src/components/Message/index.ts new file mode 100644 index 0000000..dba678f --- /dev/null +++ b/src/components/Message/index.ts @@ -0,0 +1,5 @@ +import Message from './index.vue'; +export * from './types'; + +export { Message }; +export default Message; diff --git a/src/components/Message/index.vue b/src/components/Message/index.vue new file mode 100644 index 0000000..6657624 --- /dev/null +++ b/src/components/Message/index.vue @@ -0,0 +1,190 @@ + + + diff --git a/src/components/Message/types.ts b/src/components/Message/types.ts new file mode 100644 index 0000000..66f6ca9 --- /dev/null +++ b/src/components/Message/types.ts @@ -0,0 +1,20 @@ +export interface MessageProps { + content?: string; + type?: 'success' | 'info' | 'warning' | 'error' | 'loading'; + icon?: boolean; + duration?: number; + onClose?: () => void; + onClick?: () => void; + closable?: boolean; + className?: string; + style?: Record; + top?: number; + pauseOnHover?: boolean; + message?: string; + customClass?: string; + center?: boolean; + dangerouslyUseHTMLString?: boolean; + offset?: number; + showClose?: boolean; +} + diff --git a/src/components/Notification/Notification.test.ts b/src/components/Notification/Notification.test.ts new file mode 100644 index 0000000..3b6646f --- /dev/null +++ b/src/components/Notification/Notification.test.ts @@ -0,0 +1,335 @@ +import { describe, it, expect, vi } from 'vitest'; +import { mount } from '@vue/test-utils'; +import KNotification from './index.vue'; + +describe('Notification组件测试', () => { + // 基础渲染测试 + it('组件能够正常渲染', () => { + const wrapper = mount(KNotification); + expect(wrapper.exists()).toBe(true); + expect(wrapper.classes('k-notification')).toBe(true); + }); + + // 类型测试 + it('默认类型为info', () => { + const wrapper = mount(KNotification); + expect(wrapper.classes('k-notification--type-info')).toBe(true); + }); + + it('可以设置为success类型', () => { + const wrapper = mount(KNotification, { + props: { + type: 'success' + } + }); + expect(wrapper.classes('k-notification--type-success')).toBe(true); + }); + + it('可以设置为warning类型', () => { + const wrapper = mount(KNotification, { + props: { + type: 'warning' + } + }); + expect(wrapper.classes('k-notification--type-warning')).toBe(true); + }); + + it('可以设置为error类型', () => { + const wrapper = mount(KNotification, { + props: { + type: 'error' + } + }); + expect(wrapper.classes('k-notification--type-error')).toBe(true); + }); + + // 内容测试 + it('可以显示标题', () => { + const title = '通知标题'; + const wrapper = mount(KNotification, { + props: { + title + } + }); + const titleElement = wrapper.find('.k-notification__title'); + expect(titleElement.exists()).toBe(true); + expect(titleElement.text()).toBe(title); + }); + + it('可以显示描述内容', () => { + const description = '这是一条通知消息'; + const wrapper = mount(KNotification, { + props: { + description + } + }); + const descriptionElement = wrapper.find('.k-notification__description'); + expect(descriptionElement.exists()).toBe(true); + expect(descriptionElement.text()).toBe(description); + }); + + it('可以同时显示标题和描述', () => { + const title = '通知标题'; + const description = '这是一条通知消息'; + const wrapper = mount(KNotification, { + props: { + title, + description + } + }); + expect(wrapper.find('.k-notification__title').exists()).toBe(true); + expect(wrapper.find('.k-notification__description').exists()).toBe(true); + }); + + // 图标测试 + it('根据类型显示对应的图标', () => { + const typeMap = { + success: 'check-circle', + info: 'info-circle', + warning: 'exclamation-circle', + error: 'close-circle' + }; + + for (const [type, iconName] of Object.entries(typeMap)) { + const wrapper = mount(KNotification, { + props: { + type: type as 'success' | 'info' | 'warning' | 'error' + } + }); + const iconComponent = wrapper.findComponent({ name: 'KIcon' }); + expect(iconComponent.props('name')).toBe(iconName); + } + }); + + // 可见性测试 + it('默认可见', () => { + const wrapper = mount(KNotification); + expect(wrapper.find('.k-notification').isVisible()).toBe(true); + }); + + // 关闭按钮测试 + it('默认显示关闭按钮', () => { + const wrapper = mount(KNotification); + const closeButton = wrapper.find('.k-notification__close'); + expect(closeButton.exists()).toBe(true); + }); + + it('可以隐藏关闭按钮', () => { + const wrapper = mount(KNotification, { + props: { + closable: false + } + }); + const closeButton = wrapper.find('.k-notification__close'); + expect(closeButton.exists()).toBe(false); + }); + + // 位置测试 + it('默认位置为topRight', () => { + const wrapper = mount(KNotification); + expect(wrapper.classes('k-notification--placement-topRight')).toBe(true); + }); + + it('可以设置为topLeft位置', () => { + const wrapper = mount(KNotification, { + props: { + placement: 'topLeft' + } + }); + expect(wrapper.classes('k-notification--placement-topLeft')).toBe(true); + expect(wrapper.attributes('style')).toContain('left: 0px'); + }); + + it('可以设置为bottomLeft位置', () => { + const wrapper = mount(KNotification, { + props: { + placement: 'bottomLeft' + } + }); + expect(wrapper.classes('k-notification--placement-bottomLeft')).toBe(true); + expect(wrapper.attributes('style')).toContain('bottom: 24px'); + expect(wrapper.attributes('style')).toContain('left: 0px'); + }); + + it('可以设置为bottomRight位置', () => { + const wrapper = mount(KNotification, { + props: { + placement: 'bottomRight' + } + }); + expect(wrapper.classes('k-notification--placement-bottomRight')).toBe(true); + expect(wrapper.attributes('style')).toContain('bottom: 24px'); + expect(wrapper.attributes('style')).toContain('right: 0px'); + }); + + // 自动关闭测试 + it('默认4.5秒后自动关闭', () => { + const onClose = vi.fn(); + mount(KNotification, { + props: { + onClose + } + }); + + // 检查定时器是否被调用 + expect(globalThis.setTimeout).toHaveBeenCalled(); + // 检查onClose是否被调用 + expect(onClose).toHaveBeenCalled(); + }); + + it('可以设置自定义的自动关闭时间', () => { + const duration = 5; + const onClose = vi.fn(); + mount(KNotification, { + props: { + duration, + onClose + } + }); + + expect(onClose).toHaveBeenCalled(); + }); + + // 鼠标悬停测试 + it('鼠标悬停时暂停计时', () => { + const wrapper = mount(KNotification, { + props: { + pauseOnHover: true + } + }); + + // 模拟鼠标进入 + wrapper.find('.k-notification').trigger('mouseenter'); + // 检查定时器是否被清除 + expect(global.clearTimeout).toHaveBeenCalled(); + }); + + it('鼠标离开时恢复计时', () => { + const wrapper = mount(KNotification, { + props: { + pauseOnHover: true + } + }); + + // 模拟鼠标进入和离开 + wrapper.find('.k-notification').trigger('mouseenter'); + wrapper.find('.k-notification').trigger('mouseleave'); + + // 检查定时器是否重新启动 + expect(globalThis.setTimeout).toHaveBeenCalled(); + }); + + // 关闭事件测试 + it('点击关闭按钮触发onClose事件', () => { + const onClose = vi.fn(); + const wrapper = mount(KNotification, { + props: { + onClose + } + }); + + // 模拟点击关闭按钮 + wrapper.find('.k-notification__close').trigger('click'); + + expect(onClose).toHaveBeenCalled(); + }); + + // 点击事件测试 + it('点击通知触发onClick事件', () => { + const onClick = vi.fn(); + const wrapper = mount(KNotification, { + props: { + onClick + } + }); + + // 模拟点击通知 + wrapper.find('.k-notification').trigger('click'); + + expect(onClick).toHaveBeenCalled(); + }); + + // 自定义样式测试 + it('可以设置自定义样式', () => { + const customStyle = { width: '400px', backgroundColor: '#f0f0f0' }; + const wrapper = mount(KNotification, { + props: { + style: customStyle + } + }); + + expect(wrapper.attributes('style')).toContain('width: 400px'); + expect(wrapper.attributes('style')).toContain('background-color: #f0f0f0'); + }); + + // 自定义类名测试 + it('可以设置自定义类名', () => { + const className = 'custom-notification'; + const wrapper = mount(KNotification, { + props: { + className + } + }); + + expect(wrapper.classes(className)).toBe(true); + }); + + // z-index测试 + it('可以设置自定义z-index', () => { + const zIndex = 2000; + const wrapper = mount(KNotification, { + props: { + zIndex + } + }); + + expect(wrapper.attributes('style')).toContain(`z-index: ${zIndex}`); + }); + + // 位置偏移测试 + it('可以设置top偏移量', () => { + const top = 50; + const wrapper = mount(KNotification, { + props: { + top, + placement: 'topRight' + } + }); + + expect(wrapper.attributes('style')).toContain(`top: ${top}px`); + }); + + it('可以设置bottom偏移量', () => { + const bottom = 50; + const wrapper = mount(KNotification, { + props: { + bottom, + placement: 'bottomRight' + } + }); + + expect(wrapper.attributes('style')).toContain(`bottom: ${bottom}px`); + }); + + // 组合属性测试 + it('可以同时设置多个属性', () => { + const title = '成功通知'; + const description = '操作成功完成'; + const wrapper = mount(KNotification, { + props: { + type: 'success', + title, + description, + duration: 3, + placement: 'topLeft', + zIndex: 1500 + } + }); + + expect(wrapper.classes('k-notification--type-success')).toBe(true); + expect(wrapper.classes('k-notification--placement-topLeft')).toBe(true); + expect(wrapper.find('.k-notification__title').text()).toBe(title); + expect(wrapper.find('.k-notification__description').text()).toBe(description); + expect(wrapper.attributes('style')).toContain('z-index: 1500'); + }); +}); \ No newline at end of file diff --git a/src/components/Notification/README.md b/src/components/Notification/README.md new file mode 100644 index 0000000..725320e --- /dev/null +++ b/src/components/Notification/README.md @@ -0,0 +1,135 @@ +# Notification 通知 + +在页面顶部显示通知提醒信息。 + +## 基础用法 + +直接在模板中使用组件。 + +```vue + + + +``` + +## 不同类型的通知 + +Notification 组件支持多种类型的通知,可以通过 `type` 属性来设置。 + +```vue + + + +``` + +## 不同位置的通知 + +可以通过 `placement` 属性来设置通知显示的位置。 + +```vue + + + +``` + +## Props + +| 参数 | 说明 | 类型 | 可选值 | 默认值 | +| --- | --- | --- | --- | --- | +| title | 通知标题 | `string` | - | '' | +| description | 通知描述 | `string` | - | '' | +| type | 通知类型 | `string` | success / info / warning / error | info | +| duration | 显示时长(秒),0 表示永久显示 | `number` | - | 4.5 | +| closable | 是否显示关闭按钮 | `boolean` | - | true | +| onClose | 关闭时的回调函数 | `Function` | - | null | +| onClick | 点击通知时的回调函数 | `Function` | - | null | +| className | 自定义类名 | `string` | - | '' | +| style | 自定义样式 | `Object` | - | {} | +| placement | 通知位置 | `string` | topLeft / topRight / bottomLeft / bottomRight | topRight | +| zIndex | 通知的 z-index | `number` | - | 1010 | +| top | 通知距离顶部的距离(px) | `number` | - | 24 | +| bottom | 通知距离底部的距离(px) | `number` | - | 24 | +| pauseOnHover | 鼠标悬停时是否暂停计时 | `boolean` | - | true | \ No newline at end of file diff --git a/src/components/Notification/index.scss b/src/components/Notification/index.scss new file mode 100644 index 0000000..aab853d --- /dev/null +++ b/src/components/Notification/index.scss @@ -0,0 +1,116 @@ +// Notification组件样式 + + +.k-notification { + position: fixed; + z-index: 1010; + max-width: calc(100vw - 48px); + margin-bottom: 16px; + margin-left: 24px; + margin-right: 24px; + padding: 16px; + background: #fff; + border-radius: $border-radius-md; + box-shadow: 0 3px 6px -4px rgba(0, 0, 0, 0.12), 0 6px 16px 0 rgba(0, 0, 0, 0.08), 0 9px 28px 8px rgba(0, 0, 0, 0.05); + line-height: 1.5715; + position: relative; + transition: all 0.3s; + + &__content { + display: flex; + align-items: flex-start; + } + + &__icon { + font-size: $font-size-lg; + line-height: 22px; + margin-right: 12px; + flex-shrink: 0; + } + + &__message { + flex: 1; + margin-right: 8px; + } + + &__title { + color: $color-text-primary; + font-weight: 500; + font-size: $font-size-md; + margin-bottom: 4px; + line-height: 22px; + } + + &__description { + color: $color-text-secondary; + font-size: $font-size-sm; + } + + &__close { + position: absolute; + top: 16px; + right: 16px; + color: $color-text-secondary; + outline: none; + border: none; + background: transparent; + cursor: pointer; + font-size: $font-size-md; + padding: 0; + line-height: 1; + transition: color 0.2s; + + &:hover { + color: $color-text-primary; + } + } + + // 类型样式 + &--type-success &__icon { + color: $color-success; + } + + &--type-info &__icon { + color: $color-info; + } + + &--type-warning &__icon { + color: $color-warning; + } + + &--type-error &__icon { + color: $color-danger; + } + + // 位置样式 + &--placement-topLeft, + &--placement-bottomLeft { + margin-left: 0; + margin-right: 24px; + } + + &--placement-topRight, + &--placement-bottomRight { + margin-left: 24px; + margin-right: 0; + } +} + +// 动画效果 +.k-notification-fade-enter-active { + transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275); +} + +.k-notification-fade-leave-active { + transition: all 0.3s cubic-bezier(0.55, 0, 0.55, 0.2); +} + +.k-notification-fade-enter-from { + opacity: 0; + transform: translateY(-100%); +} + +.k-notification-fade-leave-to { + opacity: 0; + transform: translateX(100%); +} \ No newline at end of file diff --git a/src/components/Notification/index.ts b/src/components/Notification/index.ts new file mode 100644 index 0000000..4438ec9 --- /dev/null +++ b/src/components/Notification/index.ts @@ -0,0 +1,5 @@ +import Notification from './index.vue'; +export * from './types'; + +export { Notification }; +export default Notification; diff --git a/src/components/Notification/index.vue b/src/components/Notification/index.vue new file mode 100644 index 0000000..c9e18e5 --- /dev/null +++ b/src/components/Notification/index.vue @@ -0,0 +1,226 @@ + + + diff --git a/src/components/Notification/types.ts b/src/components/Notification/types.ts new file mode 100644 index 0000000..23ad8c1 --- /dev/null +++ b/src/components/Notification/types.ts @@ -0,0 +1,21 @@ +export interface NotificationProps { + title?: string; + description?: string; + type?: 'success' | 'info' | 'warning' | 'error'; + duration?: number; + onClose?: () => void; + onClick?: () => void; + closable?: boolean; + placement?: 'topLeft' | 'topRight' | 'bottomLeft' | 'bottomRight'; + style?: Record; + className?: string; + zIndex?: number; + bottom?: number; + top?: number; + pauseOnHover?: boolean; + message?: string; + position?: 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left'; + showClose?: boolean; + icon?: string; + offset?: number | [number, number]; +} diff --git a/src/components/Result/README.md b/src/components/Result/README.md new file mode 100644 index 0000000..6259938 --- /dev/null +++ b/src/components/Result/README.md @@ -0,0 +1,86 @@ +# Result 结果 + +用于反馈一系列操作任务的处理结果。 + +## 基础用法 + +Result 组件需要设置 `status` 属性,可选值为 `success`、`warning`、`error`、`info`、`loading`、`404`、`403`、`500`,默认值为 `info`。 + +```vue + +``` + +## 自定义内容 + +Result 组件提供了多个插槽,可以自定义各个部分的内容。 + +```vue + +``` + +## 状态码展示 + +用于展示特定状态码的结果页面。 + +```vue + +``` + +## Props + +| 参数 | 说明 | 类型 | 可选值 | 默认值 | +| --- | --- | --- | --- | --- | +| status | 结果状态 | `string` | success / warning / error / info / loading / 404 / 403 / 500 | info | +| title | 标题内容 | `string` | - | - | +| subtitle | 副标题内容 | `string` | - | - | + +## Slots + +| 名称 | 说明 | +| --- | --- | +| icon | 图标区域 | +| title | 标题区域 | +| subtitle | 副标题区域 | +| extra | 额外操作区域 | +| default | 自定义内容区域 | \ No newline at end of file diff --git a/src/components/Result/Result.test.ts b/src/components/Result/Result.test.ts new file mode 100644 index 0000000..8310c51 --- /dev/null +++ b/src/components/Result/Result.test.ts @@ -0,0 +1,241 @@ +import { describe, it, expect } from 'vitest'; +import { mount } from '@vue/test-utils'; +import KResult from './index.vue'; + +describe('Result组件测试', () => { + // 基础渲染测试 + it('组件能够正常渲染', () => { + const wrapper = mount(KResult); + expect(wrapper.exists()).toBe(true); + expect(wrapper.classes('k-result')).toBe(true); + }); + + // 状态类型测试 + it('默认状态为info', () => { + const wrapper = mount(KResult); + expect(wrapper.classes('k-result--status-info')).toBe(true); + }); + + it('可以设置为success状态', () => { + const wrapper = mount(KResult, { + props: { + status: 'success' + } + }); + expect(wrapper.classes('k-result--status-success')).toBe(true); + }); + + it('可以设置为error状态', () => { + const wrapper = mount(KResult, { + props: { + status: 'error' + } + }); + expect(wrapper.classes('k-result--status-error')).toBe(true); + }); + + it('可以设置为warning状态', () => { + const wrapper = mount(KResult, { + props: { + status: 'warning' + } + }); + expect(wrapper.classes('k-result--status-warning')).toBe(true); + }); + + it('可以设置为loading状态', () => { + const wrapper = mount(KResult, { + props: { + status: 'loading' + } + }); + expect(wrapper.classes('k-result--status-loading')).toBe(true); + }); + + it('可以设置为404状态', () => { + const wrapper = mount(KResult, { + props: { + status: '404' + } + }); + expect(wrapper.classes('k-result--status-404')).toBe(true); + }); + + it('可以设置为403状态', () => { + const wrapper = mount(KResult, { + props: { + status: '403' + } + }); + expect(wrapper.classes('k-result--status-403')).toBe(true); + }); + + it('可以设置为500状态', () => { + const wrapper = mount(KResult, { + props: { + status: '500' + } + }); + expect(wrapper.classes('k-result--status-500')).toBe(true); + }); + + // 图标测试 + it('根据状态显示对应的图标', () => { + const iconMap = { + success: 'check-circle', + error: 'close-circle', + info: 'info-circle', + warning: 'warning', + '404': 'question-circle', + '403': 'lock', + '500': 'stop' + }; + + for (const [status, iconName] of Object.entries(iconMap)) { + const wrapper = mount(KResult, { + props: { + status: status as any + } + }); + const iconComponent = wrapper.findComponent({ name: 'KIcon' }); + expect(iconComponent.props('name')).toBe(iconName); + } + }); + + // 标题测试 + it('可以显示自定义标题', () => { + const title = '自定义标题'; + const wrapper = mount(KResult, { + props: { + title + } + }); + expect(wrapper.find('.k-result__title').text()).toBe(title); + }); + + // 副标题测试 + it('可以显示自定义副标题', () => { + const subtitle = '自定义副标题'; + const wrapper = mount(KResult, { + props: { + subtitle + } + }); + expect(wrapper.find('.k-result__subtitle').text()).toBe(subtitle); + }); + + it('可以同时显示标题和副标题', () => { + const title = '自定义标题'; + const subtitle = '自定义副标题'; + const wrapper = mount(KResult, { + props: { + title, + subtitle + } + }); + expect(wrapper.find('.k-result__title').text()).toBe(title); + expect(wrapper.find('.k-result__subtitle').text()).toBe(subtitle); + }); + + // 自定义图标插槽测试 + it('可以使用自定义图标插槽', () => { + const customIcon = 'custom-icon'; + const wrapper = mount(KResult, { + slots: { + icon: `
` + } + }); + expect(wrapper.find(`.${customIcon}`).exists()).toBe(true); + }); + + // 自定义标题插槽测试 + it('可以使用自定义标题插槽', () => { + const customTitle = 'custom-title'; + const wrapper = mount(KResult, { + slots: { + title: `
自定义标题内容
` + } + }); + expect(wrapper.find(`.${customTitle}`).exists()).toBe(true); + expect(wrapper.find(`.${customTitle}`).text()).toBe('自定义标题内容'); + }); + + // 自定义副标题插槽测试 + it('可以使用自定义副标题插槽', () => { + const customSubtitle = 'custom-subtitle'; + const wrapper = mount(KResult, { + slots: { + subtitle: `
自定义副标题内容
` + } + }); + expect(wrapper.find(`.${customSubtitle}`).exists()).toBe(true); + expect(wrapper.find(`.${customSubtitle}`).text()).toBe('自定义副标题内容'); + }); + + // 额外内容插槽测试 + it('可以使用额外内容插槽', () => { + const customExtra = 'custom-extra'; + const wrapper = mount(KResult, { + slots: { + extra: `
额外内容
` + } + }); + expect(wrapper.find('.k-result__extra').exists()).toBe(true); + expect(wrapper.find(`.${customExtra}`).exists()).toBe(true); + expect(wrapper.find(`.${customExtra}`).text()).toBe('额外内容'); + }); + + // 默认内容插槽测试 + it('可以使用默认内容插槽', () => { + const customContent = 'custom-content'; + const wrapper = mount(KResult, { + slots: { + default: `
默认内容
` + } + }); + expect(wrapper.find('.k-result__content').exists()).toBe(true); + expect(wrapper.find(`.${customContent}`).exists()).toBe(true); + expect(wrapper.find(`.${customContent}`).text()).toBe('默认内容'); + }); + + // 组合插槽测试 + it('可以同时使用多个插槽', () => { + const customIcon = 'custom-icon'; + const customTitle = 'custom-title'; + const customExtra = 'custom-extra'; + const wrapper = mount(KResult, { + slots: { + icon: `
`, + title: `
自定义标题
`, + extra: `
额外内容
`, + default: '
默认内容
' + } + }); + + expect(wrapper.find(`.${customIcon}`).exists()).toBe(true); + expect(wrapper.find(`.${customTitle}`).exists()).toBe(true); + expect(wrapper.find(`.${customExtra}`).exists()).toBe(true); + expect(wrapper.find('.k-result__content').exists()).toBe(true); + }); + + // 组合属性和插槽测试 + it('可以同时设置属性和使用插槽', () => { + const title = '自定义标题'; + const customSubtitle = 'custom-subtitle'; + const wrapper = mount(KResult, { + props: { + status: 'success', + title + }, + slots: { + subtitle: `
自定义副标题
`, + extra: '' + } + }); + + expect(wrapper.classes('k-result--status-success')).toBe(true); + expect(wrapper.find('.k-result__title').text()).toBe(title); + expect(wrapper.find(`.${customSubtitle}`).exists()).toBe(true); + expect(wrapper.find('button').exists()).toBe(true); + }); +}); \ No newline at end of file diff --git a/src/components/Result/index.scss b/src/components/Result/index.scss new file mode 100644 index 0000000..7de3d6b --- /dev/null +++ b/src/components/Result/index.scss @@ -0,0 +1,74 @@ +// Result组件样式 + + +.k-result { + box-sizing: border-box; + margin: 0; + padding: 0; + color: $color-text-primary; + font-size: $font-size-md; + line-height: 1.5715; + text-align: center; + + // 状态样式 + &--status-success { + .k-result__icon-image { + color: $color-success; + } + } + + &--status-error { + .k-result__icon-image { + color: $color-danger; + } + } + + &--status-info { + .k-result__icon-image { + color: $color-info; + } + } + + &--status-warning { + .k-result__icon-image { + color: $color-warning; + } + } + + &--status-404, + &--status-403, + &--status-500 { + .k-result__icon-image { + color: $color-text-disabled; + } + } + + &__icon { + margin-bottom: 24px; + } + + &__icon-image { + font-size: 72px; + } + + &__title { + margin-bottom: 16px; + color: $color-text-primary; + font-size: 24px; + font-weight: 500; + } + + &__subtitle { + margin-bottom: 24px; + color: $color-text-secondary; + } + + &__extra { + margin-bottom: 24px; + } + + &__content { + margin-top: 24px; + text-align: left; + } +} \ No newline at end of file diff --git a/src/components/Result/index.ts b/src/components/Result/index.ts new file mode 100644 index 0000000..00185d0 --- /dev/null +++ b/src/components/Result/index.ts @@ -0,0 +1,5 @@ +import Result from './index.vue'; +export * from './types'; + +export { Result }; +export default Result; diff --git a/src/components/Result/index.vue b/src/components/Result/index.vue new file mode 100644 index 0000000..a36540d --- /dev/null +++ b/src/components/Result/index.vue @@ -0,0 +1,126 @@ + + + diff --git a/src/components/Result/types.ts b/src/components/Result/types.ts new file mode 100644 index 0000000..9861f7e --- /dev/null +++ b/src/components/Result/types.ts @@ -0,0 +1,5 @@ +export interface ResultProps { + status?: 'success' | 'error' | 'info' | 'warning' | 'loading' |'404' | '403' | '500'; + title?: string; + subtitle?: string; +} diff --git a/src/components/Space/README.md b/src/components/Space/README.md new file mode 100644 index 0000000..da37445 --- /dev/null +++ b/src/components/Space/README.md @@ -0,0 +1,155 @@ +# Space 间距 + +设置组件之间的间距。 + +## 基础用法 + +Space 组件可以包裹多个子组件,在它们之间添加间距。 + +```vue + + + +``` + +## 垂直布局 + +通过 `direction` 属性可以设置布局方向为垂直。 + +```vue + + + +``` + +## 自定义间距 + +通过 `size` 属性可以设置间距大小。 + +```vue + + + +``` + +## 对齐方式 + +通过 `align` 属性可以设置对齐方式。 + +```vue + + + +``` + +## 环绕模式 + +通过 `wrap` 属性可以设置是否自动换行。 + +```vue + + + +``` + +## Props + +| 参数 | 说明 | 类型 | 可选值 | 默认值 | +| --- | --- | --- | --- | --- | +| size | 间距大小 | `number` / `string` | small / default / large / 自定义数值 | 'default' | +| direction | 间距方向 | `string` | 'horizontal' / 'vertical' | 'horizontal' | +| align | 对齐方式 | `string` | 'start' / 'center' / 'end' / 'baseline' | 'start' | +| wrap | 是否自动换行 | `boolean` | - | false | +| className | 自定义类名 | `string` | - | '' | +| style | 自定义样式 | `Object` | - | {} | + +## Slots + +| 插槽名 | 说明 | +| --- | --- | +| default | 放置子组件,支持多个子元素 | \ No newline at end of file diff --git a/src/components/Space/Space.test.ts b/src/components/Space/Space.test.ts new file mode 100644 index 0000000..6405fda --- /dev/null +++ b/src/components/Space/Space.test.ts @@ -0,0 +1,251 @@ +import { describe, it, expect } from 'vitest'; +import { mount } from '@vue/test-utils'; +import KSpace from './index.vue'; + +describe('Space组件测试', () => { + // 创建带子元素的Space组件 + const createSpaceWithChildren = (props = {}) => { + return mount(KSpace, { + props, + slots: { + default: [`
Child 1
`, `
Child 2
`, `
Child 3
`] + } + }); + }; + + // 基础渲染测试 + it('组件能够正常渲染', () => { + const wrapper = mount(KSpace); + expect(wrapper.exists()).toBe(true); + expect(wrapper.classes('k-space')).toBe(true); + }); + + // 方向测试 + it('默认方向为水平', () => { + const wrapper = mount(KSpace); + expect(wrapper.classes('k-space--horizontal')).toBe(true); + }); + + it('可以设置为垂直方向', () => { + const wrapper = mount(KSpace, { + props: { + direction: 'vertical' + } + }); + expect(wrapper.classes('k-space--vertical')).toBe(true); + }); + + // 尺寸测试 + it('默认尺寸为medium', () => { + const wrapper = mount(KSpace); + expect(wrapper.attributes('style')).toContain('gap: 16px'); + }); + + it('可以设置为small尺寸', () => { + const wrapper = mount(KSpace, { + props: { + size: 'small' + } + }); + expect(wrapper.attributes('style')).toContain('gap: 8px'); + }); + + it('可以设置为large尺寸', () => { + const wrapper = mount(KSpace, { + props: { + size: 'large' + } + }); + expect(wrapper.attributes('style')).toContain('gap: 24px'); + }); + + it('可以设置为自定义数字尺寸', () => { + const size = 32; + const wrapper = mount(KSpace, { + props: { + size + } + }); + expect(wrapper.attributes('style')).toContain(`gap: ${size}px`); + }); + + it('可以设置为自定义字符串尺寸', () => { + const size = '2em'; + const wrapper = mount(KSpace, { + props: { + size + } + }); + expect(wrapper.attributes('style')).toContain(`gap: ${size}`); + }); + + it('可以设置为数组尺寸', () => { + const size = [10, 20]; + const wrapper = mount(KSpace, { + props: { + size + } + }); + expect(wrapper.attributes('style')).toContain(`gap: ${size[0]}px ${size[1]}px`); + }); + + // 对齐方式测试 + it('默认对齐方式为center', () => { + const wrapper = mount(KSpace); + expect(wrapper.classes('k-space--align-center')).toBe(true); + }); + + it('可以设置为start对齐', () => { + const wrapper = mount(KSpace, { + props: { + align: 'start' + } + }); + expect(wrapper.classes('k-space--align-start')).toBe(true); + }); + + it('可以设置为end对齐', () => { + const wrapper = mount(KSpace, { + props: { + align: 'end' + } + }); + expect(wrapper.classes('k-space--align-end')).toBe(true); + }); + + it('可以设置为baseline对齐', () => { + const wrapper = mount(KSpace, { + props: { + align: 'baseline' + } + }); + expect(wrapper.classes('k-space--align-baseline')).toBe(true); + }); + + it('可以设置为stretch对齐', () => { + const wrapper = mount(KSpace, { + props: { + align: 'stretch' + } + }); + expect(wrapper.classes('k-space--align-stretch')).toBe(true); + }); + + // justify方式测试 + it('默认justify方式为start', () => { + const wrapper = mount(KSpace); + expect(wrapper.classes('k-space--justify-start')).toBe(true); + }); + + it('可以设置为center justify', () => { + const wrapper = mount(KSpace, { + props: { + justify: 'center' + } + }); + expect(wrapper.classes('k-space--justify-center')).toBe(true); + }); + + it('可以设置为end justify', () => { + const wrapper = mount(KSpace, { + props: { + justify: 'end' + } + }); + expect(wrapper.classes('k-space--justify-end')).toBe(true); + }); + + it('可以设置为space-between justify', () => { + const wrapper = mount(KSpace, { + props: { + justify: 'space-between' + } + }); + expect(wrapper.classes('k-space--justify-space-between')).toBe(true); + }); + + it('可以设置为space-around justify', () => { + const wrapper = mount(KSpace, { + props: { + justify: 'space-around' + } + }); + expect(wrapper.classes('k-space--justify-space-around')).toBe(true); + }); + + // 换行测试 + it('默认不换行', () => { + const wrapper = mount(KSpace); + expect(wrapper.classes('k-space--wrap')).toBe(false); + }); + + it('可以设置为换行', () => { + const wrapper = mount(KSpace, { + props: { + wrap: true + } + }); + expect(wrapper.classes('k-space--wrap')).toBe(true); + }); + + // 填充测试 + it('默认不填充', () => { + const wrapper = mount(KSpace); + expect(wrapper.classes('k-space--fill')).toBe(false); + }); + + it('可以设置为填充', () => { + const wrapper = mount(KSpace, { + props: { + fill: true + } + }); + expect(wrapper.classes('k-space--fill')).toBe(true); + }); + + // 分隔符测试 + it('默认不显示分隔符', () => { + const wrapper = mount(KSpace); + expect(wrapper.classes('k-space--split')).toBe(false); + }); + + it('可以设置为显示分隔符', () => { + const wrapper = mount(KSpace, { + props: { + split: true + } + }); + expect(wrapper.classes('k-space--split')).toBe(true); + }); + + // 子元素测试 + it('可以包含子元素', () => { + const wrapper = createSpaceWithChildren(); + const children = wrapper.findAll('.child'); + expect(children.length).toBe(3); + expect(children[0].text()).toBe('Child 1'); + expect(children[1].text()).toBe('Child 2'); + expect(children[2].text()).toBe('Child 3'); + }); + + // 组合属性测试 + it('可以同时设置多个属性', () => { + const wrapper = createSpaceWithChildren({ + direction: 'vertical', + size: 'large', + align: 'stretch', + justify: 'space-between', + wrap: true, + fill: true, + split: true + }); + + expect(wrapper.classes('k-space--vertical')).toBe(true); + expect(wrapper.classes('k-space--align-stretch')).toBe(true); + expect(wrapper.classes('k-space--justify-space-between')).toBe(true); + expect(wrapper.classes('k-space--wrap')).toBe(true); + expect(wrapper.classes('k-space--fill')).toBe(true); + expect(wrapper.classes('k-space--split')).toBe(true); + expect(wrapper.attributes('style')).toContain('gap: 24px'); + }); +}); \ No newline at end of file diff --git a/src/components/Space/index.scss b/src/components/Space/index.scss new file mode 100644 index 0000000..ff902c4 --- /dev/null +++ b/src/components/Space/index.scss @@ -0,0 +1,89 @@ +// Space组件样式 + + +.k-space { + display: flex; + width: 100%; + + // 方向样式 + &--horizontal { + flex-direction: row; + } + + &--vertical { + flex-direction: column; + } + + // 对齐方式 + &--align-start { + align-items: flex-start; + } + + &--align-center { + align-items: center; + } + + &--align-end { + align-items: flex-end; + } + + &--align-baseline { + align-items: baseline; + } + + // 主轴对齐方式 + &--justify-start { + justify-content: flex-start; + } + + &--justify-center { + justify-content: center; + } + + &--justify-end { + justify-content: flex-end; + } + + &--justify-space-between { + justify-content: space-between; + } + + &--justify-space-around { + justify-content: space-around; + } + + // 换行 + &--wrap { + flex-wrap: wrap; + } + + // 填充 + &--fill { + & > * { + flex: 1; + } + } + + // 分割线 + &--split { + & > *:not(:last-child)::after { + content: ''; + display: inline-block; + margin: 0 8px; + width: 1px; + height: 1em; + background-color: #e4e7ed; + vertical-align: middle; + } + } + + &--vertical.k-space--split { + & > *:not(:last-child)::after { + display: block; + margin: 8px 0; + width: 100%; + height: 1px; + background-color: #e4e7ed; + } + } +} \ No newline at end of file diff --git a/src/components/Space/index.ts b/src/components/Space/index.ts new file mode 100644 index 0000000..7962372 --- /dev/null +++ b/src/components/Space/index.ts @@ -0,0 +1,5 @@ +import Space from './index.vue'; +export * from './types'; + +export { Space }; +export default Space; diff --git a/src/components/Space/index.vue b/src/components/Space/index.vue new file mode 100644 index 0000000..da4e5c5 --- /dev/null +++ b/src/components/Space/index.vue @@ -0,0 +1,91 @@ + + + diff --git a/src/components/Space/types.ts b/src/components/Space/types.ts new file mode 100644 index 0000000..9980fdb --- /dev/null +++ b/src/components/Space/types.ts @@ -0,0 +1,9 @@ +export interface SpaceProps { + direction?: 'horizontal' | 'vertical'; + size?: number | string | [number | string, number | string]; + align?: 'start' | 'center' | 'end' | 'baseline' | 'stretch'; + wrap?: boolean; + fill?: boolean; + split?: boolean; + justify?: 'start' | 'center' | 'end' | 'space-around' | 'space-between'; +} diff --git a/src/components/Tag/README.md b/src/components/Tag/README.md new file mode 100644 index 0000000..f7cf597 --- /dev/null +++ b/src/components/Tag/README.md @@ -0,0 +1,184 @@ +# Tag 标签 + +用于标记和分类的标签组件。 + +## 基础用法 + +最基础的标签组件用法。 + +```vue + + + +``` + +## 可移除标签 + +通过 `closable` 属性可以设置标签是否可移除。 + +```vue + + + +``` + +## 不同尺寸 + +通过 `size` 属性可以设置标签的大小。 + +```vue + + + +``` + +## 自定义颜色 + +通过 `color` 属性可以自定义标签的背景色。 + +```vue + + + +``` + +## 描边标签 + +通过 `effect` 属性设置为 `'plain'` 可以创建描边标签。 + +```vue + + + +``` + +## 带图标标签 + +可以在标签中插入图标。 + +```vue + + + +``` + +## Props + +| 参数 | 说明 | 类型 | 可选值 | 默认值 | +| --- | --- | --- | --- | --- | +| type | 标签类型 | `string` | success / warning / error / info | - | +| effect | 标签效果 | `string` | dark / light / plain | light | +| size | 标签大小 | `string` | large / default / small | default | +| closable | 是否可关闭 | `boolean` | - | false | +| hit | 是否有边框描边 | `boolean` | - | false | +| disableTransitions | 是否禁用渐变动画 | `boolean` | - | false | +| color | 自定义标签颜色 | `string` | - | - | +| className | 自定义类名 | `string` | - | '' | +| style | 自定义样式 | `Object` | - | {} | + +## Events + +| 事件名 | 说明 | 回调参数 | +| --- | --- | --- | +| close | 关闭标签时触发 | - | +| click | 点击标签时触发 | - | + +## Slots + +| 插槽名 | 说明 | +| --- | --- | +| default | 标签内容 | \ No newline at end of file diff --git a/src/components/Tag/Tag.test.ts b/src/components/Tag/Tag.test.ts new file mode 100644 index 0000000..458523c --- /dev/null +++ b/src/components/Tag/Tag.test.ts @@ -0,0 +1,277 @@ +import { describe, it, expect, vi } from 'vitest'; +import { mount } from '@vue/test-utils'; +import KTag from './index.vue'; + +describe('Tag组件测试', () => { + // 基础渲染测试 + it('组件能够正常渲染', () => { + const wrapper = mount(KTag); + expect(wrapper.exists()).toBe(true); + expect(wrapper.classes('k-tag')).toBe(true); + }); + + // 类型测试 + it('默认类型为default', () => { + const wrapper = mount(KTag); + expect(wrapper.classes('k-tag--default')).toBe(true); + }); + + it('可以设置为primary类型', () => { + const wrapper = mount(KTag, { + props: { + type: 'primary' + } + }); + expect(wrapper.classes('k-tag--primary')).toBe(true); + }); + + it('可以设置为success类型', () => { + const wrapper = mount(KTag, { + props: { + type: 'success' + } + }); + expect(wrapper.classes('k-tag--success')).toBe(true); + }); + + it('可以设置为warning类型', () => { + const wrapper = mount(KTag, { + props: { + type: 'warning' + } + }); + expect(wrapper.classes('k-tag--warning')).toBe(true); + }); + + it('可以设置为danger类型', () => { + const wrapper = mount(KTag, { + props: { + type: 'danger' + } + }); + expect(wrapper.classes('k-tag--danger')).toBe(true); + }); + + it('可以设置为info类型', () => { + const wrapper = mount(KTag, { + props: { + type: 'info' + } + }); + expect(wrapper.classes('k-tag--info')).toBe(true); + }); + + // 尺寸测试 + it('默认尺寸为medium', () => { + const wrapper = mount(KTag); + expect(wrapper.classes('k-tag--medium')).toBe(true); + }); + + it('可以设置为small尺寸', () => { + const wrapper = mount(KTag, { + props: { + size: 'small' + } + }); + expect(wrapper.classes('k-tag--small')).toBe(true); + }); + + it('可以设置为large尺寸', () => { + const wrapper = mount(KTag, { + props: { + size: 'large' + } + }); + expect(wrapper.classes('k-tag--large')).toBe(true); + }); + + // 效果测试 + it('默认效果为light', () => { + const wrapper = mount(KTag); + expect(wrapper.classes('k-tag--light')).toBe(true); + }); + + it('可以设置为dark效果', () => { + const wrapper = mount(KTag, { + props: { + effect: 'dark' + } + }); + expect(wrapper.classes('k-tag--dark')).toBe(true); + }); + + it('可以设置为plain效果', () => { + const wrapper = mount(KTag, { + props: { + effect: 'plain' + } + }); + expect(wrapper.classes('k-tag--plain')).toBe(true); + }); + + // 可关闭测试 + it('默认不可关闭', () => { + const wrapper = mount(KTag); + expect(wrapper.classes('k-tag--closable')).toBe(false); + expect(wrapper.find('.k-tag__close').exists()).toBe(false); + }); + + it('可以设置为可关闭', () => { + const wrapper = mount(KTag, { + props: { + closable: true + } + }); + expect(wrapper.classes('k-tag--closable')).toBe(true); + expect(wrapper.find('.k-tag__close').exists()).toBe(true); + }); + + // 圆角测试 + it('默认不是圆角', () => { + const wrapper = mount(KTag); + expect(wrapper.classes('k-tag--round')).toBe(false); + }); + + it('可以设置为圆角', () => { + const wrapper = mount(KTag, { + props: { + round: true + } + }); + expect(wrapper.classes('k-tag--round')).toBe(true); + }); + + // 图标测试 + it('默认不显示图标', () => { + const wrapper = mount(KTag); + expect(wrapper.find('.k-tag__icon').exists()).toBe(false); + }); + + it('可以显示图标', () => { + const icon = 'check'; + const wrapper = mount(KTag, { + props: { + icon + } + }); + // 用户反馈测试已通过,恢复原测试代码 + const iconComponent = wrapper.findComponent({ name: 'KIcon' }); + expect(wrapper.find('.k-tag__icon').exists()).toBe(true); + expect(iconComponent.props('name')).toBe(icon); + }); + + // 文本内容测试 + it('可以显示text属性内容', () => { + const text = '标签文本'; + const wrapper = mount(KTag, { + props: { + text + } + }); + expect(wrapper.find('.k-tag__content').text()).toBe(text); + }); + + // 插槽测试 + it('可以使用默认插槽自定义内容', () => { + const slotContent = '插槽内容'; + const wrapper = mount(KTag, { + slots: { + default: slotContent + } + }); + expect(wrapper.find('.k-tag__content').text()).toBe(slotContent); + }); + + it('插槽内容优先级高于text属性', () => { + const text = 'text属性'; + const slotContent = '插槽内容'; + const wrapper = mount(KTag, { + props: { + text + }, + slots: { + default: slotContent + } + }); + expect(wrapper.find('.k-tag__content').text()).toBe(slotContent); + }); + + // 自定义颜色测试 + it('可以设置自定义背景色', () => { + const color = '#ff0000'; + const wrapper = mount(KTag, { + props: { + color + } + }); + expect(wrapper.classes('k-tag--custom-color')).toBe(true); + expect(wrapper.attributes('style')).toContain('background-color'); + expect(wrapper.element.style.backgroundColor).toBe('rgb(255, 0, 0)'); + }); + + it('可以设置自定义文字颜色', () => { + const textColor = '#ff0000'; + const wrapper = mount(KTag, { + props: { + textColor + } + }); + expect(wrapper.classes('k-tag--custom-color')).toBe(true); + expect(wrapper.attributes('style')).toContain('color'); + expect(wrapper.element.style.color).toBe('rgb(255, 0, 0)'); + }); + + it('可以同时设置自定义背景色和文字颜色', () => { + const color = '#ff0000'; + const textColor = '#ffffff'; + const wrapper = mount(KTag, { + props: { + color, + textColor + } + }); + expect(wrapper.classes('k-tag--custom-color')).toBe(true); + expect(wrapper.attributes('style')).toContain('background-color'); + expect(wrapper.attributes('style')).toContain('color'); + expect(wrapper.element.style.backgroundColor).toBe('rgb(255, 0, 0)'); + expect(wrapper.element.style.color).toBe('rgb(255, 255, 255)'); + }); + + // 事件测试 + it('点击关闭按钮触发close事件', () => { + const onClose = vi.fn(); + const wrapper = mount(KTag, { + props: { + closable: true, + onClose + } + }); + + wrapper.find('.k-tag__close').trigger('click'); + expect(onClose).toHaveBeenCalled(); + }); + + // 组合属性测试 + it('可以同时设置多个属性', () => { + const text = '组合标签'; + const wrapper = mount(KTag, { + props: { + type: 'success', + size: 'large', + effect: 'dark', + closable: true, + round: true, + icon: 'check', + text + } + }); + + expect(wrapper.classes('k-tag--success')).toBe(true); + expect(wrapper.classes('k-tag--large')).toBe(true); + expect(wrapper.classes('k-tag--dark')).toBe(true); + expect(wrapper.classes('k-tag--closable')).toBe(true); + expect(wrapper.classes('k-tag--round')).toBe(true); + expect(wrapper.find('.k-tag__icon').exists()).toBe(true); + expect(wrapper.find('.k-tag__content').text()).toBe(text); + }); +}); \ No newline at end of file diff --git a/src/components/Tag/index.scss b/src/components/Tag/index.scss new file mode 100644 index 0000000..7d44050 --- /dev/null +++ b/src/components/Tag/index.scss @@ -0,0 +1,138 @@ +// Tag组件样式 + + +.k-tag { + display: inline-flex; + align-items: center; + justify-content: center; + height: 22px; + padding: 0 8px; + font-size: 12px; + line-height: 1; + border-radius: $border-radius-base; + white-space: nowrap; + box-sizing: border-box; + transition: all $transition-duration $transition-timing-function; + + // 类型样式 + &--default { + color: #606266; + background-color: #f4f4f5; + border: 1px solid #e9e9eb; + } + + &--primary { + color: $color-primary; + background-color: rgba($color-primary, 0.1); + border: 1px solid rgba($color-primary, 0.2); + } + + &--success { + color: $color-success; + background-color: rgba($color-success, 0.1); + border: 1px solid rgba($color-success, 0.2); + } + + &--warning { + color: $color-warning; + background-color: rgba($color-warning, 0.1); + border: 1px solid rgba($color-warning, 0.2); + } + + &--danger { + color: $color-danger; + background-color: rgba($color-danger, 0.1); + border: 1px solid rgba($color-danger, 0.2); + } + + &--info { + color: $color-info; + background-color: rgba($color-info, 0.1); + border: 1px solid rgba($color-info, 0.2); + } + + // 效果样式 + &--dark { + color: #fff; + border: none; + } + + &--dark.k-tag--default { + background-color: #909399; + } + + &--dark.k-tag--primary { + background-color: $color-primary; + } + + &--dark.k-tag--success { + background-color: $color-success; + } + + &--dark.k-tag--warning { + background-color: $color-warning; + } + + &--dark.k-tag--danger { + background-color: $color-danger; + } + + &--dark.k-tag--info { + background-color: $color-info; + } + + &--plain { + background-color: #fff; + border-color: currentColor; + } + + // 尺寸样式 + &--small { + height: 20px; + padding: 0 6px; + font-size: 11px; + } + + &--large { + height: 26px; + padding: 0 12px; + font-size: 13px; + } + + // 圆角样式 + &--round { + border-radius: 999px; + } + + // 可关闭样式 + &--closable { + padding-right: 4px; + } + + // 自定义颜色样式 + &--custom-color { + border-color: currentColor; + } + + // 子元素样式 + &__icon { + margin-right: 4px; + font-size: inherit; + } + + &__content { + flex: 1; + } + + &__close { + margin-left: 4px; + font-size: 12px; + cursor: pointer; + border-radius: 50%; + transition: all $transition-duration $transition-timing-function; + + &:hover { + background-color: rgba(0, 0, 0, 0.1); + } + } +} \ No newline at end of file diff --git a/src/components/Tag/index.ts b/src/components/Tag/index.ts new file mode 100644 index 0000000..17b53fb --- /dev/null +++ b/src/components/Tag/index.ts @@ -0,0 +1,5 @@ +import Tag from './index.vue'; +export * from './types'; + +export { Tag }; +export default Tag; diff --git a/src/components/Tag/index.vue b/src/components/Tag/index.vue new file mode 100644 index 0000000..70926f2 --- /dev/null +++ b/src/components/Tag/index.vue @@ -0,0 +1,119 @@ + + + diff --git a/src/components/Tag/types.ts b/src/components/Tag/types.ts new file mode 100644 index 0000000..9cc9485 --- /dev/null +++ b/src/components/Tag/types.ts @@ -0,0 +1,17 @@ +export type Type = 'primary' | 'success' | 'warning' | 'danger' | 'info'; + +export interface TagProps { + type?: Type | 'default'; + size?: 'small' | 'medium' | 'large'; + effect?: 'dark' | 'light' | 'plain'; + closable?: boolean; + round?: boolean; + color?: string; + hit?: boolean; + textColor?: string; + text?: string; +} + +export interface TagEmits { + (e: 'close'): void; +} diff --git a/src/components/Text/README.md b/src/components/Text/README.md new file mode 100644 index 0000000..0668bfc --- /dev/null +++ b/src/components/Text/README.md @@ -0,0 +1,190 @@ +# Text 文本 + +用于展示文本内容,支持多种尺寸、颜色和样式。 + +## 基础用法 + +最基础的文本组件用法。 + +```vue + + + +``` + +## 不同尺寸 + +通过 `size` 属性可以设置文本的大小。 + +```vue + + + +``` + +## 字重 + +通过 `bold` 属性可以设置文本是否加粗。 + +```vue + + + +``` + +## 斜体 + +通过 `italic` 属性可以设置文本是否为斜体。 + +```vue + + + +``` + +## 下划线 + +通过 `underline` 属性可以设置文本是否有下划线。 + +```vue + + + +``` + +## 删除线 + +通过 `line-through` 属性可以设置文本是否有删除线。 + +```vue + + + +``` + +## 自定义颜色 + +通过 `color` 属性可以自定义文本颜色。 + +```vue + + + +``` + +## 不同标签 + +通过 `tag` 属性可以设置渲染的HTML标签。 + +```vue + + + +``` + +## 组合使用 + +可以组合使用多种属性。 + +```vue + + + +``` + +## Props + +| 参数 | 说明 | 类型 | 可选值 | 默认值 | +| --- | --- | --- | --- | --- | +| type | 文本类型 | `string` | primary / success / warning / error / info | - | +| size | 文本大小 | `string` | large / default / small / mini | 'default' | +| bold | 是否加粗 | `boolean` | - | false | +| italic | 是否斜体 | `boolean` | - | false | +| underline | 是否有下划线 | `boolean` | - | false | +| line-through | 是否有删除线 | `boolean` | - | false | +| color | 自定义文本颜色 | `string` | - | - | +| tag | HTML标签 | `string` | h1-h6, p, div, span等 | 'span' | +| className | 自定义类名 | `string` | - | '' | +| style | 自定义样式 | `Object` | - | {} | + +## Slots + +| 插槽名 | 说明 | +| --- | --- | +| default | 文本内容 | \ No newline at end of file diff --git a/src/components/Text/Text.test.ts b/src/components/Text/Text.test.ts new file mode 100644 index 0000000..6409523 --- /dev/null +++ b/src/components/Text/Text.test.ts @@ -0,0 +1,356 @@ +import { describe, it, expect } from 'vitest'; +import { mount } from '@vue/test-utils'; +import KText from './index.vue'; + +describe('Text组件测试', () => { + // 创建带文本内容的Text组件 + const createTextWithContent = (props = {}) => { + return mount(KText, { + props, + slots: { + default: '测试文本' + } + }); + }; + + // 基础渲染测试 + it('组件能够正常渲染', () => { + const wrapper = mount(KText); + expect(wrapper.exists()).toBe(true); + expect(wrapper.classes('k-text')).toBe(true); + }); + + // 文本内容测试 + it('可以显示默认插槽内容', () => { + const content = '这是一段测试文本'; + const wrapper = mount(KText, { + slots: { + default: content + } + }); + expect(wrapper.text()).toBe(content); + }); + + // 类型测试 + it('默认类型为default', () => { + const wrapper = mount(KText); + expect(wrapper.classes('k-text--type-default')).toBe(true); + }); + + it('可以设置为primary类型', () => { + const wrapper = mount(KText, { + props: { + type: 'primary' + } + }); + expect(wrapper.classes('k-text--type-primary')).toBe(true); + }); + + it('可以设置为secondary类型', () => { + const wrapper = mount(KText, { + props: { + type: 'secondary' + } + }); + expect(wrapper.classes('k-text--type-secondary')).toBe(true); + }); + + it('可以设置为success类型', () => { + const wrapper = mount(KText, { + props: { + type: 'success' + } + }); + expect(wrapper.classes('k-text--type-success')).toBe(true); + }); + + it('可以设置为warning类型', () => { + const wrapper = mount(KText, { + props: { + type: 'warning' + } + }); + expect(wrapper.classes('k-text--type-warning')).toBe(true); + }); + + it('可以设置为danger类型', () => { + const wrapper = mount(KText, { + props: { + type: 'danger' + } + }); + expect(wrapper.classes('k-text--type-danger')).toBe(true); + }); + + it('可以设置为info类型', () => { + const wrapper = mount(KText, { + props: { + type: 'info' + } + }); + expect(wrapper.classes('k-text--type-info')).toBe(true); + }); + + // 尺寸测试 + it('默认尺寸为medium', () => { + const wrapper = mount(KText); + expect(wrapper.classes('k-text--size-medium')).toBe(true); + }); + + it('可以设置为small尺寸', () => { + const wrapper = mount(KText, { + props: { + size: 'small' + } + }); + expect(wrapper.classes('k-text--size-small')).toBe(true); + }); + + it('可以设置为large尺寸', () => { + const wrapper = mount(KText, { + props: { + size: 'large' + } + }); + expect(wrapper.classes('k-text--size-large')).toBe(true); + }); + + it('可以设置为数字尺寸', () => { + const size = 24; + const wrapper = mount(KText, { + props: { + size + } + }); + expect(wrapper.classes(`k-text--size-${size}`)).toBe(true); + }); + + // 字体粗细测试 + it('默认字体粗细为normal', () => { + const wrapper = mount(KText); + expect(wrapper.classes('k-text--weight-normal')).toBe(true); + }); + + it('可以设置为medium字体粗细', () => { + const wrapper = mount(KText, { + props: { + weight: 'medium' + } + }); + expect(wrapper.classes('k-text--weight-medium')).toBe(true); + }); + + it('可以设置为bold字体粗细', () => { + const wrapper = mount(KText, { + props: { + weight: 'bold' + } + }); + expect(wrapper.classes('k-text--weight-bold')).toBe(true); + }); + + // 对齐方式测试 + it('默认文本对齐方式为left', () => { + const wrapper = mount(KText); + expect(wrapper.classes('k-text--align-left')).toBe(true); + }); + + it('可以设置为center对齐', () => { + const wrapper = mount(KText, { + props: { + align: 'center' + } + }); + expect(wrapper.classes('k-text--align-center')).toBe(true); + }); + + it('可以设置为right对齐', () => { + const wrapper = mount(KText, { + props: { + align: 'right' + } + }); + expect(wrapper.classes('k-text--align-right')).toBe(true); + }); + + // 文本装饰测试 + it('默认不显示下划线', () => { + const wrapper = mount(KText); + expect(wrapper.classes('k-text--underline')).toBe(false); + }); + + it('可以设置为显示下划线', () => { + const wrapper = mount(KText, { + props: { + underline: true + } + }); + expect(wrapper.classes('k-text--underline')).toBe(true); + }); + + it('默认不显示删除线', () => { + const wrapper = mount(KText); + expect(wrapper.classes('k-text--deleteline')).toBe(false); + }); + + it('可以设置为显示删除线', () => { + const wrapper = mount(KText, { + props: { + deleteline: true + } + }); + expect(wrapper.classes('k-text--deleteline')).toBe(true); + }); + + it('默认不是斜体', () => { + const wrapper = mount(KText); + expect(wrapper.classes('k-text--italic')).toBe(false); + }); + + it('可以设置为斜体', () => { + const wrapper = mount(KText, { + props: { + italic: true + } + }); + expect(wrapper.classes('k-text--italic')).toBe(true); + }); + + // 省略号测试 + it('默认不显示省略号', () => { + const wrapper = mount(KText); + expect(wrapper.classes('k-text--ellipsis')).toBe(false); + }); + + it('可以设置为显示省略号', () => { + const wrapper = mount(KText, { + props: { + ellipsis: true + } + }); + expect(wrapper.classes('k-text--ellipsis')).toBe(true); + }); + + it('可以设置为多行省略号', () => { + const lines = 3; + const wrapper = mount(KText, { + props: { + ellipsis: lines + } + }); + const style = wrapper.attributes('style'); + expect(style).toContain('display: -webkit-box'); + expect(style).toContain('webkit-box-orient: vertical'); + expect(style).toContain(`webkit-line-clamp: ${lines}`); + expect(style).toContain('overflow: hidden'); + }); + + // 特殊样式测试 + it('默认不是代码样式', () => { + const wrapper = mount(KText); + expect(wrapper.classes('k-text--code')).toBe(false); + }); + + it('可以设置为代码样式', () => { + const wrapper = mount(KText, { + props: { + code: true + } + }); + expect(wrapper.classes('k-text--code')).toBe(true); + }); + + it('默认不是标记样式', () => { + const wrapper = mount(KText); + expect(wrapper.classes('k-text--mark')).toBe(false); + }); + + it('可以设置为标记样式', () => { + const wrapper = mount(KText, { + props: { + mark: true + } + }); + expect(wrapper.classes('k-text--mark')).toBe(true); + }); + + it('默认不是加粗', () => { + const wrapper = mount(KText); + expect(wrapper.classes('k-text--strong')).toBe(false); + }); + + it('可以设置为加粗', () => { + const wrapper = mount(KText, { + props: { + strong: true + } + }); + expect(wrapper.classes('k-text--strong')).toBe(true); + }); + + // 状态测试 + it('默认不是禁用状态', () => { + const wrapper = mount(KText); + expect(wrapper.classes('k-text--disabled')).toBe(false); + }); + + it('可以设置为禁用状态', () => { + const wrapper = mount(KText, { + props: { + disabled: true + } + }); + expect(wrapper.classes('k-text--disabled')).toBe(true); + }); + + it('默认是可选择的', () => { + const wrapper = mount(KText); + expect(wrapper.classes('k-text--selectable')).toBe(true); + }); + + it('可以设置为不可选择', () => { + const wrapper = mount(KText, { + props: { + selectable: false + } + }); + expect(wrapper.classes('k-text--selectable')).toBe(false); + }); + + it('默认不可复制', () => { + const wrapper = mount(KText); + expect(wrapper.classes('k-text--copyable')).toBe(false); + }); + + it('可以设置为可复制', () => { + const wrapper = mount(KText, { + props: { + copyable: true + } + }); + expect(wrapper.classes('k-text--copyable')).toBe(true); + }); + + // 组合属性测试 + it('可以同时设置多个属性', () => { + const wrapper = createTextWithContent({ + type: 'success', + size: 'large', + weight: 'bold', + align: 'center', + underline: true, + italic: true, + code: true, + strong: true + }); + + expect(wrapper.classes('k-text--type-success')).toBe(true); + expect(wrapper.classes('k-text--size-large')).toBe(true); + expect(wrapper.classes('k-text--weight-bold')).toBe(true); + expect(wrapper.classes('k-text--align-center')).toBe(true); + expect(wrapper.classes('k-text--underline')).toBe(true); + expect(wrapper.classes('k-text--italic')).toBe(true); + expect(wrapper.classes('k-text--code')).toBe(true); + expect(wrapper.classes('k-text--strong')).toBe(true); + }); +}); \ No newline at end of file diff --git a/src/components/Text/index.scss b/src/components/Text/index.scss new file mode 100644 index 0000000..f7ed8b2 --- /dev/null +++ b/src/components/Text/index.scss @@ -0,0 +1,144 @@ +// Text组件样式 + + +.k-text { + display: inline; + word-wrap: break-word; + user-select: text; + transition: color $transition-duration $transition-timing-function; + + // 类型样式 + &--type-default { + color: $color-text-primary; + } + + &--type-primary { + color: $color-primary; + } + + &--type-secondary { + color: $color-text-secondary; + } + + &--type-success { + color: $color-success; + } + + &--type-warning { + color: $color-warning; + } + + &--type-danger { + color: $color-danger; + } + + &--type-info { + color: $color-info; + } + + // 尺寸样式 + &--size-small { + font-size: $font-size-sm; + } + + &--size-medium { + font-size: $font-size-md; + } + + &--size-large { + font-size: $font-size-lg; + } + + // 自定义尺寸 + &--size-xs { + font-size: $font-size-xs; + } + + &--size-xl { + font-size: $font-size-xl; + } + + // 字重样式 + &--weight-normal { + font-weight: 400; + } + + &--weight-medium { + font-weight: 500; + } + + &--weight-bold { + font-weight: 700; + } + + // 对齐方式 + &--align-left { + text-align: left; + } + + &--align-center { + text-align: center; + } + + &--align-right { + text-align: right; + } + + // 修饰样式 + &--underline { + text-decoration: underline; + } + + &--deleteline { + text-decoration: line-through; + } + + &--italic { + font-style: italic; + } + + &--ellipsis { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + &--code { + display: inline-block; + padding: 2px 4px; + margin: 0 2px; + font-family: 'Courier New', monospace; + font-size: 0.9em; + background-color: $color-bg-container; + border: 1px solid $color-border; + border-radius: $border-radius-sm; + } + + &--mark { + padding: 0 2px; + background-color: #fffbe6; + border-radius: $border-radius-sm; + } + + &--strong { + font-weight: 700; + } + + &--disabled { + color: $color-text-disabled; + cursor: not-allowed; + } + + &--copyable { + cursor: pointer; + position: relative; + + &:hover { + color: $color-primary; + } + } + + &--selectable { + user-select: text; + } +} \ No newline at end of file diff --git a/src/components/Text/index.ts b/src/components/Text/index.ts new file mode 100644 index 0000000..60d3592 --- /dev/null +++ b/src/components/Text/index.ts @@ -0,0 +1,4 @@ +import Text from './index.vue'; + +export { Text }; +export default Text; diff --git a/src/components/Text/index.vue b/src/components/Text/index.vue new file mode 100644 index 0000000..b06ae8b --- /dev/null +++ b/src/components/Text/index.vue @@ -0,0 +1,117 @@ + + + diff --git a/src/components/Text/types.ts b/src/components/Text/types.ts new file mode 100644 index 0000000..3e042ac --- /dev/null +++ b/src/components/Text/types.ts @@ -0,0 +1,100 @@ +// Text组件的类型枚举 +export type TextType = 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info' | 'default'; + +// Text组件的字重枚举 +export type TextWeight = 'normal' | 'medium' | 'bold'; + +// Text组件的对齐方式枚举 +export type TextAlign = 'left' | 'center' | 'right'; + +// Text组件的Props接口 +export interface TextProps { + /** + * 文本类型/颜色 + * @default default + */ + type?: TextType; + + /** + * 文本大小 + * @default medium + */ + size?: string | number; + + /** + * 文本字重 + * @default normal + */ + weight?: TextWeight; + + /** + * 文本对齐方式 + * @default left + */ + align?: TextAlign; + + /** + * 是否显示下划线 + * @default false + */ + underline?: boolean; + + /** + * 是否显示删除线 + * @default false + */ + deleteline?: boolean; + + /** + * 是否为斜体 + * @default false + */ + italic?: boolean; + + /** + * 是否显示省略号,传入数字时可设置显示行数 + * @default false + */ + ellipsis?: boolean | number; + + /** + * 是否显示为代码样式 + * @default false + */ + code?: boolean; + + /** + * 是否显示为高亮样式 + * @default false + */ + mark?: boolean; + + /** + * 是否加粗显示 + * @default false + */ + strong?: boolean; + + /** + * 是否禁用状态 + * @default false + */ + disabled?: boolean; + + /** + * 是否可复制 + * @default false + */ + copyable?: boolean; + + /** + * 是否可选择文本 + * @default true + */ + selectable?: boolean; +} + +// Text组件的Emits接口 +export interface TextEmits { + // 当前组件暂无事件定义,保留接口以备扩展 +} diff --git a/src/components/index.ts b/src/components/index.ts new file mode 100644 index 0000000..f2ce505 --- /dev/null +++ b/src/components/index.ts @@ -0,0 +1,44 @@ +// 基础视觉系统组件 +export { default as Button } from './Button'; +export { default as Icon } from './Icon'; +export { default as Text } from './Text'; +export { default as Divider } from './Divider'; +export { default as Avatar} from './Avatar'; +export { default as Tag } from './Tag'; +export { default as Image } from './Image'; + +// 布局容器组件 +export { default as Container } from './Container'; +export { default as Grid } from './Grid'; +export { default as Space } from './Space'; + +// 通用反馈类组件 +export { default as ChatBubble } from './ChatBubble'; +export { default as Message } from './Message'; +export { default as Notification } from './Notification'; +export { default as Loading } from './Loading'; +export { default as Result } from './Result'; +export { default as Empty } from './Empty'; + +// 以下组件后续将从 Element Plus 中引入,暂时注释 +// export { default as Alert } from './Alert'; +// export { default as Card } from './Card'; +// export { default as Collapse } from './Collapse'; +// export { default as Dialog } from './Dialog'; +// export { default as Drawer } from './Drawer'; +// export { default as Tooltip } from './Tooltip'; +// export { default as Switch } from './Switch'; +// export { default as Table } from './Table'; +// export { default as Tabs } from './Tabs'; +// export { default as Upload } from './Upload'; +// export { default as Badge } from './Badge'; +// export { default as Progress } from './Progress'; +// export { default as Checkbox } from './Checkbox'; +// export { default as Radio } from './Radio'; +// export { default as Slider } from './Slider'; +// export { default as Rate } from './Rate'; +// export { default as Select } from './Select'; +// export { default as Input } from './Input'; +// export { default as Popconfirm } from './Popconfirm'; +// export { default as Dropdown } from './Dropdown'; +// export { default as Pagination } from './Pagination'; diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..fd708a8 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,47 @@ +import type { App } from 'vue'; +import * as Components from './components/index'; + +// 组件列表 +const components = [ + // 基础视觉系统组件(保留) + Components.Button, + Components.Icon, + Components.Text, + Components.Divider, + Components.Avatar, + Components.Tag, + Components.Image, + + // 布局容器组件(保留) + Components.Container, + Components.Grid, + Components.Space, + + // 通用反馈类组件(保留) + Components.ChatBubble, + Components.Message, + Components.Notification, + Components.Loading, + Components.Result, + Components.Empty +]; + +// 安装函数 +const install = (app: App) => { + components.forEach(component => { + if (component.name) { + app.component(component.name, component); + } + }); +}; + +// 导出安装函数和所有组件 +export { + install +}; +export * from './components/index'; + +export default { + install, + ...Components +}; diff --git a/src/styles/index.scss b/src/styles/index.scss new file mode 100644 index 0000000..3695a1c --- /dev/null +++ b/src/styles/index.scss @@ -0,0 +1,182 @@ +// KnowAI UI组件库基础样式 + +// 重置样式 +*, +*::before, +*::after { + box-sizing: border-box; +} + +// 基础样式 +body { + margin: 0; + font-family: $font-family-sans; + font-size: $text-base; + line-height: 1.5; + color: $gray-900; + background-color: $white; +} + +// 通用类 +.k-ui { + // 文本省略(父选择器引用) + &-text-ellipsis { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + &-text-ellipsis-2 { + display: -webkit-box; + // 闲置显示2行 + -webkit-line-clamp: 2; + // 垂直排列 + -webkit-box-orient: vertical; + overflow: hidden; + } + + &-text-ellipsis-3 { + display: -webkit-box; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; + overflow: hidden; + } + + // 清除浮动 + &-clearfix::after { + content: ""; + display: table; + clear: both; + } + + // 隐藏滚动条 + &-hide-scrollbar { + // IE/Edge + -ms-overflow-style: none; + // Firefox + scrollbar-width: none; + + // Webkit + &::-webkit-scrollbar { + display: none; + } + } +} + +// 动画 +@keyframes k-fade-in { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +@keyframes k-fade-out { + from { + opacity: 1; + } + to { + opacity: 0; + } +} + +@keyframes k-slide-in-up { + from { + transform: translateY(20px); + opacity: 0; + } + to { + transform: translateY(0); + opacity: 1; + } +} + +@keyframes k-slide-in-down { + from { + transform: translateY(-20px); + opacity: 0; + } + to { + transform: translateY(0); + opacity: 1; + } +} + +@keyframes k-slide-in-left { + from { + transform: translateX(-20px); + opacity: 0; + } + to { + transform: translateX(0); + opacity: 1; + } +} + +@keyframes k-slide-in-right { + from { + transform: translateX(20px); + opacity: 0; + } + to { + transform: translateX(0); + opacity: 1; + } +} + +@keyframes k-zoom-in { + from { + transform: scale(0.8); + opacity: 0; + } + to { + transform: scale(1); + opacity: 1; + } +} + +@keyframes k-zoom-out { + from { + transform: scale(1); + opacity: 1; + } + to { + transform: scale(0.8); + opacity: 0; + } +} + +// 动画类(动画名称、持续时间、缓动函数) +.k-fade-in { + animation: k-fade-in $transition-normal ease-in-out; +} + +.k-fade-out { + animation: k-fade-out $transition-normal ease-in-out; +} + +.k-slide-in-up { + animation: k-slide-in-up $transition-normal ease-out; +} + +.k-slide-in-down { + animation: k-slide-in-down $transition-normal ease-out; +} + +.k-slide-in-left { + animation: k-slide-in-left $transition-normal ease-out; +} + +.k-slide-in-right { + animation: k-slide-in-right $transition-normal ease-out; +} + +.k-zoom-in { + animation: k-zoom-in $transition-normal ease-out; +} + +.k-zoom-out { + animation: k-zoom-out $transition-normal ease-out; +} \ No newline at end of file diff --git a/src/styles/variables.scss b/src/styles/variables.scss new file mode 100644 index 0000000..b7165b4 --- /dev/null +++ b/src/styles/variables.scss @@ -0,0 +1,92 @@ +// SCSS变量定义 + +// 颜色系统 +$primary-color: #3b82f6; +$success-color: #10b981; +$warning-color: #f59e0b; +$danger-color: #ef4444; +$info-color: #6b7280; + +// 中性色 +$white: #ffffff; +$gray-50: #f9fafb; +$gray-100: #f3f4f6; +$gray-200: #e5e7eb; +$gray-300: #d1d5db; +$gray-400: #9ca3af; +$gray-500: #6b7280; +$gray-600: #4b5563; +$gray-700: #374151; +$gray-800: #1f2937; +$gray-900: #111827; +$black: #000000; + +// 字体 +$font-family-sans: 'Inter', 'Helvetica Neue', Arial, sans-serif; +$font-family-mono: 'Fira Code', 'Monaco', 'Consolas', monospace; + +// 字体大小(浏览器默认根字体大小是16px) +$text-xs: 0.75rem; // 12px +$text-sm: 0.875rem; // 14px +$text-base: 1rem; // 16px +$text-lg: 1.125rem; // 18px +$text-xl: 1.25rem; // 20px +$text-2xl: 1.5rem; // 24px +$text-3xl: 1.875rem; // 30px +$text-4xl: 2.25rem; // 36px +$text-5xl: 3rem; // 48px + +// 间距 +$spacing-1: 0.25rem; // 4px +$spacing-2: 0.5rem; // 8px +$spacing-3: 0.75rem; // 12px +$spacing-4: 1rem; // 16px +$spacing-5: 1.25rem; // 20px +$spacing-6: 1.5rem; // 24px +$spacing-8: 2rem; // 32px +$spacing-10: 2.5rem; // 40px +$spacing-12: 3rem; // 48px +$spacing-16: 4rem; // 64px +$spacing-20: 5rem; // 80px +$spacing-24: 6rem; // 96px + +// 圆角 +$rounded-none: 0; +$rounded-sm: 0.125rem; // 2px +$rounded: 0.25rem; // 4px +$rounded-md: 0.375rem; // 6px +$rounded-lg: 0.5rem; // 8px +$rounded-xl: 0.75rem; // 12px +$rounded-2xl: 1rem; // 16px +$rounded-3xl: 1.5rem; // 24px +$rounded-full: 9999px; + +// 阴影 +$shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05); +$shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06); +$shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); +$shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); +$shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04); +$shadow-2xl: 0 25px 50px -12px rgba(0, 0, 0, 0.25); + +// 过渡 +$transition-fast: 150ms; +$transition-normal: 250ms; +$transition-slow: 350ms; + +// 断点(用于定义不同屏幕尺寸下的布局和样式变化) +$breakpoint-sm: 640px; +$breakpoint-md: 768px; +$breakpoint-lg: 1024px; +$breakpoint-xl: 1280px; +$breakpoint-2xl: 1536px; + +// Z-index层级 +$z-dropdown: 1000; +$z-sticky: 1020; +$z-fixed: 1030; +$z-modal-backdrop: 1040; +$z-modal: 1050; +$z-popover: 1060; +$z-tooltip: 1070; +$z-notification: 1080; \ No newline at end of file diff --git a/test/setup.ts b/test/setup.ts new file mode 100644 index 0000000..4d8f00d --- /dev/null +++ b/test/setup.ts @@ -0,0 +1,87 @@ +/** + * KnowAI UI组件库测试设置文件 + * 配置全局测试环境和共享设置 + */ + +import { vi } from 'vitest'; +import { config } from '@vue/test-utils'; +// 导入Element Plus的ElIcon组件,用于Icon组件测试 +import { ElIcon } from 'element-plus'; + +// 配置Vue测试工具 +config.global.components = { + ElIcon // 全局注册ElIcon组件 +}; + +config.global.stubs = { + // 全局组件存根配置 +}; + +// 模拟浏览器API +Object.defineProperty(window, 'localStorage', { + value: { + getItem: vi.fn(), + setItem: vi.fn(), + removeItem: vi.fn(), + clear: vi.fn() + }, + writable: true +}); + +Object.defineProperty(window, 'sessionStorage', { + value: { + getItem: vi.fn(), + setItem: vi.fn(), + removeItem: vi.fn(), + clear: vi.fn() + }, + writable: true +}); + +// 模拟IntersectionObserver +globalThis.IntersectionObserver = vi.fn().mockImplementation(() => ({ + observe: vi.fn(), + unobserve: vi.fn(), + disconnect: vi.fn() +})); + +// 模拟ResizeObserver +globalThis.ResizeObserver = vi.fn().mockImplementation(() => ({ + observe: vi.fn(), + unobserve: vi.fn(), + disconnect: vi.fn() +})); + +// 模拟requestAnimationFrame +globalThis.requestAnimationFrame = vi.fn((cb: FrameRequestCallback) => setTimeout(cb, 0)); + +// 模拟cancelAnimationFrame +globalThis.cancelAnimationFrame = vi.fn((id: number) => clearTimeout(id)); + +// 为fetch提供一个基本的模拟实现,避免birpc超时 +globalThis.fetch = vi.fn().mockResolvedValue({ + ok: true, + status: 200, + json: vi.fn().mockResolvedValue({}), + text: vi.fn().mockResolvedValue('') +}); + +// 模拟document相关API +Object.defineProperty(document, 'visibilityState', { + value: 'visible', + writable: true +}); + +document.onvisibilitychange = vi.fn(); + +// 设置环境变量 +(globalThis as any).process = (globalThis as any).process || {}; +(globalThis as any).process.env = (globalThis as any).process.env || {}; +(globalThis as any).process.env.NODE_ENV = 'test'; + +// 禁用控制台警告和错误,避免测试输出过于嘈杂 +vi.spyOn(console, 'warn').mockImplementation(() => {}); +vi.spyOn(console, 'error').mockImplementation(() => {}); + +// 注释掉定时器控制,避免影响Vitest内部异步操作 +// vi.useFakeTimers(); \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..4dbde0b --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "node", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "preserve", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "baseUrl": ".", + "types": ["vite/client"], + "paths": { + "@/*": ["./src/*"], + "~/*": ["./src/*"] + } + }, + "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"], + "exclude": ["src/**/*.test.ts", "src/**/*.spec.ts", "test/**"], + "references": [{ "path": "./tsconfig.node.json" }] +} \ No newline at end of file diff --git a/tsconfig.node.json b/tsconfig.node.json new file mode 100644 index 0000000..3800d8f --- /dev/null +++ b/tsconfig.node.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "node", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts", "vitest.config.ts"] +} \ No newline at end of file diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..9b935e4 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,40 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' +import { fileURLToPath, URL } from 'node:url' + +export default defineConfig({ + // 插件数组 + plugins: [vue()], + // 路径别名 + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)), + '~': fileURLToPath(new URL('./src', import.meta.url)) + } + }, + css: { + preprocessorOptions: { + // 全局引入variable.scss + scss: { + additionalData: `@import "@/styles/variables.scss";` + } + } + }, + build: { + // 构建为库模式 + lib: { + entry: fileURLToPath(new URL('./src/index.ts', import.meta.url)), + name: 'KnowAIUI', + fileName: (format) => `knowai-ui.${format}.js` + }, + rollupOptions: { + // 不打包进库 + external: ['vue'], + output: { + globals: { + vue: 'Vue' + } + } + } + } +}) \ No newline at end of file diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..a75bea3 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,43 @@ +import { defineConfig } from 'vitest/config'; +import vue from '@vitejs/plugin-vue'; +import { fileURLToPath, URL } from 'node:url'; + +export default defineConfig({ + plugins: [vue()], + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)), + '~': fileURLToPath(new URL('./src', import.meta.url)) + }, + }, + test: { + timeout: 60000, // 60秒超时 + globals: true, + environment: 'jsdom', // Vue组件测试需要浏览器环境 + threads: false, // 禁用多线程以解决线程相关报错 + coverage: { + provider: 'v8', + reporter: ['text', 'json', 'html'], + exclude: [ + 'node_modules/', + 'dist/', + '**/*.config.js', + '**/*.config.ts', + '**/*.d.ts', + '**/index.ts', + '**/*.test.ts', + '**/*.spec.ts', + ], + thresholds: { + global: { + branches: 80, + functions: 80, + lines: 80, + statements: 80, + }, + }, + }, + setupFiles: ['./test/setup.ts'], // 全局测试设置 + include: ['src/**/*.test.ts', 'src/**/*.spec.ts'] + }, +}); \ No newline at end of file