├── .editorconfig ├── .github └── workflows │ └── publish.yml ├── .gitignore ├── .npmrc ├── .prettierignore ├── .prettierrc.json ├── CDNLocalCheck.html ├── LICENSE ├── README.md ├── README_CN.md ├── eslint.config.mjs ├── package.json ├── src ├── assets │ ├── airui-logo.png │ └── home.svg ├── components.d.ts ├── components │ ├── avatar │ │ ├── avatar.md │ │ ├── avatar.scss │ │ ├── avatar.tsx │ │ └── readme.md │ ├── button-group │ │ ├── button-group.css │ │ ├── button-group.spec.ts │ │ ├── button-group.tsx │ │ ├── readme.md │ │ └── showCase.md │ ├── button │ │ ├── __snapshots__ │ │ │ └── button.spec.ts.snap │ │ ├── button.css │ │ ├── button.spec.ts │ │ ├── button.tsx │ │ ├── readme.md │ │ └── showCase.md │ ├── card │ │ ├── card.css │ │ ├── card.tsx │ │ ├── readme.md │ │ └── showcase.md │ ├── chat │ │ ├── chat.css │ │ ├── chat.tsx │ │ └── readme.md │ ├── icon │ │ ├── icon.css │ │ ├── icon.tsx │ │ ├── readme.md │ │ └── usage.md │ ├── input │ │ ├── input.md │ │ ├── input.scss │ │ ├── input.spec.ts │ │ ├── input.tsx │ │ └── readme.md │ ├── previwer │ │ ├── previewer.scss │ │ ├── previwer.tsx │ │ ├── readme.md │ │ └── shwocase.md │ ├── ratingStars │ │ ├── rating.css │ │ ├── rating.md │ │ ├── rating.tsx │ │ └── readme.md │ ├── tag │ │ ├── readme.md │ │ ├── tag.css │ │ ├── tag.md │ │ └── tag.tsx │ ├── text │ │ ├── readme.md │ │ ├── showCase.md │ │ ├── text.css │ │ ├── text.spec.ts │ │ └── text.tsx │ └── user-profile │ │ ├── readme.md │ │ ├── user-profile.md │ │ ├── user-profile.scss │ │ └── user-profile.tsx ├── index.html ├── index.ts ├── styles │ ├── designToken.css │ └── tailwind.css └── utils │ └── utils.tsx ├── stencil.config.ts ├── tailwind.config.js ├── test-project ├── eaample.jsx ├── index.html └── package.json ├── tsconfig.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | indent_style = space 8 | indent_size = 2 9 | end_of_line = lf 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | 13 | [*.md] 14 | insert_final_newline = false 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish to npm and GitHub Packages 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' # 监听标签推送 7 | 8 | jobs: 9 | build-and-publish: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | # 检出代码 14 | - name: Checkout code 15 | uses: actions/checkout@v3 16 | 17 | # 设置 Node.js 环境 18 | - name: Setup Node.js 19 | uses: actions/setup-node@v3 20 | with: 21 | node-version: '18' 22 | cache: 'yarn' 23 | 24 | # 配置 npm 认证(包括 GitHub 和 npm) 25 | - name: Configure npm registry and authenticate 26 | run: | 27 | echo "//npm.pkg.github.com/:_authToken=${{ secrets.GITHUB_TOKEN }}" > ~/.npmrc 28 | echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" >> ~/.npmrc 29 | 30 | # 安装依赖 31 | - name: Install dependencies with yarn 32 | run: yarn install 33 | 34 | # 构建项目 35 | - name: Build project 36 | run: yarn build 37 | 38 | # 运行测试 39 | - name: Run tests 40 | run: yarn test 41 | 42 | # 发布到 GitHub Packages 43 | - name: Publish to GitHub Package 44 | if: startsWith(github.ref, 'refs/tags/') 45 | env: 46 | NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 47 | run: yarn publish --access public --registry=https://npm.pkg.github.com 48 | 49 | # 发布到 npm 50 | - name: Publish to npm 51 | if: startsWith(github.ref, 'refs/tags/') 52 | env: 53 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 54 | run: yarn publish --access public --registry=https://registry.npmjs.org 55 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | docs/ 107 | 108 | dist/ 109 | www/ 110 | loader/ 111 | 112 | *~ 113 | *.sw[mnpcod] 114 | *.log 115 | *.lock 116 | *.tmp 117 | *.tmp.* 118 | log.txt 119 | *.sublime-project 120 | *.sublime-workspace 121 | 122 | .stencil/ 123 | .idea/ 124 | .vscode/ 125 | .sass-cache/ 126 | .versions/ 127 | node_modules/ 128 | $RECYCLE.BIN/ 129 | 130 | .DS_Store 131 | Thumbs.db 132 | UserInterfaceState.xcuserstate 133 | .env 134 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openwebstudio/AirUI/a0efa4ffb6c53fbdef320a23e2ba111e05a09834/.npmrc -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | loader/ 4 | www/ 5 | test-project/ -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": true, 4 | "printWidth": 80, 5 | "tabWidth": 2 6 | } 7 | -------------------------------------------------------------------------------- /CDNLocalCheck.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Test AirComponents 7 | 8 | 9 | 12 | 13 | 14 | 15 | 主按钮 16 | 成功按钮 17 | 信息按钮 18 | 警告按钮 19 | 危险按钮 20 | 幽灵按钮 21 | 22 | 23 | 实心按钮 24 | 描边按钮 25 | 文字按钮 26 | 霓虹按钮 27 | 默认按钮 28 | 29 | 30 | 图标按钮 31 | 后缀图标按钮 32 | 双图标按钮 33 | 34 | 35 | 加载中 36 | 禁用按钮 37 | 禁用图标按钮 38 | 选中按钮 39 | 选中图标按钮 40 | 41 | 42 | 小按钮 43 | 中按钮 44 | 大按钮 45 | 46 | 47 | 48 | 按钮 1 49 | 按钮 2 50 | 按钮 3 51 | 52 | 53 | 54 | 垂直按钮 1 55 | 垂直按钮 2 56 | 57 | 58 | 59 | 按钮 60 | 61 | 70 | 71 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Richard Shephard 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # In Early Development Version, Currently Unavailable 2 | 3 | ![Air-Components Logo](./src/assets/air-components-board.png) 4 | 5 | [中文](https://github.com/SisyphusZheng/Components/blob/main/README_CN.md) 6 | [![npm version](https://img.shields.io/npm/v/air-components)](https://www.npmjs.com/package/air-components) 7 | [![npm downloads](https://img.shields.io/npm/dm/air-components)](https://www.npmjs.com/package/air-components) 8 | [![GitHub license](https://img.shields.io/github/license/aircomponents/Components)](https://github.com/aircomponents/Components/blob/main/LICENSE) 9 | [![Last Commit](https://img.shields.io/github/last-commit/aircomponents/Components)](https://github.com/aircomponents/Components/commits/main) 10 | [![Dependabot Status](https://img.shields.io/badge/dependencies-up%20to%20date-brightgreen)](https://github.com/aircomponents/Components/network/updates) 11 | 12 | ## Project Highlights 13 | 14 | - **Modular Architecture**: Focused on creating reusable and flexible components. 15 | 16 | - **Modern Design Principles**: Emphasizes minimalist UI design with support for multiple themes and visual styles. 17 | - **Customizable Components**: Easily adapt component styles to meet project-specific requirements using standard CSS. 18 | 19 | --- 20 | 21 | ## Installation 22 | 23 | Install Air-Components using npm: 24 | 25 | ```bash 26 | npm install @airdesign/ui 27 | ``` 28 | 29 | ### CDN 30 | 31 | ```html 32 | Test AirComponents 33 | 34 | 35 | 36 | Primary Button 37 | Outline Primary 38 | 39 | 40 | ``` 41 | 42 | ```JS 43 | import '@aircomponents/ui'; 44 | 45 | document.body.innerHTML = ` 46 | Primary Button 47 | `; 48 | ``` 49 | 50 | ## Development Notes 51 | 52 | ### This project is in early development; features and components are actively being built 53 | 54 | ### Contributions and feedback are welcome. Visit our GitHub repository for more information 55 | -------------------------------------------------------------------------------- /README_CN.md: -------------------------------------------------------------------------------- 1 | # In Early Development Version, Currently Unavailable 2 | 3 | ![Air-Components Logo](./src/assets/air-components-board.png) 4 | 5 | [![npm version](https://img.shields.io/npm/v/air-components)](https://www.npmjs.com/package/air-components) 6 | [![npm downloads](https://img.shields.io/npm/dm/air-components)](https://www.npmjs.com/package/air-components) [![GitHub license](https://img.shields.io/github/license/aircomponents/Components)](https://github.com/aircomponents/Components/blob/main/LICENSE) [![Last Commit](https://img.shields.io/github/last-commit/aircomponents/Components)](https://github.com/aircomponents/Components/commits/main) [![Dependabot Status](https://img.shields.io/badge/dependencies-up%20to%20date-brightgreen)](https://github.com/aircomponents/Components/network/updates) 7 | 8 | --- 9 | 10 | ## 项目亮点 11 | 12 | - **模块化架构:**: 专注于创建可复用且灵活的组件 13 | - **现代设计原则**: 强调极简的用户界面设计,同时支持多种主题和视觉风格。 14 | - **可定制的组件**: 通过标准 CSS,轻松调整组件样式以满足特定项目需求。 15 | 16 | --- 17 | 18 | ## 安装 19 | 20 | 使用 npm 安装 AirComponents: 21 | 22 | ```bash 23 | npm install @airdesign/ui 24 | ``` 25 | 26 | ## 使用示例通过CDN 27 | 28 | ```html 29 | Test AirComponents 30 | 31 | 32 | 33 | Primary Button 34 | Outline Primary 35 | 36 | 37 | ``` 38 | 39 | ## 开发说明 40 | 41 | ### 本项目处于早期开发阶段,功能和组件正在积极开发中 42 | 43 | ### 欢迎贡献代码和反馈意见。有关更多信息,请访问我们的 GitHub 仓库 44 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import eslint from '@eslint/js'; 4 | import tseslint from 'typescript-eslint'; 5 | 6 | export default tseslint.config( 7 | eslint.configs.recommended, 8 | tseslint.configs.recommended, 9 | ); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@airdesign/ui", 3 | "version": "0.0.17", 4 | "private": false, 5 | "license": "MIT", 6 | "description": "custom components", 7 | "main": "dist/index.cjs.js", 8 | "module": "dist/index.js", 9 | "es2015": "dist/esm/index.js", 10 | "es2017": "dist/esm/index.js", 11 | "types": "dist/types/index.d.ts", 12 | "collection": "dist/collection/collection-manifest.json", 13 | "collection:main": "dist/collection/index.js", 14 | "unpkg": "dist/aircomponents/aircomponents.esm.js", 15 | "files": [ 16 | "dist/", 17 | "loader/" 18 | ], 19 | "keywords": [ 20 | "stencil", 21 | "web-components", 22 | "components" 23 | ], 24 | "author": "SisyphusZheng", 25 | "homepage": "https://github.com/SisyphusZheng/Components#readme", 26 | "scripts": { 27 | "build": "stencil build --docs", 28 | "start": "stencil build --dev --watch --serve", 29 | "test": "stencil test --spec --e2e", 30 | "test.watch": "stencil test --spec --e2e --watchAll", 31 | "generate": "stencil generate", 32 | "prepare": "yarn run build" 33 | }, 34 | "devDependencies": { 35 | "@eslint/js": "^9.17.0", 36 | "@stencil-community/postcss": "^2.2.0", 37 | "@stencil/core": "^4.23.0", 38 | "@stencil/sass": "^3.0.12", 39 | "@tailwindcss/forms": "^0.5.9", 40 | "@types/jest": "^29.5.14", 41 | "@types/node": "^22.10.2", 42 | "autoprefixer": "^10.4.20", 43 | "cssnano": "^7.0.6", 44 | "eslint": "^9.17.0", 45 | "jest": "^29.7.0", 46 | "postcss": "^8.4.49", 47 | "postcss-svgo": "^7.0.1", 48 | "puppeteer": "^23.11.1", 49 | "stencil-tailwind-plugin": "^1.8.0", 50 | "svgo": "^3.3.2", 51 | "tailwindcss": "^3.4.17", 52 | "ts-jest": "^29.2.5", 53 | "typescript": "^5.7.2", 54 | "typescript-eslint": "^8.19.1" 55 | }, 56 | "publishConfig": { 57 | "registry": "https://registry.npmjs.org", 58 | "access": "public" 59 | }, 60 | "repository": { 61 | "type": "git", 62 | "url": "git+https://github.com/aircomponents/Components.git" 63 | }, 64 | "dependencies": { 65 | "ace-builds": "^1.37.5", 66 | "classnames": "^2.5.1" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/assets/airui-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openwebstudio/AirUI/a0efa4ffb6c53fbdef320a23e2ba111e05a09834/src/assets/airui-logo.png -------------------------------------------------------------------------------- /src/assets/home.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* tslint:disable */ 3 | /** 4 | * This is an autogenerated file created by the Stencil compiler. 5 | * It contains typing information for all components that exist in this project. 6 | */ 7 | import { HTMLStencilElement, JSXBase } from "@stencil/core/internal"; 8 | import { IconSet } from "./components/icon/icon"; 9 | export { IconSet } from "./components/icon/icon"; 10 | export namespace Components { 11 | /** 12 | * @name AirAvatar 13 | * @description 显示用户头像,支持头像图片、姓名首字母或默认样式,支持圆形和方形。 14 | * @category Data Display 15 | * @example 16 | */ 17 | interface AirAvatar { 18 | /** 19 | * 边框样式,可自定义。 20 | */ 21 | "border": string; 22 | /** 23 | * 用户姓名,用于生成首字母缩写。 24 | */ 25 | "name": string; 26 | /** 27 | * 头像形状,支持 'circle' 或 'square'。 28 | */ 29 | "shape": 'circle' | 'square'; 30 | /** 31 | * 头像尺寸,支持 `small`、`medium`、`large` 或自定义值(例如:`5rem`)。 32 | */ 33 | "size": 'small' | 'medium' | 'large' | string; 34 | /** 35 | * 用户头像图片的 URL 地址。 36 | */ 37 | "src": string; 38 | } 39 | interface AirButton { 40 | "disabled": boolean; 41 | "icon": string; 42 | "loading": boolean; 43 | "selected": boolean; 44 | "size": 'small' | 'medium' | 'large'; 45 | "state": | 'primary' 46 | | 'success' 47 | | 'info' 48 | | 'warning' 49 | | 'danger' 50 | | 'ghost' 51 | | 'outline' 52 | | 'solid'; 53 | "suffixIcon": string; 54 | "type": 'button' | 'submit' | 'reset'; 55 | } 56 | interface AirButtonGroup { 57 | "customStyles": { [key: string]: string }; 58 | "direction": "horizontal" | "vertical"; 59 | "group": string; 60 | "spacing": string; 61 | } 62 | interface AirCard { 63 | "cardTitle": string; 64 | "center": boolean; 65 | "content": string; 66 | "imageUrl": string; 67 | "isHighlighted": boolean; 68 | "size": 'small' | 'medium' | 'large' | 'auto'; 69 | } 70 | interface AirChat { 71 | } 72 | interface AirIcon { 73 | "color": string; 74 | "iconSet": IconSet; 75 | "name": string; 76 | "size": string; 77 | } 78 | interface AirInput { 79 | "autofocus": boolean; 80 | "customClass": string; 81 | "customStyle": { [key: string]: string }; 82 | "disabled": boolean; 83 | "error": boolean; 84 | "errorMessage": string; 85 | "label": string; 86 | "maxLength": number; 87 | "minLength": number; 88 | "name": string; 89 | "pattern": string; 90 | "placeholder": string; 91 | "required": boolean; 92 | "type": string; 93 | "value": string; 94 | } 95 | interface AirPreviewer { 96 | "customLink": string; 97 | "size": 'small' | 'medium' | 'large' | 'auto'; 98 | } 99 | interface AirRating { 100 | /** 101 | * 自定义样式(CSS键值对对象) 102 | */ 103 | "customStyle": { [key: string]: string }; 104 | /** 105 | * 未选中图标 106 | */ 107 | "emptyIcon": string; 108 | /** 109 | * 选中图标(直接使用air-icon的name) 110 | */ 111 | "filledIcon": string; 112 | "iconSet": IconSet; 113 | /** 114 | * 当前评分等级 (0~max) 115 | */ 116 | "level": number; 117 | /** 118 | * 最大星数(1-10) 119 | */ 120 | "max": number; 121 | } 122 | interface AirTag { 123 | "closable": boolean; 124 | "color": 'gray' | 'blue' | 'green' | 'yellow' | 'red'; 125 | "rounded": 'none' | 'md' | 'full'; 126 | "size": 'sm' | 'md'; 127 | } 128 | interface AirRating { 129 | /** 130 | * 自定义样式(CSS键值对对象) 131 | */ 132 | "customStyle": { [key: string]: string }; 133 | /** 134 | * 未选中图标 135 | */ 136 | "emptyIcon": string; 137 | /** 138 | * 选中图标(直接使用air-icon的name) 139 | */ 140 | "filledIcon": string; 141 | "iconSet": IconSet; 142 | /** 143 | * 当前评分等级 (0~max) 144 | */ 145 | "level": number; 146 | /** 147 | * 最大星数(1-10) 148 | */ 149 | "max": number; 150 | } 151 | interface AirTag { 152 | "closable": boolean; 153 | "color": 'gray' | 'blue' | 'green' | 'yellow' | 'red'; 154 | "rounded": 'none' | 'md' | 'full'; 155 | "size": 'sm' | 'md'; 156 | } 157 | /** 158 | * @name AirText 159 | * @description Typography for rendering headlines, paragraphs, captions, and body text with various style options. 160 | * @category General 161 | * @example Heading 162 | */ 163 | interface AirText { 164 | "color": | 'primary' 165 | | 'secondary' 166 | | 'tertiary' 167 | | 'helper' 168 | | 'error' 169 | | 'on-color' 170 | | 'inverse'; 171 | "configAria": any; 172 | "expressive": boolean; 173 | "headingLevel": 1 | 2 | 3 | 4 | 5 | 6; 174 | "headingSize": 1 | 2 | 3 | 4 | 5 | 6 | 7; 175 | "inline": boolean; 176 | "type": | 'code' 177 | | 'helper-text' 178 | | 'label' 179 | | 'legal' 180 | | 'heading' 181 | | 'body' 182 | | 'body-compact' 183 | | 'body-large' 184 | | 'body-emphasis' 185 | | 'fluid-heading'; 186 | } 187 | interface AirUserProfile { 188 | "avatarSrc": string; 189 | "editable": boolean; 190 | "userBio": string; 191 | "userName": string; 192 | } 193 | } 194 | export interface AirButtonCustomEvent extends CustomEvent { 195 | detail: T; 196 | target: HTMLAirButtonElement; 197 | } 198 | export interface AirCardCustomEvent extends CustomEvent { 199 | detail: T; 200 | target: HTMLAirCardElement; 201 | } 202 | export interface AirRatingCustomEvent extends CustomEvent { 203 | detail: T; 204 | target: HTMLAirRatingElement; 205 | } 206 | export interface AirTagCustomEvent extends CustomEvent { 207 | detail: T; 208 | target: HTMLAirTagElement; 209 | } 210 | declare global { 211 | /** 212 | * @name AirAvatar 213 | * @description 显示用户头像,支持头像图片、姓名首字母或默认样式,支持圆形和方形。 214 | * @category Data Display 215 | * @example 216 | */ 217 | interface HTMLAirAvatarElement extends Components.AirAvatar, HTMLStencilElement { 218 | } 219 | var HTMLAirAvatarElement: { 220 | prototype: HTMLAirAvatarElement; 221 | new (): HTMLAirAvatarElement; 222 | }; 223 | interface HTMLAirButtonElementEventMap { 224 | "buttonClick": { event: MouseEvent; selected: boolean }; 225 | } 226 | interface HTMLAirButtonElement extends Components.AirButton, HTMLStencilElement { 227 | addEventListener(type: K, listener: (this: HTMLAirButtonElement, ev: AirButtonCustomEvent) => any, options?: boolean | AddEventListenerOptions): void; 228 | addEventListener(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void; 229 | addEventListener(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void; 230 | addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void; 231 | removeEventListener(type: K, listener: (this: HTMLAirButtonElement, ev: AirButtonCustomEvent) => any, options?: boolean | EventListenerOptions): void; 232 | removeEventListener(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | EventListenerOptions): void; 233 | removeEventListener(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | EventListenerOptions): void; 234 | removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void; 235 | } 236 | var HTMLAirButtonElement: { 237 | prototype: HTMLAirButtonElement; 238 | new (): HTMLAirButtonElement; 239 | }; 240 | interface HTMLAirButtonGroupElement extends Components.AirButtonGroup, HTMLStencilElement { 241 | } 242 | var HTMLAirButtonGroupElement: { 243 | prototype: HTMLAirButtonGroupElement; 244 | new (): HTMLAirButtonGroupElement; 245 | }; 246 | interface HTMLAirCardElementEventMap { 247 | "cardClicked": void; 248 | } 249 | interface HTMLAirCardElement extends Components.AirCard, HTMLStencilElement { 250 | addEventListener(type: K, listener: (this: HTMLAirCardElement, ev: AirCardCustomEvent) => any, options?: boolean | AddEventListenerOptions): void; 251 | addEventListener(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void; 252 | addEventListener(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void; 253 | addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void; 254 | removeEventListener(type: K, listener: (this: HTMLAirCardElement, ev: AirCardCustomEvent) => any, options?: boolean | EventListenerOptions): void; 255 | removeEventListener(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | EventListenerOptions): void; 256 | removeEventListener(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | EventListenerOptions): void; 257 | removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void; 258 | } 259 | var HTMLAirCardElement: { 260 | prototype: HTMLAirCardElement; 261 | new (): HTMLAirCardElement; 262 | }; 263 | interface HTMLAirChatElement extends Components.AirChat, HTMLStencilElement { 264 | } 265 | var HTMLAirChatElement: { 266 | prototype: HTMLAirChatElement; 267 | new (): HTMLAirChatElement; 268 | }; 269 | interface HTMLAirIconElement extends Components.AirIcon, HTMLStencilElement { 270 | } 271 | var HTMLAirIconElement: { 272 | prototype: HTMLAirIconElement; 273 | new (): HTMLAirIconElement; 274 | }; 275 | interface HTMLAirInputElement extends Components.AirInput, HTMLStencilElement { 276 | } 277 | var HTMLAirInputElement: { 278 | prototype: HTMLAirInputElement; 279 | new (): HTMLAirInputElement; 280 | }; 281 | interface HTMLAirPreviewerElement extends Components.AirPreviewer, HTMLStencilElement { 282 | } 283 | var HTMLAirPreviewerElement: { 284 | prototype: HTMLAirPreviewerElement; 285 | new (): HTMLAirPreviewerElement; 286 | }; 287 | interface HTMLAirRatingElementEventMap { 288 | "ratingChange": number; 289 | } 290 | interface HTMLAirRatingElement extends Components.AirRating, HTMLStencilElement { 291 | addEventListener(type: K, listener: (this: HTMLAirRatingElement, ev: AirRatingCustomEvent) => any, options?: boolean | AddEventListenerOptions): void; 292 | addEventListener(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void; 293 | addEventListener(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void; 294 | addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void; 295 | removeEventListener(type: K, listener: (this: HTMLAirRatingElement, ev: AirRatingCustomEvent) => any, options?: boolean | EventListenerOptions): void; 296 | removeEventListener(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | EventListenerOptions): void; 297 | removeEventListener(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | EventListenerOptions): void; 298 | removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void; 299 | } 300 | var HTMLAirRatingElement: { 301 | prototype: HTMLAirRatingElement; 302 | new (): HTMLAirRatingElement; 303 | }; 304 | interface HTMLAirTagElementEventMap { 305 | "airClose": void; 306 | } 307 | interface HTMLAirTagElement extends Components.AirTag, HTMLStencilElement { 308 | addEventListener(type: K, listener: (this: HTMLAirTagElement, ev: AirTagCustomEvent) => any, options?: boolean | AddEventListenerOptions): void; 309 | addEventListener(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void; 310 | addEventListener(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void; 311 | addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void; 312 | removeEventListener(type: K, listener: (this: HTMLAirTagElement, ev: AirTagCustomEvent) => any, options?: boolean | EventListenerOptions): void; 313 | removeEventListener(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | EventListenerOptions): void; 314 | removeEventListener(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | EventListenerOptions): void; 315 | removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void; 316 | } 317 | var HTMLAirTagElement: { 318 | prototype: HTMLAirTagElement; 319 | new (): HTMLAirTagElement; 320 | }; 321 | /** 322 | * @name AirText 323 | * @description Typography for rendering headlines, paragraphs, captions, and body text with various style options. 324 | * @category General 325 | * @example Heading 326 | */ 327 | interface HTMLAirTextElement extends Components.AirText, HTMLStencilElement { 328 | } 329 | var HTMLAirTextElement: { 330 | prototype: HTMLAirTextElement; 331 | new (): HTMLAirTextElement; 332 | }; 333 | interface HTMLAirUserProfileElement extends Components.AirUserProfile, HTMLStencilElement { 334 | } 335 | var HTMLAirUserProfileElement: { 336 | prototype: HTMLAirUserProfileElement; 337 | new (): HTMLAirUserProfileElement; 338 | }; 339 | interface HTMLElementTagNameMap { 340 | "air-avatar": HTMLAirAvatarElement; 341 | "air-button": HTMLAirButtonElement; 342 | "air-button-group": HTMLAirButtonGroupElement; 343 | "air-card": HTMLAirCardElement; 344 | "air-chat": HTMLAirChatElement; 345 | "air-icon": HTMLAirIconElement; 346 | "air-input": HTMLAirInputElement; 347 | "air-previewer": HTMLAirPreviewerElement; 348 | "air-rating": HTMLAirRatingElement; 349 | "air-tag": HTMLAirTagElement; 350 | "air-text": HTMLAirTextElement; 351 | "air-user-profile": HTMLAirUserProfileElement; 352 | } 353 | } 354 | declare namespace LocalJSX { 355 | /** 356 | * @name AirAvatar 357 | * @description 显示用户头像,支持头像图片、姓名首字母或默认样式,支持圆形和方形。 358 | * @category Data Display 359 | * @example 360 | */ 361 | interface AirAvatar { 362 | /** 363 | * 边框样式,可自定义。 364 | */ 365 | "border"?: string; 366 | /** 367 | * 用户姓名,用于生成首字母缩写。 368 | */ 369 | "name"?: string; 370 | /** 371 | * 头像形状,支持 'circle' 或 'square'。 372 | */ 373 | "shape"?: 'circle' | 'square'; 374 | /** 375 | * 头像尺寸,支持 `small`、`medium`、`large` 或自定义值(例如:`5rem`)。 376 | */ 377 | "size"?: 'small' | 'medium' | 'large' | string; 378 | /** 379 | * 用户头像图片的 URL 地址。 380 | */ 381 | "src"?: string; 382 | } 383 | interface AirButton { 384 | "disabled"?: boolean; 385 | "icon"?: string; 386 | "loading"?: boolean; 387 | "onButtonClick"?: (event: AirButtonCustomEvent<{ event: MouseEvent; selected: boolean }>) => void; 388 | "selected"?: boolean; 389 | "size"?: 'small' | 'medium' | 'large'; 390 | "state"?: | 'primary' 391 | | 'success' 392 | | 'info' 393 | | 'warning' 394 | | 'danger' 395 | | 'ghost' 396 | | 'outline' 397 | | 'solid'; 398 | "suffixIcon"?: string; 399 | "type"?: 'button' | 'submit' | 'reset'; 400 | } 401 | interface AirButtonGroup { 402 | "customStyles"?: { [key: string]: string }; 403 | "direction"?: "horizontal" | "vertical"; 404 | "group"?: string; 405 | "spacing"?: string; 406 | } 407 | interface AirCard { 408 | "cardTitle"?: string; 409 | "center"?: boolean; 410 | "content"?: string; 411 | "imageUrl"?: string; 412 | "isHighlighted"?: boolean; 413 | "onCardClicked"?: (event: AirCardCustomEvent) => void; 414 | "size"?: 'small' | 'medium' | 'large' | 'auto'; 415 | } 416 | interface AirChat { 417 | } 418 | interface AirIcon { 419 | "color"?: string; 420 | "iconSet"?: IconSet; 421 | "name"?: string; 422 | "size"?: string; 423 | } 424 | interface AirInput { 425 | "autofocus"?: boolean; 426 | "customClass"?: string; 427 | "customStyle"?: { [key: string]: string }; 428 | "disabled"?: boolean; 429 | "error"?: boolean; 430 | "errorMessage"?: string; 431 | "label"?: string; 432 | "maxLength"?: number; 433 | "minLength"?: number; 434 | "name"?: string; 435 | "pattern"?: string; 436 | "placeholder"?: string; 437 | "required"?: boolean; 438 | "type"?: string; 439 | "value"?: string; 440 | } 441 | interface AirPreviewer { 442 | "customLink"?: string; 443 | "size"?: 'small' | 'medium' | 'large' | 'auto'; 444 | } 445 | interface AirRating { 446 | /** 447 | * 自定义样式(CSS键值对对象) 448 | */ 449 | "customStyle"?: { [key: string]: string }; 450 | /** 451 | * 未选中图标 452 | */ 453 | "emptyIcon"?: string; 454 | /** 455 | * 选中图标(直接使用air-icon的name) 456 | */ 457 | "filledIcon"?: string; 458 | "iconSet"?: IconSet; 459 | /** 460 | * 当前评分等级 (0~max) 461 | */ 462 | "level"?: number; 463 | /** 464 | * 最大星数(1-10) 465 | */ 466 | "max"?: number; 467 | /** 468 | * 评分变化事件 469 | */ 470 | "onRatingChange"?: (event: AirRatingCustomEvent) => void; 471 | } 472 | interface AirTag { 473 | "closable"?: boolean; 474 | "color"?: 'gray' | 'blue' | 'green' | 'yellow' | 'red'; 475 | "onAirClose"?: (event: AirTagCustomEvent) => void; 476 | "rounded"?: 'none' | 'md' | 'full'; 477 | "size"?: 'sm' | 'md'; 478 | } 479 | interface AirRating { 480 | /** 481 | * 自定义样式(CSS键值对对象) 482 | */ 483 | "customStyle"?: { [key: string]: string }; 484 | /** 485 | * 未选中图标 486 | */ 487 | "emptyIcon"?: string; 488 | /** 489 | * 选中图标(直接使用air-icon的name) 490 | */ 491 | "filledIcon"?: string; 492 | "iconSet"?: IconSet; 493 | /** 494 | * 当前评分等级 (0~max) 495 | */ 496 | "level"?: number; 497 | /** 498 | * 最大星数(1-10) 499 | */ 500 | "max"?: number; 501 | /** 502 | * 评分变化事件 503 | */ 504 | "onRatingChange"?: (event: AirRatingCustomEvent) => void; 505 | } 506 | interface AirTag { 507 | "closable"?: boolean; 508 | "color"?: 'gray' | 'blue' | 'green' | 'yellow' | 'red'; 509 | "onAirClose"?: (event: AirTagCustomEvent) => void; 510 | "rounded"?: 'none' | 'md' | 'full'; 511 | "size"?: 'sm' | 'md'; 512 | } 513 | /** 514 | * @name AirText 515 | * @description Typography for rendering headlines, paragraphs, captions, and body text with various style options. 516 | * @category General 517 | * @example Heading 518 | */ 519 | interface AirText { 520 | "color"?: | 'primary' 521 | | 'secondary' 522 | | 'tertiary' 523 | | 'helper' 524 | | 'error' 525 | | 'on-color' 526 | | 'inverse'; 527 | "configAria"?: any; 528 | "expressive"?: boolean; 529 | "headingLevel"?: 1 | 2 | 3 | 4 | 5 | 6; 530 | "headingSize"?: 1 | 2 | 3 | 4 | 5 | 6 | 7; 531 | "inline"?: boolean; 532 | "type"?: | 'code' 533 | | 'helper-text' 534 | | 'label' 535 | | 'legal' 536 | | 'heading' 537 | | 'body' 538 | | 'body-compact' 539 | | 'body-large' 540 | | 'body-emphasis' 541 | | 'fluid-heading'; 542 | } 543 | interface AirUserProfile { 544 | "avatarSrc"?: string; 545 | "editable"?: boolean; 546 | "userBio"?: string; 547 | "userName"?: string; 548 | } 549 | interface IntrinsicElements { 550 | "air-avatar": AirAvatar; 551 | "air-button": AirButton; 552 | "air-button-group": AirButtonGroup; 553 | "air-card": AirCard; 554 | "air-chat": AirChat; 555 | "air-icon": AirIcon; 556 | "air-input": AirInput; 557 | "air-previewer": AirPreviewer; 558 | "air-rating": AirRating; 559 | "air-tag": AirTag; 560 | "air-text": AirText; 561 | "air-user-profile": AirUserProfile; 562 | } 563 | } 564 | export { LocalJSX as JSX }; 565 | declare module "@stencil/core" { 566 | export namespace JSX { 567 | interface IntrinsicElements { 568 | /** 569 | * @name AirAvatar 570 | * @description 显示用户头像,支持头像图片、姓名首字母或默认样式,支持圆形和方形。 571 | * @category Data Display 572 | * @example 573 | */ 574 | "air-avatar": LocalJSX.AirAvatar & JSXBase.HTMLAttributes; 575 | "air-button": LocalJSX.AirButton & JSXBase.HTMLAttributes; 576 | "air-button-group": LocalJSX.AirButtonGroup & JSXBase.HTMLAttributes; 577 | "air-card": LocalJSX.AirCard & JSXBase.HTMLAttributes; 578 | "air-chat": LocalJSX.AirChat & JSXBase.HTMLAttributes; 579 | "air-icon": LocalJSX.AirIcon & JSXBase.HTMLAttributes; 580 | "air-input": LocalJSX.AirInput & JSXBase.HTMLAttributes; 581 | "air-previewer": LocalJSX.AirPreviewer & JSXBase.HTMLAttributes; 582 | "air-rating": LocalJSX.AirRating & JSXBase.HTMLAttributes; 583 | "air-tag": LocalJSX.AirTag & JSXBase.HTMLAttributes; 584 | /** 585 | * @name AirText 586 | * @description Typography for rendering headlines, paragraphs, captions, and body text with various style options. 587 | * @category General 588 | * @example Heading 589 | */ 590 | "air-text": LocalJSX.AirText & JSXBase.HTMLAttributes; 591 | "air-user-profile": LocalJSX.AirUserProfile & JSXBase.HTMLAttributes; 592 | } 593 | } 594 | } 595 | -------------------------------------------------------------------------------- /src/components/avatar/avatar.md: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 |
11 |

无头像显示首字母

12 | 18 |
19 | 20 |
21 |

无头像显示首字母

22 | -------------------------------------------------------------------------------- /src/components/avatar/avatar.scss: -------------------------------------------------------------------------------- 1 | @use '../../styles/designToken.css' as *; 2 | 3 | /* Avatar Styles */ 4 | .air-avatar-container { 5 | @apply flex justify-center items-center overflow-hidden; 6 | } 7 | 8 | .air-avatar-image { 9 | @apply w-full h-full object-cover; 10 | } 11 | 12 | .air-avatar-initials { 13 | @apply text-xl font-bold; 14 | } 15 | -------------------------------------------------------------------------------- /src/components/avatar/avatar.tsx: -------------------------------------------------------------------------------- 1 | import { Component, Prop, h, Host } from '@stencil/core'; 2 | 3 | /** 4 | * @name AirAvatar 5 | * @description 显示用户头像,支持头像图片、姓名首字母或默认样式,支持圆形和方形。 6 | * @category Data Display 7 | * @example 8 | */ 9 | @Component({ 10 | tag: 'air-avatar', 11 | styleUrl: 'avatar.scss', 12 | shadow: true, 13 | }) 14 | export class AirAvatar { 15 | /** 16 | * 头像尺寸,支持 `small`、`medium`、`large` 或自定义值(例如:`5rem`)。 17 | */ 18 | @Prop() size: 'small' | 'medium' | 'large' | string = 'medium'; 19 | 20 | /** 21 | * 用户姓名,用于生成首字母缩写。 22 | */ 23 | @Prop() name: string = ''; 24 | 25 | /** 26 | * 用户头像图片的 URL 地址。 27 | */ 28 | @Prop() src: string = ''; 29 | 30 | /** 31 | * 头像形状,支持 'circle' 或 'square'。 32 | */ 33 | @Prop() shape: 'circle' | 'square' = 'circle'; 34 | 35 | /** 36 | * 边框样式,可自定义。 37 | */ 38 | @Prop() border: string = ''; 39 | 40 | /** 41 | * 获取姓名首字母缩写。 42 | * 如果姓名不规范(如只有一个名字),会返回第一个字母。 43 | */ 44 | private getInitials(): string { 45 | const nameParts = this.name.split(' '); 46 | const firstName = nameParts[0]?.charAt(0).toUpperCase() || ''; 47 | const lastName = nameParts[1]?.charAt(0).toUpperCase() || ''; 48 | return `${firstName}${lastName}` || '?'; // 如果无法获取首字母,默认显示问号 49 | } 50 | 51 | /** 52 | * 根据 `size` 属性返回相应的 Tailwind 类名。 53 | */ 54 | private getSizeClass(): string { 55 | switch (this.size) { 56 | case 'small': 57 | return 'air-avatar-size-small'; 58 | case 'medium': 59 | return 'air-avatar-size-medium'; 60 | case 'large': 61 | return 'air-avatar-size-large'; 62 | default: 63 | return ''; // 如果是自定义尺寸,使用内联样式 64 | } 65 | } 66 | 67 | render() { 68 | const containerClasses = [ 69 | 'air-avatar-container', 70 | this.getSizeClass(), 71 | this.shape === 'circle' ? 'air-rounded-full' : 'air-rounded-none', 72 | this.src 73 | ? '' 74 | : 'air-bg-neutral air-text-center air-font-bold air-items-center air-text-large', 75 | ].join(' '); 76 | 77 | return ( 78 | 79 |
93 | {this.src ? ( 94 | {this.name} 95 | ) : ( 96 |
{this.getInitials()}
97 | )} 98 |
99 |
100 | ); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/components/avatar/readme.md: -------------------------------------------------------------------------------- 1 | # air-avatar 2 | 3 | 4 | 5 | 6 | 7 | 8 | ## Properties 9 | 10 | | Property | Attribute | Description | Type | Default | 11 | | -------- | --------- | -------------------------------------------------- | ---------------------- | ---------- | 12 | | `border` | `border` | 边框样式,可自定义。 | `string` | `''` | 13 | | `name` | `name` | 用户姓名,用于生成首字母缩写。 | `string` | `''` | 14 | | `shape` | `shape` | 头像形状,支持 'circle' 或 'square'。 | `"circle" \| "square"` | `'circle'` | 15 | | `size` | `size` | 头像尺寸,支持 `small`、`medium`、`large` 或自定义值(例如:`5rem`)。 | `string` | `'medium'` | 16 | | `src` | `src` | 用户头像图片的 URL 地址。 | `string` | `''` | 17 | 18 | 19 | ## Dependencies 20 | 21 | ### Used by 22 | 23 | - [air-chat](../chat) 24 | - [air-user-profile](../user-profile) 25 | 26 | ### Graph 27 | ```mermaid 28 | graph TD; 29 | air-chat --> air-avatar 30 | air-user-profile --> air-avatar 31 | style air-avatar fill:#f9f,stroke:#333,stroke-width:4px 32 | ``` 33 | 34 | ---------------------------------------------- 35 | 36 | *Built with [StencilJS](https://stenciljs.com/)* 37 | -------------------------------------------------------------------------------- /src/components/button-group/button-group.css: -------------------------------------------------------------------------------- 1 | .air-button-group > .button { 2 | margin: var(--spacing, 5px); 3 | } 4 | 5 | @media (max-width: 640px) { 6 | .btn-group:not([data-direction="horizontal"]) { 7 | flex-direction: column; 8 | } 9 | } 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/components/button-group/button-group.spec.ts: -------------------------------------------------------------------------------- 1 | import { newSpecPage } from "@stencil/core/testing"; 2 | import { ButtonGroup } from "./button-group"; 3 | 4 | describe("air-button-group", () => { 5 | it("renders default button group", async () => { 6 | const page = await newSpecPage({ 7 | components: [ButtonGroup], 8 | html: ``, 9 | }); 10 | const group = page.root.shadowRoot.querySelector("div"); 11 | expect(group).toHaveAttribute("role"); 12 | expect(group).toHaveClass("flex"); 13 | expect(group).toHaveClass("flex-row"); 14 | expect(group.style.gap).toBe("5px"); 15 | }); 16 | 17 | it("applies direction prop correctly", async () => { 18 | const page = await newSpecPage({ 19 | components: [ButtonGroup], 20 | html: ``, 21 | }); 22 | const group = page.root.shadowRoot.querySelector("div"); 23 | expect(group).toHaveClass("flex-col"); 24 | expect(group).not.toHaveClass("flex-row"); 25 | }); 26 | 27 | it("applies spacing prop correctly", async () => { 28 | const page = await newSpecPage({ 29 | components: [ButtonGroup], 30 | html: ``, 31 | }); 32 | const group = page.root.shadowRoot.querySelector("div"); 33 | expect(group.style.gap).toBe("10px"); 34 | }); 35 | 36 | it("applies customStyles prop correctly", async () => { 37 | const customStyles = { border: "1px solid red", padding: "10px" }; 38 | const page = await newSpecPage({ 39 | components: [ButtonGroup], 40 | html: ``, 41 | }); 42 | page.root.customStyles = customStyles; 43 | await page.waitForChanges(); 44 | const group = page.root.shadowRoot.querySelector("div"); 45 | expect(group.style.border).toBe("1px solid red"); 46 | expect(group.style.padding).toBe("10px"); 47 | }); 48 | 49 | it("renders slot content correctly", async () => { 50 | const page = await newSpecPage({ 51 | components: [ButtonGroup], 52 | html: ``, 53 | }); 54 | const slotContent = page.root.shadowRoot.querySelector("slot"); 55 | expect(slotContent).not.toBeNull(); 56 | expect(page.root.innerHTML).toContain("Button 1"); 57 | expect(page.root.innerHTML).toContain("Button 2"); 58 | }); 59 | 60 | it("applies group prop correctly", async () => { 61 | const page = await newSpecPage({ 62 | components: [ButtonGroup], 63 | html: ``, 64 | }); 65 | const group = page.root.shadowRoot.querySelector("div"); 66 | expect(group.getAttribute("data-group")).toBe("test-group"); 67 | }); 68 | 69 | it("applies correct accessibility attributes", async () => { 70 | const page = await newSpecPage({ 71 | components: [ButtonGroup], 72 | html: ``, 73 | }); 74 | const group = page.root.shadowRoot.querySelector("div"); 75 | expect(group.getAttribute("role")).toBe("group"); 76 | expect(group.getAttribute("aria-label")).toBe("Button Group"); 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /src/components/button-group/button-group.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | ComponentInterface, 4 | h, 5 | Host, 6 | Prop, 7 | State, 8 | Element, 9 | } from "@stencil/core";/** 10 | * Button Group component for grouping multiple buttons together 11 | * with optional customization of layout and spacing. 12 | */ 13 | @Component({ 14 | tag: "air-button-group", 15 | styleUrl: "button-group.css", 16 | shadow: true, 17 | }) 18 | export class ButtonGroup implements ComponentInterface { 19 | @Element() nativeElement!: HTMLElement; 20 | 21 | @Prop() direction: "horizontal" | "vertical" = "horizontal"; // Direction of button group layout 22 | @Prop() spacing: string = "5px"; // Spacing between buttons 23 | @Prop() customStyles: { [key: string]: string } = {}; // Custom CSS class 24 | @Prop() group: string = ""; // Custom group name for targeting with CSS 25 | 26 | @State() buttonCount: number = 0; // Track number of buttons in the group 27 | 28 | // Slot change handler to count the buttons 29 | private onSlotChange() { 30 | const slot = this.nativeElement.shadowRoot?.querySelector("slot"); 31 | if (slot) { 32 | const assignedNodes = slot.assignedNodes(); 33 | this.buttonCount = assignedNodes ? assignedNodes.length : 0; 34 | } 35 | } 36 | 37 | render() { 38 | return ( 39 | 40 |
47 | 48 |
49 |
50 | ); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/components/button-group/readme.md: -------------------------------------------------------------------------------- 1 | # air-button-group 2 | 3 | 4 | 5 | 6 | 7 | 8 | ## Properties 9 | 10 | | Property | Attribute | Description | Type | Default | 11 | | -------------- | ----------- | ----------- | ---------------------------- | -------------- | 12 | | `customStyles` | -- | | `{ [key: string]: string; }` | `{}` | 13 | | `direction` | `direction` | | `"horizontal" \| "vertical"` | `"horizontal"` | 14 | | `group` | `group` | | `string` | `""` | 15 | | `spacing` | `spacing` | | `string` | `"5px"` | 16 | 17 | 18 | ---------------------------------------------- 19 | 20 | *Built with [StencilJS](https://stenciljs.com/)* 21 | -------------------------------------------------------------------------------- /src/components/button-group/showCase.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 按钮 1 5 | 按钮 2 6 | 按钮 3 7 | 8 | 9 | 10 | 垂直按钮 1 11 | 垂直按钮 2 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/components/button/__snapshots__/button.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`air-button renders default air-button 1`] = ` 4 | 5 | 10 | 11 | `; 12 | -------------------------------------------------------------------------------- /src/components/button/button.css: -------------------------------------------------------------------------------- 1 | .native-button { 2 | @apply relative inline-flex items-center justify-center font-medium transition-all duration-200 ease-in-out focus:outline-none; 3 | @apply px-4 py-2 text-center overflow-hidden gap-2; 4 | min-height: 2.5rem; /* 最小高度 */ 5 | min-width: 6rem; /* 最小宽度 */ 6 | border-radius: 0.375rem; /* 默认圆角 */ 7 | width: auto; /* 自动宽度 */ 8 | height: 100%; /* 固定高度 */ 9 | } 10 | 11 | /* 按钮尺寸 */ 12 | .native-button.size-small { 13 | @apply inline-block text-center px-2 py-1 text-sm min-w-[70px] h-[30px]; 14 | } 15 | 16 | .native-button.size-medium { 17 | @apply inline-block text-center px-3 py-1.5 text-base min-w-[90px] h-[40px]; 18 | } 19 | 20 | .native-button.size-large { 21 | @apply inline-block text-center px-4 py-2 text-lg min-w-[120px] h-[50px]; 22 | } 23 | 24 | /* 按钮颜色 */ 25 | .native-button.state-primary { 26 | @apply text-white bg-blue-500 border-blue-500; 27 | } 28 | .native-button.state-primary:hover { 29 | @apply text-white bg-blue-600 border-blue-500; 30 | } 31 | .native-button.state-success { 32 | @apply text-white bg-green-500 border-green-500; 33 | } 34 | .native-button.state-success:hover { 35 | @apply text-white bg-green-600 border-green-500; 36 | } 37 | .native-button.state-info { 38 | @apply text-white bg-teal-500 border-teal-500; 39 | } 40 | .native-button.state-info:hover { 41 | @apply text-white bg-teal-600 border-teal-500; 42 | } 43 | .native-button.state-warning { 44 | @apply text-black bg-yellow-600 border-yellow-500; 45 | } 46 | .native-button.state-warning:hover { 47 | @apply text-white bg-yellow-700 border-yellow-500; 48 | } 49 | .native-button.state-danger { 50 | @apply text-white bg-red-500 border-red-500; 51 | } 52 | .native-button.state-danger:hover { 53 | @apply text-white bg-red-700 border-red-500; 54 | } 55 | .native-button.state-ghost { 56 | @apply text-gray-700 bg-transparent border-gray-500; 57 | } 58 | .native-button.state-solid { 59 | @apply border border-solid border-slate-500 bg-slate-300; 60 | } 61 | .native-button.state-outline { 62 | @apply border border-slate-500 bg-transparent text-gray-700; 63 | } 64 | 65 | .native-button__icon, 66 | .native-button__suffix-icon { 67 | @apply flex-shrink-0; /* 防止图标缩放 */ 68 | } 69 | 70 | .native-button__icon, 71 | .native-button__loading-icon, 72 | .native-button__suffix-icon { 73 | @apply inline-block; /* 图标与文本对齐 */ 74 | } 75 | 76 | .native-button__text { 77 | @apply flex-1 text-center min-w-0; /* 文本区域自适应压缩 */ 78 | } 79 | 80 | .native-button__loading-icon { 81 | @apply w-6 h-6 border-4 border-t-transparent border-solid rounded-full animate-spin; /* 加载圆环 */ 82 | border-color: currentColor transparent transparent transparent; 83 | animation-duration: 1s; /* 动画时长 */ 84 | } 85 | 86 | .native-button.selected { 87 | @apply border-2 border-dashed border-blue-500 shadow-md; /* 增加更醒目的蓝色虚线边框 */ 88 | transition: border-color 0.2s ease-in-out, box-shadow 0.2s ease-in-out; /* 边框颜色和阴影的过渡效果 */ 89 | } 90 | .native-button.selected:hover { 91 | @apply border-2 border-dashed border-blue-500 shadow-md; /* 增加更醒目的蓝色虚线边框 */ 92 | transition: border-color 0.2s ease-in-out, box-shadow 0.2s ease-in-out; /* 边框颜色和阴影的过渡效果 */ 93 | } 94 | 95 | .native-button:disabled { 96 | @apply bg-slate-300 text-gray-500 cursor-not-allowed; /* 禁用状态 */ 97 | } 98 | .native-button:disabled:hover { 99 | @apply bg-slate-300 text-gray-500 cursor-not-allowed; /* 禁用状态 */ 100 | } 101 | /* 加载状态 */ 102 | .loading { 103 | @apply cursor-wait pointer-events-none; /* 显示加载时禁用点击 */ 104 | } 105 | -------------------------------------------------------------------------------- /src/components/button/button.spec.ts: -------------------------------------------------------------------------------- 1 | import { newSpecPage } from "@stencil/core/testing"; 2 | import { AirButton } from "./button"; 3 | 4 | describe("air-button", () => { 5 | it("renders default air-button", async () => { 6 | const page = await newSpecPage({ 7 | components: [AirButton], 8 | html: ``, 9 | }); 10 | expect(page.root).toMatchSnapshot(); 11 | }); 12 | 13 | it("applies size prop correctly", async () => { 14 | const page = await newSpecPage({ 15 | components: [AirButton], 16 | html: ``, 17 | }); 18 | const button = page.root.shadowRoot.querySelector("button"); 19 | expect(button.classList.contains("size-small")).toBeTruthy(); 20 | }); 21 | 22 | it("applies variant prop correctly", async () => { 23 | const page = await newSpecPage({ 24 | components: [AirButton], 25 | html: ``, 26 | }); 27 | const button = page.root.shadowRoot.querySelector("button"); 28 | expect(button.classList.contains("variant-outline")).toBeTruthy(); 29 | }); 30 | 31 | it("applies color prop correctly", async () => { 32 | const page = await newSpecPage({ 33 | components: [AirButton], 34 | html: ``, 35 | }); 36 | const button = page.root.shadowRoot.querySelector("button"); 37 | expect(button.classList.contains("color-primary")).toBeTruthy(); 38 | }); 39 | 40 | it("disables the button when disabled prop is true", async () => { 41 | const page = await newSpecPage({ 42 | components: [AirButton], 43 | html: ``, 44 | }); 45 | const button = page.root.shadowRoot.querySelector("button"); 46 | expect(button.hasAttribute("disabled")).toBeTruthy(); 47 | expect(button.classList.contains("disabled")).toBeTruthy(); 48 | }); 49 | 50 | it("shows loading icon when loading is true", async () => { 51 | const page = await newSpecPage({ 52 | components: [AirButton], 53 | html: ``, 54 | }); 55 | const button = page.root.shadowRoot.querySelector("button"); 56 | expect(button.getAttribute("aria-busy")).toBe("true"); 57 | const loadingIcon = button.querySelector(".native-button__loading-icon"); 58 | expect(loadingIcon).not.toBeNull(); 59 | }); 60 | 61 | it("renders icon and suffixIcon correctly", async () => { 62 | const page = await newSpecPage({ 63 | components: [AirButton], 64 | html: ``, 65 | }); 66 | const icon = page.root.shadowRoot.querySelector(".native-button__icon"); 67 | const suffixIcon = page.root.shadowRoot.querySelector(".native-button__suffix-icon"); 68 | expect(icon.textContent).toBe("✔"); 69 | expect(suffixIcon.textContent).toBe("❌"); 70 | }); 71 | 72 | it("handles slot content correctly", async () => { 73 | const page = await newSpecPage({ 74 | components: [AirButton], 75 | html: `Click Me`, 76 | }); 77 | expect(page.root.shadowRoot.querySelector("slot")).not.toBeNull(); 78 | expect(page.root.textContent).toContain("Click Me"); 79 | }); 80 | 81 | it("updates aria-label when slot is empty", async () => { 82 | const page = await newSpecPage({ 83 | components: [AirButton], 84 | html: ``, 85 | }); 86 | const button = page.root.shadowRoot.querySelector("button"); 87 | expect(button.getAttribute("aria-label")).toBe("Button"); 88 | }); 89 | 90 | it('emits buttonClick event on click', async () => { 91 | const page = await newSpecPage({ 92 | components: [AirButton], 93 | html: `Click Me`, 94 | }); 95 | 96 | const button = page.root.shadowRoot.querySelector('button'); 97 | const buttonClickSpy = jest.fn(); 98 | 99 | // 监听自定义事件 100 | page.root.addEventListener('buttonClick', buttonClickSpy); 101 | 102 | // 模拟点击 103 | button.click(); 104 | 105 | // 验证事件被触发 106 | expect(buttonClickSpy).toHaveBeenCalled(); 107 | }); 108 | 109 | it('does not emit buttonClick event when disabled', async () => { 110 | const page = await newSpecPage({ 111 | components: [AirButton], 112 | html: `Disabled Button`, 113 | }); 114 | 115 | const button = page.root.shadowRoot.querySelector('button'); 116 | const buttonClickSpy = jest.fn(); 117 | 118 | // 监听自定义事件 119 | page.root.addEventListener('buttonClick', buttonClickSpy); 120 | 121 | // 模拟点击 122 | button.click(); 123 | 124 | // 验证事件未触发 125 | expect(buttonClickSpy).not.toHaveBeenCalled(); 126 | }); 127 | 128 | it('does not emit buttonClick event when loading', async () => { 129 | const page = await newSpecPage({ 130 | components: [AirButton], 131 | html: `Loading Button`, 132 | }); 133 | 134 | const button = page.root.shadowRoot.querySelector('button'); 135 | const buttonClickSpy = jest.fn(); 136 | 137 | // 监听自定义事件 138 | page.root.addEventListener('buttonClick', buttonClickSpy); 139 | 140 | // 模拟点击 141 | button.click(); 142 | 143 | // 验证事件未触发 144 | expect(buttonClickSpy).not.toHaveBeenCalled(); 145 | }); 146 | }); 147 | 148 | -------------------------------------------------------------------------------- /src/components/button/button.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | h, 4 | Host, 5 | Prop, 6 | State, 7 | Event, 8 | EventEmitter, 9 | } from '@stencil/core'; 10 | 11 | @Component({ 12 | tag: 'air-button', 13 | styleUrl: 'button.css', 14 | shadow: false, 15 | }) 16 | export class AirButton { 17 | @Prop() type: 'button' | 'submit' | 'reset' = 'button'; 18 | @Prop() size: 'small' | 'medium' | 'large' = 'medium'; 19 | @Prop() state: 20 | | 'primary' 21 | | 'success' 22 | | 'info' 23 | | 'warning' 24 | | 'danger' 25 | | 'ghost' 26 | | 'outline' 27 | | 'solid' = 'primary'; 28 | @Prop() icon: string = ''; 29 | @Prop() suffixIcon: string = ''; 30 | @Prop() disabled: boolean = false; 31 | @Prop() loading: boolean = false; // 加载状态 32 | @Prop() selected: boolean = false; // 选中状态 33 | 34 | @State() hasSlotContent: boolean = false; // 插槽内容检测 35 | 36 | @Event() buttonClick: EventEmitter<{ event: MouseEvent; selected: boolean }>; // 定义事件 37 | 38 | private nativeElement!: HTMLButtonElement; 39 | // 检查按钮插槽内容 40 | private computeSlotHasContent() { 41 | const slot = this.nativeElement.shadowRoot?.querySelector('slot'); 42 | if (slot) { 43 | const assignedNodes = slot.assignedNodes(); 44 | this.hasSlotContent = 45 | assignedNodes.length > 0 || slot.textContent?.trim().length > 0; 46 | } 47 | } 48 | private handleClick = (event: MouseEvent) => { 49 | if (this.disabled || this.loading) { 50 | event.preventDefault(); 51 | return; 52 | } 53 | 54 | // 触发外部注入的逻辑 55 | this.buttonClick.emit({ event, selected: this.selected }); 56 | }; 57 | 58 | render() { 59 | const ariaLabel = this.hasSlotContent ? null : 'Button'; // 适配无内容时的 aria-label 60 | 61 | return ( 62 | 63 | 89 | 90 | ); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/components/button/readme.md: -------------------------------------------------------------------------------- 1 | # air-button 2 | 3 | 4 | 5 | 6 | 7 | 8 | ## Properties 9 | 10 | | Property | Attribute | Description | Type | Default | 11 | | ------------ | ------------- | ----------- | ---------------------------------------------------------------------------------------------- | ----------- | 12 | | `disabled` | `disabled` | | `boolean` | `false` | 13 | | `icon` | `icon` | | `string` | `''` | 14 | | `loading` | `loading` | | `boolean` | `false` | 15 | | `selected` | `selected` | | `boolean` | `false` | 16 | | `size` | `size` | | `"large" \| "medium" \| "small"` | `'medium'` | 17 | | `state` | `state` | | `"danger" \| "ghost" \| "info" \| "outline" \| "primary" \| "solid" \| "success" \| "warning"` | `'primary'` | 18 | | `suffixIcon` | `suffix-icon` | | `string` | `''` | 19 | | `type` | `type` | | `"button" \| "reset" \| "submit"` | `'button'` | 20 | 21 | 22 | ## Events 23 | 24 | | Event | Description | Type | 25 | | ------------- | ----------- | -------------------------------------------------------- | 26 | | `buttonClick` | | `CustomEvent<{ event: MouseEvent; selected: boolean; }>` | 27 | 28 | 29 | ## Dependencies 30 | 31 | ### Used by 32 | 33 | - [air-chat](../chat) 34 | - [air-user-profile](../user-profile) 35 | 36 | ### Graph 37 | ```mermaid 38 | graph TD; 39 | air-chat --> air-button 40 | air-user-profile --> air-button 41 | style air-button fill:#f9f,stroke:#333,stroke-width:4px 42 | ``` 43 | 44 | ---------------------------------------------- 45 | 46 | *Built with [StencilJS](https://stenciljs.com/)* 47 | -------------------------------------------------------------------------------- /src/components/button/showCase.md: -------------------------------------------------------------------------------- 1 | 2 | 主按钮 3 | 成功按钮 4 | 信息按钮 5 | 警告按钮 6 | 危险按钮 7 | 幽灵按钮 8 | 9 | 10 | 实心按钮 11 | 描边按钮 12 | 文字按钮 13 | 霓虹按钮 14 | 默认按钮 15 | 16 | 17 | 图标按钮 18 | 后缀图标按钮 19 | 双图标按钮 20 | 21 | 22 | 加载中 23 | 禁用按钮 24 | 禁用图标按钮 25 | 选中按钮 26 | 选中图标按钮 27 | 28 | 29 | 小按钮 30 | 中按钮 31 | 大按钮 32 | -------------------------------------------------------------------------------- /src/components/card/card.css: -------------------------------------------------------------------------------- 1 | @use '../../styles/designToken.css' as *; 2 | 3 | .card { 4 | @apply rounded-lg shadow-lg overflow-hidden bg-white transition-all duration-300 ease-in-out; 5 | border: 2px solid transparent; /* 默认透明边框,避免闪烁 */ 6 | border-radius: 12px; /* 使边角圆润 */ 7 | } 8 | 9 | /* 当卡片被悬停时,边框变粗并变成蓝色 */ 10 | .card:hover { 11 | @apply transform scale-105; 12 | border: 2px solid #3b82f6; /* 悬停时显示蓝色边框 */ 13 | } 14 | 15 | /* 高亮卡片时,使用较厚的边框,并将边框颜色变为绿色 */ 16 | .card.highlighted { 17 | @apply border-4 border-green-500; /* 高亮时,使用较粗的绿色边框 */ 18 | } 19 | 20 | .card:active { 21 | transform: scale(0.98); 22 | transition: transform 0.1s ease-out; 23 | } 24 | 25 | /* 卡片图像 */ 26 | .card-image { 27 | @apply w-full h-48 object-cover rounded-t-lg; /* 上圆角 */ 28 | } 29 | 30 | /* 卡片标题 */ 31 | .card-title { 32 | @apply text-xl font-semibold text-gray-800; 33 | } 34 | 35 | /* 卡片内容 */ 36 | .card-content { 37 | @apply text-gray-600 text-base; 38 | } 39 | 40 | /* 卡片底部 */ 41 | .card-footer { 42 | @apply text-gray-600 text-center; 43 | } 44 | 45 | /* 插槽内容 */ 46 | slot[name='title']::slotted(*) { 47 | @apply text-lg font-bold text-gray-900; 48 | } 49 | 50 | slot[name='content']::slotted(*) { 51 | @apply text-base text-gray-700; 52 | } 53 | 54 | /* 加载状态占位符 */ 55 | .card .loading-placeholder { 56 | @apply w-full h-48 bg-gray-200 animate-pulse; 57 | } 58 | 59 | /* 卡片背景色 */ 60 | .card-background { 61 | @apply bg-gray-50; 62 | } 63 | 64 | .card.small { 65 | width: 200px; 66 | padding: 0.75rem; 67 | } 68 | 69 | .card.medium { 70 | width: 300px; 71 | padding: 1rem; 72 | } 73 | 74 | .card.large { 75 | width: 400px; 76 | padding: 1.25rem; 77 | } 78 | 79 | .card.auto { 80 | @apply w-full; 81 | } 82 | .card.center { 83 | @apply flex justify-center; 84 | } 85 | -------------------------------------------------------------------------------- /src/components/card/card.tsx: -------------------------------------------------------------------------------- 1 | import { Component, h, Prop, Event, EventEmitter } from '@stencil/core'; 2 | 3 | @Component({ 4 | tag: 'air-card', 5 | styleUrl: 'card.css', 6 | shadow: false, 7 | }) 8 | export class AirCard { 9 | @Prop() cardTitle: string = 'Card Title'; 10 | @Prop() content: string = 'This is the card content.'; 11 | @Prop() imageUrl: string = ''; 12 | @Prop() isHighlighted: boolean = false; 13 | @Prop() size: 'small' | 'medium' | 'large' | 'auto' = 'medium'; 14 | @Prop() center: boolean = false; 15 | @Event() cardClicked: EventEmitter; 16 | 17 | private handleClick() { 18 | this.cardClicked.emit(); 19 | } 20 | 21 | render() { 22 | return ( 23 |
this.handleClick()} 28 | > 29 | {this.imageUrl && ( 30 | Card image 31 | )} 32 | 33 |
34 |

35 | {this.cardTitle} 36 |

37 |

38 | {this.content} 39 |

40 |
41 | 44 |
45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/components/card/readme.md: -------------------------------------------------------------------------------- 1 | # air-card 2 | 3 | 4 | 5 | 6 | 7 | 8 | ## Properties 9 | 10 | | Property | Attribute | Description | Type | Default | 11 | | --------------- | ---------------- | ----------- | ------------------------------------------ | ----------------------------- | 12 | | `cardTitle` | `card-title` | | `string` | `'Card Title'` | 13 | | `center` | `center` | | `boolean` | `false` | 14 | | `content` | `content` | | `string` | `'This is the card content.'` | 15 | | `imageUrl` | `image-url` | | `string` | `''` | 16 | | `isHighlighted` | `is-highlighted` | | `boolean` | `false` | 17 | | `size` | `size` | | `"auto" \| "large" \| "medium" \| "small"` | `'medium'` | 18 | 19 | 20 | ## Events 21 | 22 | | Event | Description | Type | 23 | | ------------- | ----------- | ------------------- | 24 | | `cardClicked` | | `CustomEvent` | 25 | 26 | 27 | ---------------------------------------------- 28 | 29 | *Built with [StencilJS](https://stenciljs.com/)* 30 | -------------------------------------------------------------------------------- /src/components/card/showcase.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Custom Card Title 5 | 6 | 7 | This is custom content for the card. You can replace it with any custom HTML. 8 | 9 | 10 |
11 | 20 | Click Me 21 | 22 |
23 |
24 | 25 | 26 | 27 | Another Card Title 28 | Here is another custom content for a different card. 29 | 30 | 31 | 32 | 33 | 34 | 35 | Card Title 36 | 37 | 38 | This is a description of the card content. It can be anything, such as a brief introduction or message. 39 | 40 |
41 | Learn More 42 |
43 |
44 | 45 | 46 | 47 | Featured Card 48 | 49 | 50 | This card includes an image and some descriptive content. Click on it for more information. 51 | 52 |
53 | Learn More 54 |
55 |
56 | 57 | 58 | 59 | Responsive Card Title 60 | 61 | 62 | 63 | 64 | 65 | Terms and Conditions 66 | 67 | 68 | Please read these terms and conditions carefully before using our service. 69 | 70 |
71 | I Agree 72 |
73 |
74 | -------------------------------------------------------------------------------- /src/components/chat/chat.css: -------------------------------------------------------------------------------- 1 | /* ai-chat.css */ 2 | :host { 3 | --primary-color: #3b82f6; 4 | --hover-color: #2563eb; 5 | } 6 | 7 | button { 8 | background-color: var(--primary-color); 9 | } 10 | button:hover { 11 | background-color: var(--hover-color); 12 | } 13 | -------------------------------------------------------------------------------- /src/components/chat/chat.tsx: -------------------------------------------------------------------------------- 1 | import { Component, State, h } from '@stencil/core'; 2 | 3 | @Component({ 4 | tag: 'air-chat', 5 | styleUrl: 'chat.css', 6 | shadow: true, 7 | }) 8 | export class AiChat { 9 | @State() messages: Array<{ role: string; content: string }> = []; 10 | @State() userInput: string = ''; // 用来跟踪用户输入 11 | @State() error: string = ''; // 用来存储错误信息 12 | @State() isLoading: boolean = false; // 用来控制按钮的加载状态 13 | 14 | // 更新消息列表的帮助函数 15 | private updateMessages( 16 | newMessages: Array<{ role: string; content: string }> 17 | ) { 18 | this.messages = [...this.messages, ...newMessages]; 19 | } 20 | 21 | // 调用API 22 | private async fetchAIResponse(prompt: string) { 23 | try { 24 | const response = await fetch( 25 | 'https://api.siliconflow.cn/v1/chat/completions', 26 | { 27 | method: 'POST', 28 | headers: { 29 | Authorization: 30 | 'Bearer sk-lmztunjtjtuvgsnriqltrrohmeygybeemvweaxweqdfrmspk', // 替换为你的 API 密钥 31 | 'Content-Type': 'application/json', 32 | }, 33 | body: JSON.stringify({ 34 | model: 'deepseek-ai/DeepSeek-V3', 35 | messages: [...this.messages, { role: 'user', content: prompt }], 36 | temperature: 0.7, 37 | }), 38 | } 39 | ); 40 | 41 | const data = await response.json(); 42 | if (response.ok) { 43 | return data.choices[0].message.content; 44 | } else { 45 | throw new Error(data.error || 'API 请求失败'); 46 | } 47 | } catch (err) { 48 | this.error = `错误: ${err.message || '未知错误'}`; // 更详细的错误信息 49 | this.updateMessages([{ role: 'assistant', content: this.error }]); 50 | return null; // 返回 null, 表示没有有效的 AI 响应 51 | } 52 | } 53 | 54 | // 处理用户输入 55 | private handleSubmit = async (e: Event) => { 56 | e.preventDefault(); 57 | 58 | const userInput = this.userInput; 59 | 60 | // 在提交时设置 isLoading 为 true 61 | this.isLoading = true; 62 | 63 | // 添加用户输入和正在思考的提示 64 | this.updateMessages([ 65 | { role: 'user', content: userInput }, 66 | { role: 'assistant', content: '正在思考...' }, 67 | ]); 68 | 69 | // 获取AI的回复 70 | const aiResponse = await this.fetchAIResponse(userInput); 71 | 72 | // 如果 AI 返回了有效响应,更新消息列表 73 | if (aiResponse) { 74 | this.updateMessages([ 75 | { role: 'assistant', content: aiResponse }, // 添加 AI 的回答 76 | ]); 77 | } 78 | 79 | // 无论请求成功与否,清空输入框内容并更新 State 80 | this.userInput = ''; // 确保输入框被清空 81 | this.isLoading = false; // 更新按钮状态为可点击 82 | }; 83 | 84 | render() { 85 | return ( 86 |
87 |
88 | {/* 如果没有消息且没有错误,显示默认提示 */} 89 | {this.messages.length === 0 && !this.error && ( 90 |
91 | 我能为你提供什么? 92 |
93 | )} 94 | {/* 显示消息 */} 95 | {this.messages.map((msg, index) => ( 96 |
102 | {/* 头像的位置根据消息角色调整 */} 103 | {msg.role !== 'user' && ( 104 | 110 | )} 111 |
116 | {msg.content} 117 |
118 |
119 | ))} 120 |
121 | 122 |
123 | 127 | (this.userInput = (e.target as HTMLInputElement).value) 128 | } // 更新 State 129 | placeholder="输入消息..." 130 | > 131 | 137 | {this.isLoading ? '暂停' : '发送'} {/* 显示按钮文本 */} 138 | 139 |
140 |
141 | ); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/components/chat/readme.md: -------------------------------------------------------------------------------- 1 | # air-chat 2 | 3 | 4 | 5 | 6 | 7 | 8 | ## Dependencies 9 | 10 | ### Depends on 11 | 12 | - [air-text](../text) 13 | - [air-avatar](../avatar) 14 | - [air-input](../input) 15 | - [air-button](../button) 16 | 17 | ### Graph 18 | ```mermaid 19 | graph TD; 20 | air-chat --> air-text 21 | air-chat --> air-avatar 22 | air-chat --> air-input 23 | air-chat --> air-button 24 | style air-chat fill:#f9f,stroke:#333,stroke-width:4px 25 | ``` 26 | 27 | ---------------------------------------------- 28 | 29 | *Built with [StencilJS](https://stenciljs.com/)* 30 | -------------------------------------------------------------------------------- /src/components/icon/icon.css: -------------------------------------------------------------------------------- 1 | :host { 2 | display: inline-block; 3 | font-size: var(--icon-size, 1.5em); 4 | color: var(--icon-color, currentColor); 5 | } 6 | 7 | .air-icon { 8 | font-family: 'Material Icons', 'Font Awesome', 'Boxicons', 'Iconfont', 9 | 'IconPark', sans-serif; 10 | } 11 | -------------------------------------------------------------------------------- /src/components/icon/icon.tsx: -------------------------------------------------------------------------------- 1 | import { Component, Prop, h } from '@stencil/core'; 2 | 3 | // 图标库类型 4 | export type IconSet = 5 | | 'material-icons' 6 | | 'fas' 7 | | 'bx' 8 | | 'bxs' 9 | | 'iconfont' // 阿里巴巴图标库 10 | | 'iconpark'; // 腾讯图标库 11 | 12 | @Component({ 13 | tag: 'air-icon', 14 | styleUrl: 'icon.css', 15 | shadow: true, 16 | }) 17 | export class airIcon { 18 | @Prop() name: string; 19 | @Prop() color: string = 'currentColor'; 20 | @Prop() size: string = '1.5em'; 21 | @Prop() iconSet: IconSet = 'material-icons'; 22 | 23 | render() { 24 | let iconClass = ''; 25 | 26 | // Material Icons 处理 27 | if (this.iconSet === 'material-icons') { 28 | iconClass = `material-icons air-icon`; 29 | } 30 | // FontAwesome 处理 31 | else if (this.iconSet === 'fas') { 32 | iconClass = `fa-${this.name} ${this.iconSet} air-icon`; 33 | } 34 | // Boxicons 处理 35 | else if (this.iconSet === 'bx') { 36 | iconClass = `bx-${this.name} ${this.iconSet} air-icon`; 37 | } 38 | // Boxicons (Solid) 处理 39 | else if (this.iconSet === 'bxs') { 40 | iconClass = `bxs-${this.name} ${this.iconSet} air-icon`; 41 | } 42 | // Iconfont (阿里巴巴图标库) 处理 43 | else if (this.iconSet === 'iconfont') { 44 | iconClass = `iconfont icon-${this.name} air-icon`; 45 | } 46 | // IconPark (腾讯图标库) 处理 47 | else if (this.iconSet === 'iconpark') { 48 | iconClass = `iconpark-${this.name} air-icon`; 49 | } 50 | 51 | return ( 52 | 61 | ); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/components/icon/readme.md: -------------------------------------------------------------------------------- 1 | # air-icon 2 | 3 | 4 | 5 | 6 | 7 | 8 | ## Properties 9 | 10 | | Property | Attribute | Description | Type | Default | 11 | | --------- | ---------- | ----------- | ------------------------------------------------------------------------ | ------------------ | 12 | | `color` | `color` | | `string` | `'currentColor'` | 13 | | `iconSet` | `icon-set` | | `"bx" \| "bxs" \| "fas" \| "iconfont" \| "iconpark" \| "material-icons"` | `'material-icons'` | 14 | | `name` | `name` | | `string` | `undefined` | 15 | | `size` | `size` | | `string` | `'1.5em'` | 16 | 17 | 18 | ## Dependencies 19 | 20 | ### Used by 21 | 22 | - [air-previewer](../previwer) 23 | - [air-rating](../ratingStars) 24 | - [air-tag](../tag) 25 | 26 | ### Graph 27 | ```mermaid 28 | graph TD; 29 | air-previewer --> air-icon 30 | air-rating --> air-icon 31 | air-tag --> air-icon 32 | style air-icon fill:#f9f,stroke:#333,stroke-width:4px 33 | ``` 34 | 35 | ---------------------------------------------- 36 | 37 | *Built with [StencilJS](https://stenciljs.com/)* 38 | -------------------------------------------------------------------------------- /src/components/icon/usage.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Icon Display Example 7 | 8 | 9 | 10 | 11 | 12 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 |
32 |

Material Icons

33 |
34 | 35 | 36 |
37 | 38 |

Font Awesome

39 |
40 | 41 | 42 |
43 | 44 |

Boxicons

45 |
46 | 47 | 48 |
49 | 50 |

Iconfont (阿里巴巴)

51 |
52 | 53 | 54 |
55 | 56 |

IconPark (腾讯)

57 |
58 | 59 | 60 |
61 |
62 | 63 | 64 | -------------------------------------------------------------------------------- /src/components/input/input.md: -------------------------------------------------------------------------------- 1 | {/* 普通文本输入框 */} 2 | 7 | 8 | {/* 密码输入框 */} 9 | 17 | 18 | {/* 邮箱输入框 */} 19 | 27 | 28 | {/* 带有前后缀图标的输入框 */} 29 | 30 | 📞 31 | 32 | 33 | 34 | {/* 带有正则验证的输入框 */} 35 | 44 | 45 | {/* 禁用输入框 */} 46 | -------------------------------------------------------------------------------- /src/components/input/input.scss: -------------------------------------------------------------------------------- 1 | /* src/components/my-input/input.css */ 2 | @use "../../styles/designToken.css" as *; 3 | 4 | .input-wrapper { 5 | @apply flex flex-col; 6 | } 7 | 8 | .input-label { 9 | @apply text-sm font-medium text-gray-700 mb-1; 10 | } 11 | 12 | .input-container { 13 | @apply flex items-center border border-gray-300 rounded-md shadow-sm; 14 | transition: border-color 0.2s ease; 15 | @apply air-rounded-small; /* 应用圆角样式 */ 16 | } 17 | 18 | .input-container:focus-within { 19 | @apply border-blue-500; 20 | } 21 | 22 | slot[name='prefix'], 23 | slot[name='suffix'] { 24 | @apply px-2 text-gray-500; 25 | } 26 | 27 | .input-field { 28 | @apply flex-1 w-full px-4 py-2 text-sm outline-none; 29 | @apply air-rounded-small; /* 应用圆角样式 */ 30 | } 31 | 32 | .input-normal { 33 | @apply border-gray-300; 34 | } 35 | 36 | .input-error { 37 | @apply border-red-500; 38 | } 39 | 40 | .input-disabled { 41 | @apply bg-gray-100 cursor-not-allowed; 42 | } 43 | 44 | .input-error-message { 45 | @apply mt-2 text-sm text-red-600; 46 | } 47 | -------------------------------------------------------------------------------- /src/components/input/input.spec.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openwebstudio/AirUI/a0efa4ffb6c53fbdef320a23e2ba111e05a09834/src/components/input/input.spec.ts -------------------------------------------------------------------------------- /src/components/input/input.tsx: -------------------------------------------------------------------------------- 1 | import { Component, Prop, State, h, Element } from '@stencil/core'; 2 | 3 | @Component({ 4 | tag: 'air-input', 5 | styleUrl: 'input.scss', 6 | shadow: false, 7 | }) 8 | export class AirInput { 9 | @Element() el: HTMLElement; // 获取组件的根元素 10 | @Prop() label: string; 11 | @Prop() placeholder: string; 12 | @Prop() value: string; 13 | @Prop() name: string; 14 | @Prop() disabled: boolean = false; 15 | @Prop() error: boolean = false; 16 | @Prop() errorMessage: string = ''; 17 | @Prop() required: boolean = false; 18 | @Prop() pattern: string; 19 | @Prop() type: string = 'text'; 20 | @Prop() maxLength: number; 21 | @Prop() minLength: number; 22 | @Prop() autofocus: boolean = false; 23 | @Prop() customClass: string = ''; 24 | @Prop() customStyle: { [key: string]: string } = {}; 25 | 26 | @State() inputValue: string; 27 | 28 | handleInputChange(event: Event) { 29 | const input = event.target as HTMLInputElement; 30 | this.inputValue = input.value; 31 | 32 | if (this.pattern) { 33 | this.error = !new RegExp(this.pattern).test(input.value); 34 | } else { 35 | this.error = false; 36 | } 37 | } 38 | 39 | // 提供方法访问原生 input 元素 40 | getInputElement() { 41 | return this.el.shadowRoot?.querySelector('input'); 42 | } 43 | 44 | render() { 45 | return ( 46 |
47 | {this.label && ( 48 | 51 | )} 52 | 53 |
54 | 55 | this.handleInputChange(event)} 70 | style={this.customStyle} 71 | aria-label={this.label} 72 | aria-invalid={this.error ? 'true' : 'false'} 73 | aria-describedby={`${this.name}-error`} 74 | /> 75 | 76 |
77 | 78 | {this.error && this.errorMessage && ( 79 |

80 | {this.errorMessage} 81 |

82 | )} 83 |
84 | ); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/components/input/readme.md: -------------------------------------------------------------------------------- 1 | # air-input 2 | 3 | 4 | 5 | 6 | 7 | 8 | ## Properties 9 | 10 | | Property | Attribute | Description | Type | Default | 11 | | -------------- | --------------- | ----------- | ---------------------------- | ----------- | 12 | | `autofocus` | `autofocus` | | `boolean` | `false` | 13 | | `customClass` | `custom-class` | | `string` | `''` | 14 | | `customStyle` | -- | | `{ [key: string]: string; }` | `{}` | 15 | | `disabled` | `disabled` | | `boolean` | `false` | 16 | | `error` | `error` | | `boolean` | `false` | 17 | | `errorMessage` | `error-message` | | `string` | `''` | 18 | | `label` | `label` | | `string` | `undefined` | 19 | | `maxLength` | `max-length` | | `number` | `undefined` | 20 | | `minLength` | `min-length` | | `number` | `undefined` | 21 | | `name` | `name` | | `string` | `undefined` | 22 | | `pattern` | `pattern` | | `string` | `undefined` | 23 | | `placeholder` | `placeholder` | | `string` | `undefined` | 24 | | `required` | `required` | | `boolean` | `false` | 25 | | `type` | `type` | | `string` | `'text'` | 26 | | `value` | `value` | | `string` | `undefined` | 27 | 28 | 29 | ## Dependencies 30 | 31 | ### Used by 32 | 33 | - [air-chat](../chat) 34 | - [air-user-profile](../user-profile) 35 | 36 | ### Graph 37 | ```mermaid 38 | graph TD; 39 | air-chat --> air-input 40 | air-user-profile --> air-input 41 | style air-input fill:#f9f,stroke:#333,stroke-width:4px 42 | ``` 43 | 44 | ---------------------------------------------- 45 | 46 | *Built with [StencilJS](https://stenciljs.com/)* 47 | -------------------------------------------------------------------------------- /src/components/previwer/previewer.scss: -------------------------------------------------------------------------------- 1 | /* 预览容器 */ 2 | .preview-container { 3 | @apply border border-gray-300 rounded-xl shadow-lg p-6 flex flex-col space-y-6; 4 | backdrop-filter: blur(15px); /* 增强的背景模糊效果 */ 5 | background-color: rgba(255, 255, 255, 0.7); /* 使用透明背景 */ 6 | transform-origin: center; /* 确保从容器中心缩放 */ 7 | position: relative; 8 | overflow: hidden; /* 防止内容溢出容器 */ 9 | } 10 | 11 | /* Tailwind Config 或者使用 @apply 来设置这些样式 */ 12 | .preview-small { 13 | @apply w-64 h-64; /* 小的预览 */ 14 | } 15 | 16 | .preview-medium { 17 | @apply w-[400px] h-[400px]; /* 固定宽度和高度 */ 18 | } 19 | 20 | .preview-large { 21 | @apply w-[600px] h-[600px] /* 水平方向平铺,固定高度 */ 22 | } 23 | 24 | .preview-auto { 25 | @apply w-auto h-auto; /* 全屏预览 */ 26 | } 27 | 28 | /* 预览区域 */ 29 | .preview { 30 | @apply w-full h-full p-5 overflow-hidden flex-grow; 31 | display: flex; 32 | justify-content: center; 33 | align-items: center; 34 | } 35 | 36 | /* slot部分内容调整 */ 37 | .preview-container ::slotted(*) { 38 | @apply w-full h-full; 39 | display: block; 40 | transform: scale(0.8); /* 将slot内容缩小到原尺寸的一半 */ 41 | transform-origin: center; /* 确保从容器中心缩放 */ 42 | overflow-y: auto; /* 启用垂直滚动条 */ 43 | transition: transform 0.3s ease, opacity 0.3s ease; /* 增加过渡效果 */ 44 | } 45 | 46 | .preview-container ::slotted(*):hover { 47 | opacity: 0.8; /* 鼠标悬停时调整透明度 */ 48 | } 49 | 50 | /* 操作按钮区域 */ 51 | .actions { 52 | @apply flex items-center space-x-4 justify-end mt-4; 53 | } 54 | 55 | .action-btn { 56 | @apply w-12 h-12 flex items-center justify-center rounded-lg transition-all duration-300 57 | hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-blue-500 58 | transform hover:scale-110; /* 鼠标悬停时按钮增大效果 */ 59 | box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); /* 添加阴影 */ 60 | } 61 | 62 | .action-btn:hover { 63 | box-shadow: 0 6px 10px rgba(0, 0, 0, 0.2); /* 鼠标悬停时加强阴影 */ 64 | } 65 | 66 | /* 文本区域样式 */ 67 | -------------------------------------------------------------------------------- /src/components/previwer/previwer.tsx: -------------------------------------------------------------------------------- 1 | import { Component, Prop, State, h, Element, Watch } from '@stencil/core'; 2 | 3 | @Component({ 4 | tag: 'air-previewer', 5 | styleUrl: 'previewer.scss', 6 | shadow: true, 7 | }) 8 | export class AirPreviewer { 9 | @Prop() size: 'small' | 'medium' | 'large' | 'auto' = 'medium'; 10 | @Prop() customLink: string = 'https://github.com/SisyphusZheng/Components'; // 默认链接 11 | @State() code: string = ''; 12 | @State() showSource: boolean = false; 13 | @Element() el: HTMLElement; 14 | 15 | observer: MutationObserver; 16 | 17 | // 监听 customLink 的变化 18 | @Watch('customLink') 19 | watchCustomLink(newValue: string) { 20 | console.log('customLink changed to:', newValue); 21 | } 22 | 23 | // 验证链接是否有 24 | 25 | componentDidLoad() { 26 | const slot = this.el.shadowRoot.querySelector('slot'); 27 | 28 | if (slot) { 29 | const updateCode = () => { 30 | const nodes = slot.assignedNodes({ flatten: true }); 31 | const formattedHTML = nodes 32 | .map((node) => { 33 | if (node.nodeType === Node.ELEMENT_NODE) { 34 | return this.formatHTML((node as HTMLElement).outerHTML); 35 | } 36 | if (node.nodeType === Node.TEXT_NODE) { 37 | return node.textContent?.trim() || ''; 38 | } 39 | return ''; 40 | }) 41 | .filter((content) => content !== '') 42 | .join('\n'); 43 | this.code = formattedHTML.trim(); 44 | }; 45 | 46 | updateCode(); 47 | 48 | this.observer = new MutationObserver(updateCode); 49 | this.observer.observe(slot, { childList: true, subtree: true }); 50 | } 51 | } 52 | 53 | disconnectedCallback() { 54 | if (this.observer) { 55 | this.observer.disconnect(); 56 | } 57 | } 58 | 59 | handleInputChange(event: Event) { 60 | const textarea = event.target as HTMLTextAreaElement; 61 | this.code = textarea.value; 62 | 63 | const parser = new DOMParser(); 64 | const doc = parser.parseFromString(this.code, 'text/html'); 65 | const slotContent = doc.body.childNodes; 66 | 67 | const slot = this.el.shadowRoot.querySelector('slot'); 68 | if (slot) { 69 | const parentNode = this.el.querySelector('[slot]') || this.el; 70 | while (parentNode.firstChild) { 71 | parentNode.firstChild.remove(); 72 | } 73 | slotContent.forEach((node) => { 74 | parentNode.appendChild(node.cloneNode(true)); 75 | }); 76 | } 77 | } 78 | 79 | copyToClipboard() { 80 | if (this.code) { 81 | navigator.clipboard.writeText(this.code).then(() => { 82 | console.log('Code copied to clipboard!'); 83 | }); 84 | } 85 | } 86 | 87 | formatHTML(html: string): string { 88 | const formatted = html 89 | .replace(/>\n<') 90 | .replace(/( )*<(\/?)(\w+)([^>]*)>/g, (match, _, closing, tag, rest) => { 91 | const indent = closing ? ' ' : ''; 92 | console.log(match); // 在这里使用 match,例如打印完整的匹配内容 93 | return `${indent}<${closing}${tag}${rest}>`; 94 | }); 95 | return formatted; 96 | } 97 | 98 | render() { 99 | const sizeClass = 100 | { 101 | small: 'preview-small', 102 | medium: 'preview-medium', 103 | large: 'preview-large', 104 | auto: 'preview-auto', 105 | }[this.size] || 'preview-medium'; 106 | 107 | return ( 108 |
109 |
110 | 111 |
112 | 113 |
114 | 120 | 125 | 126 | 137 | 148 |
149 | 150 | {this.showSource && ( 151 |
152 |