├── .eslintignore ├── .eslintrc.js ├── .github └── ISSUE_TEMPLATE │ ├── bug-report.md │ └── feature-request-------.md ├── .gitignore ├── .husky ├── .gitignore └── commit-msg ├── .prettierrc ├── CHANGELOG.md ├── LICENSE ├── README.md ├── README.zh-CN.md ├── babel.config.js ├── commitlint.config.js ├── demo ├── .gitignore ├── index.html ├── package.json ├── src │ ├── components │ │ ├── DemoIndex.tsx │ │ ├── DemoPage.tsx │ │ ├── DemoSection.tsx │ │ ├── LangSwitcher.tsx │ │ └── index.ts │ ├── demo │ │ ├── Avatar.tsx │ │ ├── Bubble.tsx │ │ ├── Button.tsx │ │ ├── Card.tsx │ │ ├── Carousel.tsx │ │ ├── Chat.tsx │ │ ├── Checkbox.tsx │ │ ├── ComponentProvider.tsx │ │ ├── Confirm.tsx │ │ ├── Divider.tsx │ │ ├── Empty.tsx │ │ ├── ErrorBoundary.tsx │ │ ├── FileCard.tsx │ │ ├── Flex.tsx │ │ ├── Form.tsx │ │ ├── Goods.tsx │ │ ├── Icon.tsx │ │ ├── Image.tsx │ │ ├── InfiniteScroll.tsx │ │ ├── Input.tsx │ │ ├── List.tsx │ │ ├── Loading.tsx │ │ ├── MediaObject.tsx │ │ ├── MessageStatus.tsx │ │ ├── Modal.tsx │ │ ├── Navbar.tsx │ │ ├── Notice.tsx │ │ ├── OrdderSelector.tsx │ │ ├── Popup.tsx │ │ ├── Portal.tsx │ │ ├── Price.tsx │ │ ├── Progress.tsx │ │ ├── PullToRefresh.tsx │ │ ├── Quote.tsx │ │ ├── Radio.tsx │ │ ├── RateActions.tsx │ │ ├── RichText.tsx │ │ ├── ScrollGrid.tsx │ │ ├── ScrollView.tsx │ │ ├── Search.tsx │ │ ├── Select.tsx │ │ ├── Skeleton.tsx │ │ ├── Stepper.tsx │ │ ├── SystemMessage.tsx │ │ ├── Tabs.tsx │ │ ├── Tag.tsx │ │ ├── Text.tsx │ │ ├── Think.tsx │ │ ├── Time.tsx │ │ ├── Toast.tsx │ │ ├── Typing.tsx │ │ ├── TypingBubble.tsx │ │ ├── Video.tsx │ │ ├── VisuallyHidden.tsx │ │ └── index.ts │ ├── index.less │ ├── main.tsx │ ├── navConfig.ts │ ├── routerConfig.tsx │ ├── utils │ │ ├── index.ts │ │ └── toPascalCase.ts │ └── vite-env.d.ts ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts ├── jest.config.js ├── jest.setup.ts ├── package.json ├── postcss.config.js ├── rollup.config.js ├── src ├── components │ ├── Avatar │ │ ├── __tests__ │ │ │ └── index.test.tsx │ │ ├── index.tsx │ │ └── style.less │ ├── BackBottom │ │ ├── index.tsx │ │ └── style.less │ ├── Backdrop │ │ ├── __tests__ │ │ │ └── index.test.tsx │ │ ├── index.tsx │ │ └── style.less │ ├── Bubble │ │ ├── __tests__ │ │ │ └── index.test.tsx │ │ ├── index.tsx │ │ └── style.less │ ├── Button │ │ ├── __tests__ │ │ │ └── index.test.tsx │ │ ├── index.tsx │ │ └── style.less │ ├── Card │ │ ├── Card.tsx │ │ ├── CardActions.tsx │ │ ├── CardContent.tsx │ │ ├── CardHeader.tsx │ │ ├── CardMedia.tsx │ │ ├── CardText.tsx │ │ ├── CardTitle.tsx │ │ ├── __tests__ │ │ │ ├── actions.test.tsx │ │ │ ├── content.test.tsx │ │ │ ├── index.test.tsx │ │ │ ├── media.test.tsx │ │ │ ├── text.test.tsx │ │ │ └── title.test.tsx │ │ ├── index.ts │ │ └── style.less │ ├── Carousel │ │ ├── Item.tsx │ │ ├── index.tsx │ │ └── style.less │ ├── Chat │ │ ├── index.tsx │ │ └── style.less │ ├── Checkbox │ │ ├── Checkbox.tsx │ │ ├── CheckboxGroup.tsx │ │ ├── __tests__ │ │ │ ├── group.test.tsx │ │ │ └── index.test.tsx │ │ └── index.ts │ ├── ClickOutside │ │ ├── __tests__ │ │ │ └── index.test.tsx │ │ └── index.tsx │ ├── ComponentsProvider │ │ ├── ComponentsContext.ts │ │ ├── index.tsx │ │ ├── interface.ts │ │ └── useComponents.ts │ ├── Composer │ │ ├── AccessoryWrap.tsx │ │ ├── Action.tsx │ │ ├── ComposerInput.tsx │ │ ├── SendButton.tsx │ │ ├── ToolbarItem.tsx │ │ ├── index.tsx │ │ ├── riseInput.ts │ │ ├── style.less │ │ └── viewportTop.ts │ ├── ConfigProvider │ │ ├── __tests__ │ │ │ └── index.test.tsx │ │ ├── index.tsx │ │ └── locales │ │ │ ├── ar_EG.ts │ │ │ ├── en_US.ts │ │ │ ├── fr_FR.ts │ │ │ ├── index.ts │ │ │ └── zh_CN.ts │ ├── Divider │ │ ├── __tests__ │ │ │ └── index.test.tsx │ │ ├── index.tsx │ │ └── style.less │ ├── Empty │ │ ├── __tests__ │ │ │ └── index.test.tsx │ │ ├── index.tsx │ │ └── style.less │ ├── ErrorBoundary │ │ └── index.tsx │ ├── FileCard │ │ ├── __tests__ │ │ │ └── index.test.tsx │ │ ├── index.tsx │ │ └── style.less │ ├── Flex │ │ ├── Flex.tsx │ │ ├── FlexItem.tsx │ │ ├── __tests__ │ │ │ ├── index.test.tsx │ │ │ └── item.test.tsx │ │ ├── index.ts │ │ └── style.less │ ├── Form │ │ ├── Form.tsx │ │ ├── FormActions.tsx │ │ ├── FormItem.tsx │ │ ├── __tests__ │ │ │ ├── actions.test.tsx │ │ │ ├── index.test.tsx │ │ │ └── item.test.tsx │ │ ├── index.ts │ │ └── style.less │ ├── Goods │ │ ├── __tests__ │ │ │ └── index.test.tsx │ │ ├── index.tsx │ │ └── style.less │ ├── HelpText │ │ ├── __tests__ │ │ │ └── index.test.tsx │ │ ├── index.tsx │ │ └── style.less │ ├── Icon │ │ ├── __tests__ │ │ │ └── index.test.tsx │ │ ├── index.tsx │ │ └── style.less │ ├── IconButton │ │ ├── __tests__ │ │ │ └── index.test.tsx │ │ ├── index.tsx │ │ └── style.less │ ├── Image │ │ ├── __tests__ │ │ │ └── index.test.tsx │ │ ├── index.tsx │ │ └── style.less │ ├── InfiniteScroll │ │ ├── index.tsx │ │ └── style.less │ ├── Input │ │ ├── __tests__ │ │ │ └── index.test.tsx │ │ ├── index.tsx │ │ └── style.less │ ├── Label │ │ ├── __tests__ │ │ │ └── index.test.tsx │ │ ├── index.tsx │ │ └── style.less │ ├── LazyComponent │ │ ├── SuspenseWrap.tsx │ │ ├── index.tsx │ │ └── interface.ts │ ├── List │ │ ├── List.tsx │ │ ├── ListItem.tsx │ │ ├── __tests__ │ │ │ └── index.test.tsx │ │ ├── index.ts │ │ └── style.less │ ├── Loading │ │ ├── __tests__ │ │ │ └── index.test.tsx │ │ ├── index.tsx │ │ └── style.less │ ├── MediaObject │ │ ├── __tests__ │ │ │ └── index.test.tsx │ │ ├── index.tsx │ │ └── style.less │ ├── Message │ │ ├── Message.tsx │ │ ├── SystemMessage.tsx │ │ ├── __tests__ │ │ │ └── index.test.tsx │ │ ├── index.ts │ │ └── style.less │ ├── MessageContainer │ │ ├── __tests__ │ │ │ └── index.test.tsx │ │ ├── index.tsx │ │ └── style.less │ ├── MessageStatus │ │ ├── index.tsx │ │ └── style.less │ ├── Modal │ │ ├── Base.tsx │ │ ├── Confirm.tsx │ │ ├── Modal.tsx │ │ ├── Popup.tsx │ │ ├── __tests__ │ │ │ ├── base.test.tsx │ │ │ ├── confirm.test.tsx │ │ │ ├── modal.test.tsx │ │ │ └── popup.test.tsx │ │ ├── index.ts │ │ └── style.less │ ├── Navbar │ │ ├── __tests__ │ │ │ └── index.test.tsx │ │ ├── index.tsx │ │ └── style.less │ ├── Notice │ │ ├── __tests__ │ │ │ └── index.test.tsx │ │ ├── index.tsx │ │ └── style.less │ ├── Popover │ │ ├── __tests__ │ │ │ └── index.test.tsx │ │ ├── index.tsx │ │ └── style.less │ ├── Portal │ │ └── index.ts │ ├── Price │ │ ├── __tests__ │ │ │ └── index.test.tsx │ │ ├── index.tsx │ │ └── style.less │ ├── Progress │ │ ├── __tests__ │ │ │ └── index.test.tsx │ │ ├── index.tsx │ │ └── style.less │ ├── PullToRefresh │ │ ├── index.tsx │ │ └── style.less │ ├── QuickReplies │ │ ├── QuickReplies.tsx │ │ ├── QuickReply.tsx │ │ ├── __tests__ │ │ │ ├── index.test.tsx │ │ │ └── item.test.tsx │ │ ├── index.ts │ │ └── style.less │ ├── Quote │ │ ├── index.tsx │ │ └── style.less │ ├── Radio │ │ ├── Radio.tsx │ │ ├── RadioGroup.tsx │ │ ├── __tests__ │ │ │ ├── group.test.tsx │ │ │ └── index.test.tsx │ │ ├── index.ts │ │ └── style.less │ ├── RateActions │ │ ├── __tests__ │ │ │ └── index.test.tsx │ │ ├── index.tsx │ │ └── style.less │ ├── Recorder │ │ ├── index.tsx │ │ └── style.less │ ├── RichText │ │ ├── __tests__ │ │ │ └── index.test.tsx │ │ ├── configDOMPurify.ts │ │ ├── index.tsx │ │ └── style.less │ ├── ScrollGrid │ │ ├── index.tsx │ │ └── style.less │ ├── ScrollView │ │ ├── Item.tsx │ │ ├── ScrollView.tsx │ │ ├── __tests__ │ │ │ └── index.test.tsx │ │ ├── index.ts │ │ └── style.less │ ├── Search │ │ ├── index.tsx │ │ └── style.less │ ├── Select │ │ ├── index.tsx │ │ └── style.less │ ├── SendConfirm │ │ ├── SendConfirm.tsx │ │ ├── index.ts │ │ └── style.less │ ├── Skeleton │ │ ├── index.tsx │ │ └── style.less │ ├── Stepper │ │ ├── Step.tsx │ │ ├── Stepper.tsx │ │ ├── __tests__ │ │ │ ├── index.test.tsx │ │ │ └── step.test.tsx │ │ ├── index.ts │ │ └── style.less │ ├── Tabs │ │ ├── Tab.tsx │ │ ├── Tabs.tsx │ │ ├── __tests__ │ │ │ ├── index.test.tsx │ │ │ └── tab.test.tsx │ │ ├── index.ts │ │ └── style.less │ ├── Tag │ │ ├── __tests__ │ │ │ └── index.test.tsx │ │ ├── index.tsx │ │ └── style.less │ ├── Text │ │ ├── __tests__ │ │ │ └── index.test.tsx │ │ ├── index.tsx │ │ └── style.less │ ├── Think │ │ ├── index.tsx │ │ └── style.less │ ├── Time │ │ ├── Time.tsx │ │ ├── __tests__ │ │ │ ├── index.test.tsx │ │ │ └── parser.test.ts │ │ ├── index.ts │ │ ├── parser.ts │ │ └── style.less │ ├── Toast │ │ ├── Toast.tsx │ │ ├── __tests__ │ │ │ ├── index.test.ts │ │ │ └── toast.test.tsx │ │ ├── index.tsx │ │ └── style.less │ ├── Toolbar │ │ ├── Toolbar.tsx │ │ ├── ToolbarButton.tsx │ │ ├── __tests__ │ │ │ ├── button.test.tsx │ │ │ └── index.test.tsx │ │ ├── index.ts │ │ └── style.less │ ├── Tooltip │ │ └── style.less │ ├── Tree │ │ ├── Tree.tsx │ │ ├── TreeNode.tsx │ │ ├── index.ts │ │ └── style.less │ ├── Typing │ │ ├── Typing.tsx │ │ ├── __tests__ │ │ │ ├── __snapshots__ │ │ │ │ └── index.test.tsx.snap │ │ │ └── index.test.tsx │ │ ├── index.ts │ │ └── style.less │ ├── TypingBubble │ │ └── index.tsx │ ├── Video │ │ ├── __tests__ │ │ │ └── index.test.tsx │ │ ├── index.tsx │ │ └── style.less │ └── VisuallyHidden │ │ ├── __tests__ │ │ ├── __snapshots__ │ │ │ └── index.test.tsx.snap │ │ └── index.test.tsx │ │ ├── index.tsx │ │ └── style.less ├── hooks │ ├── __test__ │ │ ├── useMessages.test.tsx │ │ ├── useNextId.test.tsx │ │ └── useQuickReplies.test.tsx │ ├── useClickOutside.ts │ ├── useForwardRef.ts │ ├── useLatest.ts │ ├── useMessages.ts │ ├── useMount.ts │ ├── useNextId.ts │ ├── useQuickReplies.ts │ ├── useTitleTyping.ts │ ├── useTypewriter.ts │ └── useWindowResize.ts ├── index.ts ├── styles │ ├── animation.less │ ├── dark.less │ ├── index.less │ ├── root.less │ ├── scale.less │ ├── utils.less │ └── var.less └── utils │ ├── __tests__ │ ├── getExtName.test.ts │ ├── index.test.tsx │ ├── prettyBytes.test.ts │ ├── style.test.tsx │ └── toggleClass.test.tsx │ ├── canUse.ts │ ├── countLines.ts │ ├── formatTime.ts │ ├── getExtName.ts │ ├── getFps.ts │ ├── getRandomInt.ts │ ├── getToBottom.ts │ ├── importScript.ts │ ├── index.ts │ ├── lazyComponent.tsx │ ├── mountComponent.ts │ ├── parseDataTransfer.ts │ ├── prettyBytes.ts │ ├── smoothScroll.ts │ ├── style.ts │ ├── throttle.ts │ ├── toggleClass.ts │ └── ua.ts ├── tsconfig.build.json ├── tsconfig.eslint.json └── tsconfig.json /.eslintignore: -------------------------------------------------------------------------------- 1 | *.js 2 | 3 | # build 4 | dist 5 | es 6 | lib 7 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parserOptions: { 4 | ecmaVersion: 2020, 5 | project: './tsconfig.json', 6 | }, 7 | extends: [ 8 | 'airbnb/hooks', 9 | 'airbnb-typescript', 10 | 'prettier', 11 | ], 12 | env: { 13 | browser: true, 14 | jest: true, 15 | }, 16 | plugins: ['compat', 'import', 'jsx-a11y', 'react', 'react-hooks'], 17 | rules: { 18 | 'compat/compat': 'error', 19 | 'import/prefer-default-export': 'off', 20 | // 'no-underscore-dangle': 'off', 21 | // 'react/jsx-filename-extension': 'off', 22 | 'react/jsx-props-no-spreading': 'off', 23 | // 'react/no-array-index-key': 'off', 24 | 'react/require-default-props': 'off', 25 | 'react/prop-types': 'off', 26 | // 'jsx-a11y/click-events-have-key-events': 'off', 27 | // 'jsx-a11y/label-has-associated-control': 'off', 28 | // 'jsx-a11y/label-has-for': 'off', 29 | }, 30 | settings: { 31 | polyfills: ['IntersectionObserver', 'Promise'], 32 | }, 33 | }; 34 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Version information (版本信息)** 11 | - ChatUI or ChatUI Pro? 12 | - ChatUI Version: 13 | - React Version: 14 | - OS Version: 15 | - Browser Version: 16 | 17 | **Describe the bug (描述问题)** 18 | 19 | **Steps To Reproduce (重现步骤)** 20 | 1. 21 | 2. 22 | 23 | **Link to minimal reproduction (最小化重现链接)** 24 | 25 | **Expected behavior (期望的结果是什么)** 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request-------.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request (功能要求) 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **What problem does this feature solve? (这个功能解决了什么问题)** 11 | 12 | **Describe the solution you'd like (描述您想要的解决方案)** 13 | 14 | **What does the proposed API look like? (你期望的 API 是怎样的)** 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | /node_modules 3 | /storybook/node_modules 4 | 5 | # testing 6 | /coverage 7 | 8 | # production 9 | /build 10 | /dist 11 | /lib 12 | /es 13 | 14 | # misc 15 | .DS_Store 16 | 17 | npm-debug.log* 18 | yarn-debug.log* 19 | yarn-error.log* 20 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx --no-install commitlint --edit "$1" 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "singleQuote": true, 4 | "trailingComma": "all", 5 | "proseWrap": "never", 6 | "endOfLine": "lf" 7 | } 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Alibaba 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 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = (api) => { 2 | const env = api.env(); 3 | api.cache.using(() => env === 'development'); 4 | 5 | return { 6 | presets: ['@babel/preset-env', '@babel/preset-typescript', '@babel/preset-react'], 7 | plugins: [ 8 | '@babel/plugin-transform-runtime', 9 | 10 | // Stage 3 11 | '@babel/plugin-proposal-class-properties', 12 | ], 13 | env: { 14 | esm: { 15 | presets: [ 16 | [ 17 | '@babel/preset-env', 18 | { 19 | modules: false, 20 | }, 21 | ], 22 | ], 23 | plugins: [ 24 | [ 25 | '@babel/plugin-transform-runtime', 26 | { 27 | useESModules: true, 28 | }, 29 | ], 30 | ], 31 | }, 32 | umd: { 33 | presets: [ 34 | [ 35 | '@babel/preset-env', 36 | { 37 | targets: { 38 | android: '4.4', 39 | ios: '9', 40 | }, 41 | useBuiltIns: 'usage', 42 | corejs: 3, 43 | }, 44 | ], 45 | ], 46 | plugins: [['@babel/plugin-transform-runtime', { corejs: 3 }]], 47 | }, 48 | }, 49 | ignore: [ 50 | '**/*.test.ts', 51 | '**/*.test.tsx', 52 | ] 53 | }; 54 | }; 55 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional'], 3 | }; 4 | -------------------------------------------------------------------------------- /demo/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ChatUI DEMO 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo", 3 | "private": true, 4 | "version": "0.1.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite --host", 8 | "build": "tsc -b && vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "marked": "^15.0.7", 13 | "react": "^17.0.2", 14 | "react-dom": "^17.0.2", 15 | "react-router-dom": "^6.28.2" 16 | }, 17 | "devDependencies": { 18 | "@types/react": "^17.0.83", 19 | "@types/react-dom": "^17.0.26", 20 | "@types/react-router-dom": "^5.3.3", 21 | "@vitejs/plugin-react": "^4.3.4", 22 | "typescript": "^5.7.3", 23 | "vite": "^6.0.11" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /demo/src/components/DemoIndex.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import { navConfig } from '../navConfig'; 4 | import { toPascalCase } from '../utils'; 5 | 6 | export default function DemoIndex() { 7 | useEffect(() => { 8 | const colorSchemeQuery = window.matchMedia('(prefers-color-scheme: dark)'); 9 | const handleColorSchemeChange = (e: MediaQueryListEvent | MediaQueryList) => { 10 | document.documentElement.dataset.colorScheme = e.matches ? 'dark' : 'light'; 11 | }; 12 | 13 | colorSchemeQuery.addEventListener('change', handleColorSchemeChange); 14 | handleColorSchemeChange(colorSchemeQuery); 15 | }, []); 16 | 17 | return ( 18 |
19 | {navConfig.map((t) => ( 20 |
21 |

{t.title}

22 | 31 |
32 | ))} 33 |
34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /demo/src/components/DemoPage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link, useLocation } from 'react-router-dom'; 3 | import { toPascalCase } from '../utils'; 4 | 5 | interface DemoPageProps { 6 | children: React.ReactNode; 7 | } 8 | 9 | export const DemoPage = ({ children }: DemoPageProps) => { 10 | const { pathname } = useLocation(); 11 | const name = pathname.slice(1); 12 | 13 | return ( 14 |
15 |
16 | 17 | 18 | 19 | 20 | 21 |

{toPascalCase(name)}

22 |
23 | {children} 24 |
25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /demo/src/components/DemoSection.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface DemoSectionProps { 4 | title: string; 5 | bg?: 'white' | 'gray'; 6 | children: React.ReactNode; 7 | } 8 | 9 | export const DemoSection = ({ title, bg = 'white', children }: DemoSectionProps) => ( 10 |
11 |

{title}

12 | {children} 13 |
14 | ); 15 | -------------------------------------------------------------------------------- /demo/src/components/LangSwitcher.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import locales from '../../../src/components/ConfigProvider/locales'; 3 | 4 | interface LangSwitcherProps { 5 | value: string; 6 | onChange: (lang: string) => void; 7 | } 8 | 9 | export const LangSwitcher = ({ value, onChange }: LangSwitcherProps) => ( 10 | 22 | ); 23 | -------------------------------------------------------------------------------- /demo/src/components/index.ts: -------------------------------------------------------------------------------- 1 | export { DemoPage } from './DemoPage'; 2 | export { DemoSection } from './DemoSection'; 3 | export { LangSwitcher } from './LangSwitcher'; 4 | -------------------------------------------------------------------------------- /demo/src/demo/Avatar.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { DemoPage, DemoSection } from '../components'; 3 | import { Avatar } from '../../../src'; 4 | 5 | const img = '//gw.alicdn.com/tfs/TB1U7FBiAT2gK0jSZPcXXcKkpXa-108-108.jpg'; 6 | 7 | export default () => ( 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | ); 21 | -------------------------------------------------------------------------------- /demo/src/demo/Bubble.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { DemoPage, DemoSection } from '../components'; 3 | import { Bubble } from '../../../src'; 4 | 5 | export default () => ( 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 | 33 | 34 |
35 |
36 |
37 |
38 | ); 39 | -------------------------------------------------------------------------------- /demo/src/demo/Carousel.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { DemoPage, DemoSection } from '../components'; 3 | import { Carousel, Image } from '../../../src'; 4 | 5 | const imgs = [ 6 | '//gw.alicdn.com/tfs/TB1GRW3voY1gK0jSZFMXXaWcVXa-620-320.jpg', 7 | '//gw.alicdn.com/tfs/TB1I6i2vhD1gK0jSZFsXXbldVXa-620-320.jpg', 8 | '//gw.alicdn.com/tfs/TB1XCq4veH2gK0jSZFEXXcqMpXa-620-320.jpg', 9 | '//gw.alicdn.com/tfs/TB1dzG8vbj1gK0jSZFuXXcrHpXa-620-319.jpg', 10 | ]; 11 | 12 | export default () => ( 13 | 14 | 15 | 16 | {imgs.map((img, i) => ( 17 |
18 |

{i}

19 | 20 |
21 | ))} 22 |
23 |
24 |
25 | ); 26 | -------------------------------------------------------------------------------- /demo/src/demo/Confirm.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { DemoPage, DemoSection } from '../components'; 3 | import { Confirm, Card, List, ListItem } from '../../../src'; 4 | 5 | export default () => { 6 | const [open1, setOpen1] = useState(false); 7 | 8 | return ( 9 | 10 | 11 | 12 | 13 | { 17 | setOpen1(true); 18 | }} 19 | rightIcon="chevron-right" 20 | /> 21 | 22 | 23 | 24 | { 28 | setOpen1(false); 29 | }} 30 | actions={[ 31 | { 32 | label: '确认', 33 | color: 'primary', 34 | }, 35 | ]} 36 | > 37 |
内容详情内容详情内容详情内容详情内容详情内容详情
38 |
39 |
40 | ); 41 | }; 42 | -------------------------------------------------------------------------------- /demo/src/demo/Divider.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { DemoPage, DemoSection } from '../components'; 3 | import { Divider } from '../../../src'; 4 | 5 | export default () => ( 6 | 7 | 8 | 9 | 10 | 11 | 文本 12 | 13 | 14 | 文本 15 | 左文本 16 | 右文本 17 | 18 | 19 | ); 20 | -------------------------------------------------------------------------------- /demo/src/demo/Empty.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { DemoPage, DemoSection } from '../components'; 3 | import { Empty, Button } from '../../../src'; 4 | 5 | export default () => ( 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | ); 17 | -------------------------------------------------------------------------------- /demo/src/demo/FileCard.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { DemoPage, DemoSection } from '../components'; 3 | import { FileCard } from '../../../src'; 4 | 5 | const file = new File(['foo'], 'foo.txt', { 6 | type: 'text/plain', 7 | }); 8 | 9 | export default () => ( 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 下载 20 | 21 | 22 | 23 | ); 24 | -------------------------------------------------------------------------------- /demo/src/demo/Icon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { DemoPage, DemoSection } from '../components'; 3 | import { Icon } from '../../../src'; 4 | 5 | const symbols = document.getElementById('__CHATUI_ICONS__')?.querySelectorAll('symbol') || []; 6 | 7 | export default () => ( 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 |
16 |
17 | 18 |
19 | {Array.from(symbols).map((item) => { 20 | const icon = item.id.replace('icon-', ''); 21 | return ( 22 |
23 | 24 | {icon} 25 |
26 | ); 27 | })} 28 |
29 |
30 |
31 | ); 32 | -------------------------------------------------------------------------------- /demo/src/demo/Image.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { DemoPage, DemoSection } from '../components'; 3 | import { Image } from '../../../src'; 4 | 5 | const picUrl = '//gw.alicdn.com/tfs/TB1GRW3voY1gK0jSZFMXXaWcVXa-620-320.jpg'; 6 | 7 | const imgs = [ 8 | '//gw.alicdn.com/tfs/TB1yGi2vfb2gK0jSZK9XXaEgFXa-320-240.png', 9 | '//gw.alicdn.com/tfs/TB1I6i2vhD1gK0jSZFsXXbldVXa-620-320.jpg', 10 | '//gw.alicdn.com/tfs/TB1GRW3voY1gK0jSZFMXXaWcVXa-620-320.jpg', 11 | '//gw.alicdn.com/tfs/TB1XCq4veH2gK0jSZFEXXcqMpXa-620-320.jpg', 12 | '//gw.alicdn.com/tfs/TB1dzG8vbj1gK0jSZFuXXcrHpXa-620-319.jpg', 13 | ]; 14 | 15 | export default () => ( 16 | 17 | 18 | 图片名 19 | 20 | 21 | Responsive image 22 | 23 | 24 | {imgs.map((img) => ( 25 |
26 |

placeholder

27 | 28 |
29 | ))} 30 |
31 |
32 | ); 33 | -------------------------------------------------------------------------------- /demo/src/demo/InfiniteScroll.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { DemoPage, DemoSection } from '../components'; 3 | import { InfiniteScroll } from '../../../src'; 4 | 5 | export default () => { 6 | const [list, setList] = useState([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]); 7 | const [disabled, setDisabled] = useState(false); 8 | 9 | function handleLoadMore() { 10 | if (list.length > 50) { 11 | setDisabled(true); 12 | return; 13 | } 14 | 15 | for (let i = 0; i < 10; i += 1) { 16 | setList((s) => [...s, s.length + 1]); 17 | } 18 | } 19 | 20 | return ( 21 | 22 | 23 | 26 | {` : ${disabled}`} 27 | 28 |
    29 | {list.map((t) => ( 30 |
  • {t}
  • 31 | ))} 32 |
33 |
34 |
35 |
36 | ); 37 | }; 38 | -------------------------------------------------------------------------------- /demo/src/demo/Loading.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { DemoPage, DemoSection } from '../components'; 3 | import { Loading } from '../../../src'; 4 | 5 | export default () => ( 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | ); 15 | -------------------------------------------------------------------------------- /demo/src/demo/MediaObject.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { DemoPage, DemoSection } from '../components'; 3 | import { MediaObject } from '../../../src'; 4 | 5 | export default () => ( 6 | 7 | 8 | 15 | 16 | 17 | ); 18 | -------------------------------------------------------------------------------- /demo/src/demo/MessageStatus.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { DemoPage, DemoSection } from '../components'; 3 | import { MessageStatus } from '../../../src'; 4 | 5 | export default () => { 6 | return ( 7 | 8 | 9 | { 12 | console.log('retry', isAutoRetry); 13 | }} 14 | onChange={(t) => { 15 | console.log('change:', t); 16 | }} 17 | /> 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | ); 27 | }; 28 | -------------------------------------------------------------------------------- /demo/src/demo/Notice.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { DemoPage, DemoSection } from '../components'; 3 | import { Notice } from '../../../src'; 4 | 5 | export default () => ( 6 | 7 | 8 |
9 | 10 |
11 |
12 | 13 |
14 | 18 |
19 |
20 | 21 |
22 | 23 |
24 |
25 | 26 |
27 | 28 |
29 |
30 |
31 | ); 32 | -------------------------------------------------------------------------------- /demo/src/demo/Portal.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef } from 'react'; 2 | import { DemoPage, DemoSection } from '../components'; 3 | import { Portal } from '../../../src'; 4 | 5 | export default () => { 6 | const containerRef = useRef(null); 7 | return ( 8 | 9 | 10 | 11 |

出现到 `document.body`

12 |
13 |
14 | 15 |
16 | 17 |

出现到指定 `ref`

18 |
19 | 20 | 21 | 22 |

出现到指定元素

23 |
24 |
25 | 26 | ); 27 | }; 28 | -------------------------------------------------------------------------------- /demo/src/demo/Progress.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { DemoPage, DemoSection } from '../components'; 3 | import { Progress, Button } from '../../../src'; 4 | 5 | export default () => { 6 | const [percentage, setPercentage] = useState(20); 7 | return ( 8 | 9 | 10 | 11 |
12 | 13 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /demo/src/demo/PullToRefresh.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { DemoPage, DemoSection } from '../components'; 3 | import { PullToRefresh } from '../../../src'; 4 | 5 | export default () => ( 6 | 7 | 8 |
9 | Promise.resolve({})}> 10 |
list
11 |
12 |
13 |
14 |
15 | ); 16 | -------------------------------------------------------------------------------- /demo/src/demo/Quote.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { DemoPage, DemoSection } from '../components'; 3 | import { Quote, Text, Image, Video } from '../../../src'; 4 | 5 | export default () => ( 6 | 7 | 8 | 9 | 亲,您好我是官方客服014964,马上为您服务~ 10 | 11 | 12 | 13 | 14 | 15 | 我们会帮您联系物流催促配送,后续会在24小时内联系您的手机号158****0702。我们会帮您联系物流催促配送,后续会在24小时内联系您的手机号158****0702。 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 31 | 32 | 33 | ); 34 | -------------------------------------------------------------------------------- /demo/src/demo/Radio.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { DemoPage, DemoSection } from '../components'; 3 | import { Radio, RadioGroup, RadioValue } from '../../../src'; 4 | 5 | const options = [ 6 | { label: 'Apple', value: 'Apple' }, 7 | { label: 'Pear', value: 'Pear', disabled: true }, 8 | { label: 'Orange', value: 'Orange' }, 9 | { label: 'Banana', value: 'Banana' }, 10 | ]; 11 | 12 | export default () => { 13 | const [value, setValue] = useState('a'); 14 | 15 | function handleChange(val: RadioValue) { 16 | setValue(val); 17 | } 18 | 19 | return ( 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /demo/src/demo/RateActions.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { DemoPage, DemoSection } from '../components'; 3 | import { RateActions, ConfigProvider } from '../../../src'; 4 | 5 | export default () => ( 6 | 7 | 8 | { 10 | console.log(val); 11 | }} 12 | /> 13 | 14 | 15 | 16 | { 18 | console.log(val); 19 | }} 20 | /> 21 | 22 | 23 | 24 | 25 | { 29 | console.log(val); 30 | }} 31 | /> 32 | 33 | 34 | 35 | ); 36 | -------------------------------------------------------------------------------- /demo/src/demo/RichText.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { DemoPage, DemoSection } from '../components'; 3 | import { RichText } from '../../../src'; 4 | 5 | const html = '

H1标题

这是段落em标签strong标签

'; 6 | 7 | export default () => ( 8 | 9 | 10 | 11 | 12 | 13 | ); 14 | -------------------------------------------------------------------------------- /demo/src/demo/ScrollGrid.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { DemoPage, DemoSection } from '../components'; 3 | import { ScrollGrid } from '../../../src'; 4 | 5 | export default () => { 6 | return ( 7 | 8 | 9 | 10 | {Array.from({ length: 6 }).map((t, i) => ( 11 |
20 | {i} 21 |
22 | ))} 23 |
24 |
25 | 26 | 27 | {Array.from({ length: 6 }).map((t, i) => ( 28 |
37 | {i} 38 |
39 | ))} 40 |
41 |
42 |
43 | ); 44 | }; 45 | -------------------------------------------------------------------------------- /demo/src/demo/ScrollView.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { DemoPage, DemoSection } from '../components'; 3 | import { ScrollView, Button } from '../../../src'; 4 | 5 | // const list = [{ text: '内容1' }, { text: '内容2' }, { text: '内容3' }]; 6 | 7 | export default () => { 8 | const [list, setList] = React.useState([{ text: '内容1' }, { text: '内容2' }, { text: '内容3' }]); 9 | return ( 10 | 11 | 12 | 20 | 26 | 15 | 16 | 17 | ); 18 | -------------------------------------------------------------------------------- /demo/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { createHashRouter, RouterProvider } from 'react-router-dom'; 4 | import { routerConfig } from './routerConfig'; 5 | import './index.less'; 6 | import '../../src/styles/index.less'; 7 | 8 | const router = createHashRouter(routerConfig, { 9 | future: { 10 | v7_relativeSplatPath: true, 11 | }, 12 | }); 13 | 14 | ReactDOM.render( 15 | 16 | 22 | , 23 | document.getElementById('root'), 24 | ); 25 | -------------------------------------------------------------------------------- /demo/src/routerConfig.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import DemoIndex from './components/DemoIndex'; 3 | import * as demos from './demo'; 4 | import { navConfig } from './navConfig'; 5 | import { toPascalCase } from './utils'; 6 | 7 | export const routerConfig = navConfig.reduce( 8 | (prev, current) => { 9 | const currentList = current.list.map((item: any) => { 10 | const Comp = (demos as any)[toPascalCase(item.code)]; 11 | return { path: item.code, element: }; 12 | }); 13 | return [...prev, ...currentList]; 14 | }, 15 | [{ path: '/', element: }], 16 | ); 17 | -------------------------------------------------------------------------------- /demo/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export { default as toPascalCase } from './toPascalCase'; 2 | -------------------------------------------------------------------------------- /demo/src/utils/toPascalCase.ts: -------------------------------------------------------------------------------- 1 | export default function toPascalCase(str: string) { 2 | return str 3 | .split('-') 4 | .map((w) => w[0].toUpperCase() + w.slice(1).toLowerCase()) 5 | .join(''); 6 | } 7 | -------------------------------------------------------------------------------- /demo/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /demo/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", 4 | "target": "ES2020", 5 | "useDefineForClassFields": true, 6 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 7 | "module": "ESNext", 8 | "skipLibCheck": true, 9 | 10 | /* Bundler mode */ 11 | "moduleResolution": "bundler", 12 | "allowImportingTsExtensions": true, 13 | "isolatedModules": true, 14 | "moduleDetection": "force", 15 | "noEmit": true, 16 | "jsx": "react-jsx", 17 | 18 | /* Linting */ 19 | "strict": true, 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": true, 22 | "noFallthroughCasesInSwitch": true, 23 | "noUncheckedSideEffectImports": true 24 | }, 25 | "include": ["src"] 26 | } 27 | -------------------------------------------------------------------------------- /demo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { "path": "./tsconfig.app.json" }, 5 | { "path": "./tsconfig.node.json" } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /demo/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", 4 | "target": "ES2022", 5 | "lib": ["ES2023"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "isolatedModules": true, 13 | "moduleDetection": "force", 14 | "noEmit": true, 15 | 16 | /* Linting */ 17 | "strict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "noFallthroughCasesInSwitch": true, 21 | "noUncheckedSideEffectImports": true 22 | }, 23 | "include": ["vite.config.ts"] 24 | } 25 | -------------------------------------------------------------------------------- /demo/vite.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | import { defineConfig } from 'vite'; 3 | import react from '@vitejs/plugin-react'; 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | base: './', 8 | plugins: [react()], 9 | build: { 10 | outDir: '../dist/demo', 11 | rollupOptions: { 12 | output: { 13 | entryFileNames: `[name].js`, 14 | chunkFileNames: `[name].js`, 15 | assetFileNames: `[name].[ext]`, 16 | }, 17 | }, 18 | }, 19 | }); 20 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'jsdom', 4 | testPathIgnorePatterns: ['/node_modules/', '/lib/', '/es/', '/dist/', 'examples'], 5 | setupFilesAfterEnv: ['./jest.setup.ts'], 6 | }; 7 | -------------------------------------------------------------------------------- /jest.setup.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | // https://github.com/testing-library/jest-dom 3 | import '@testing-library/jest-dom/extend-expect'; 4 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | const cssnanoConfig = { 2 | preset: [ 3 | 'default', 4 | { 5 | calc: false, 6 | discardComments: { removeAll: true }, 7 | }, 8 | ], 9 | }; 10 | 11 | const pxtoremConfig = { 12 | propList: ['*', '!border*', '!box-shadow'], 13 | selectorBlackList: [':root'], 14 | }; 15 | 16 | /** @type {import('postcss-load-config').Config} */ 17 | const config = { 18 | plugins: 19 | process.env.NODE_ENV === 'production' 20 | ? [ 21 | require('autoprefixer'), 22 | require('postcss-pxtorem')(pxtoremConfig), 23 | require('cssnano')(cssnanoConfig), 24 | ] 25 | : [], 26 | }; 27 | 28 | module.exports = config; 29 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import commonjs from '@rollup/plugin-commonjs'; 2 | import resolve from '@rollup/plugin-node-resolve'; 3 | import babel from '@rollup/plugin-babel'; 4 | import terser from '@rollup/plugin-terser'; 5 | import pkg from './package.json'; 6 | 7 | const name = 'ChatUI'; 8 | const extensions = ['.js', '.jsx', '.ts', '.tsx']; 9 | 10 | export default { 11 | input: './src/index.ts', 12 | external: ['react', 'react-dom'], 13 | plugins: [ 14 | resolve({ extensions }), 15 | commonjs(), 16 | babel({ 17 | extensions, 18 | babelHelpers: 'runtime', 19 | include: ['src/**/*'], 20 | }), 21 | terser({ 22 | output: { comments: false }, 23 | compress: { drop_console: true }, 24 | }), 25 | ], 26 | output: { 27 | file: pkg.browser, 28 | format: 'umd', 29 | name, 30 | globals: { 31 | react: 'React', 32 | 'react-dom': 'ReactDOM', 33 | }, 34 | intro: `exports.version = '${pkg.version}';`, 35 | }, 36 | }; 37 | -------------------------------------------------------------------------------- /src/components/Avatar/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import clsx from 'clsx'; 3 | 4 | export type AvatarSize = 'sm' | 'md' | 'lg'; 5 | 6 | export type AvatarShape = 'circle' | 'square'; 7 | 8 | export interface AvatarProps { 9 | className?: string; 10 | src?: string; 11 | alt?: string; 12 | url?: string; 13 | size?: AvatarSize; 14 | shape?: AvatarShape; 15 | } 16 | 17 | export const Avatar: React.FC = (props) => { 18 | const { className, src, alt, url, size = 'md', shape = 'circle', children } = props; 19 | 20 | const Element = url ? 'a' : 'span'; 21 | return ( 22 | 26 | {src ? {alt} : children} 27 | 28 | ); 29 | }; 30 | -------------------------------------------------------------------------------- /src/components/Avatar/style.less: -------------------------------------------------------------------------------- 1 | .Avatar { 2 | --avatar-size: 32px; 3 | 4 | display: inline-block; 5 | overflow: hidden; 6 | border-radius: 50%; 7 | 8 | img { 9 | display: block; 10 | width: var(--avatar-size); 11 | height: var(--avatar-size); 12 | object-fit: cover; 13 | } 14 | } 15 | 16 | .Avatar--sm { 17 | --avatar-size: 18px; 18 | } 19 | 20 | .Avatar--lg { 21 | --avatar-size: 40px; 22 | } 23 | 24 | .Avatar--square { 25 | border-radius: var(--radius-md); 26 | } 27 | -------------------------------------------------------------------------------- /src/components/BackBottom/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { Button } from '../Button'; 3 | import { Icon } from '../Icon'; 4 | import { useLocale } from '../ConfigProvider'; 5 | 6 | interface BackBottomProps { 7 | count: number; 8 | onClick: () => void; 9 | onDidMount?: () => void; 10 | } 11 | 12 | export const BackBottom = ({ count, onClick, onDidMount }: BackBottomProps) => { 13 | const { trans } = useLocale('BackBottom'); 14 | let text = trans('bottom'); 15 | if (count) { 16 | text = trans(count === 1 ? 'newMsgOne' : 'newMsgOther').replace('{n}', count); 17 | } 18 | 19 | useEffect(() => { 20 | if (onDidMount) { 21 | onDidMount(); 22 | } 23 | }, [onDidMount]); 24 | 25 | return ( 26 |
27 | 31 |
32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /src/components/BackBottom/style.less: -------------------------------------------------------------------------------- 1 | .BackBottom { 2 | position: absolute; 3 | right: 0; 4 | bottom: (20px + 48px); 5 | z-index: 10; 6 | overflow: hidden; 7 | 8 | .Btn { 9 | border-radius: 50px 0 0 50px; 10 | border-right: 0; 11 | background: rgba(255, 255, 255, 0.85); 12 | color: var(--brand-1); 13 | font-size: var(--font-size-sm); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/components/Backdrop/__tests__/index.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, cleanup } from '@testing-library/react'; 3 | import { Backdrop } from '..'; 4 | 5 | afterEach(cleanup); 6 | 7 | describe('', () => { 8 | it('should render Backdrop', () => { 9 | const { container } = render(); 10 | const element = container.querySelector('.Backdrop'); 11 | 12 | expect(element).toBeInTheDocument(); 13 | expect(element).toHaveClass('active'); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /src/components/Backdrop/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import clsx from 'clsx'; 3 | 4 | export interface BackdropProps { 5 | className?: string; 6 | active?: boolean; 7 | onClick?: React.MouseEventHandler; 8 | } 9 | 10 | export const Backdrop = (props: BackdropProps) => { 11 | const { className, active, onClick, ...rest } = props; 12 | return ( 13 |
18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /src/components/Backdrop/style.less: -------------------------------------------------------------------------------- 1 | .Backdrop { 2 | position: fixed; 3 | bottom: 0; 4 | left: 0; 5 | right: 0; 6 | z-index: @zindex-backdrop; 7 | transition: 0.3s; 8 | width: 100vw; 9 | height: 100vh; 10 | background: var(--color-mask); 11 | opacity: 0; 12 | outline: 0; 13 | 14 | &.active { 15 | opacity: 1; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/components/Bubble/__tests__/index.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, cleanup } from '@testing-library/react'; 3 | import { Bubble } from '..'; 4 | 5 | afterEach(cleanup); 6 | 7 | describe('', () => { 8 | it('should render bubble', () => { 9 | const content = 'myTestContent'; 10 | const type = 'text'; 11 | 12 | const { getByText } = render({content}); 13 | const element = getByText(content); 14 | 15 | expect(element).toHaveClass(type); 16 | expect(element).toHaveAttribute('data-type', type); 17 | }); 18 | 19 | it('should render content', () => { 20 | const text = 'myText'; 21 | const { container } = render(); 22 | const element = container.querySelector('.Bubble'); 23 | 24 | expect(element).toHaveClass('text'); 25 | expect(element).toHaveAttribute('data-type', 'text'); 26 | expect(element).toHaveTextContent(text); 27 | }); 28 | 29 | it('should render custom type', () => { 30 | const text = 'myCustom'; 31 | const content = {text}; 32 | const type = 'myType'; 33 | 34 | const { container } = render({content}); 35 | const element = container.querySelector('.Bubble'); 36 | 37 | expect(element).toHaveClass(type); 38 | expect(element).toHaveAttribute('data-type', type); 39 | expect(element).toHaveTextContent(text); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /src/components/Bubble/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export interface BubbleProps extends React.HTMLAttributes { 4 | type?: string; 5 | content?: string; 6 | } 7 | 8 | export const Bubble = React.forwardRef((props, ref) => { 9 | const { type = 'text', content, children, ...other } = props; 10 | return ( 11 |
12 | {content &&

{content}

} 13 | {children} 14 |
15 | ); 16 | }); 17 | -------------------------------------------------------------------------------- /src/components/Bubble/style.less: -------------------------------------------------------------------------------- 1 | .Bubble { 2 | max-width: 512px; 3 | // min-width: 0; 4 | min-width: 1Px; // for IE bug 5 | background: var(--color-fill-1); 6 | border-radius: var(--radius-md); 7 | 8 | &.text, 9 | &.typing, 10 | &.richtext { 11 | padding: 12px; 12 | box-sizing: border-box; 13 | word-wrap: break-word; 14 | overflow-wrap: break-word; 15 | } 16 | &.text { 17 | min-width: 40px; 18 | white-space: pre-wrap; 19 | } 20 | &.image { 21 | img { 22 | display: block; 23 | max-width: 180px; 24 | min-width: 80px; 25 | max-height: 180px; 26 | min-height: 80px; 27 | object-fit: cover; 28 | height: auto; 29 | border-radius: inherit; 30 | } 31 | } 32 | p { 33 | margin: 0; 34 | } 35 | } 36 | 37 | [data-effect='typing'] { 38 | position: relative; 39 | overflow: hidden; 40 | 41 | h1, 42 | h2, 43 | h3, 44 | h4, 45 | h5, 46 | h6, 47 | p, 48 | ol:last-child li, 49 | ul:last-child li { 50 | &:last-child:after { 51 | content: ''; 52 | width: 80px; 53 | height: 1.5em; 54 | background: linear-gradient(90deg, transparent, var(--color-fill-1)); 55 | position: absolute; 56 | margin-left: -80px; 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/components/Card/Card.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import clsx from 'clsx'; 3 | 4 | export type CardSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl'; 5 | 6 | export interface CardProps { 7 | className?: string; 8 | size?: CardSize; 9 | fluid?: boolean | 'order'; 10 | children?: React.ReactNode; 11 | } 12 | 13 | export const Card = React.forwardRef((props, ref) => { 14 | const { className, size, fluid, children, ...other } = props; 15 | 16 | return ( 17 |
23 | {children} 24 |
25 | ); 26 | }); 27 | -------------------------------------------------------------------------------- /src/components/Card/CardActions.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import clsx from 'clsx'; 3 | 4 | export type CardActionsProps = { 5 | className?: string; 6 | direction?: 'column' | 'row'; 7 | }; 8 | 9 | export const CardActions: React.FC = (props) => { 10 | const { children, className, direction, ...other } = props; 11 | return ( 12 |
16 | {children} 17 |
18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /src/components/Card/CardContent.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import clsx from 'clsx'; 3 | 4 | export type CardContentProps = { 5 | className?: string; 6 | }; 7 | 8 | export const CardContent: React.FC = (props) => { 9 | const { className, children, ...other } = props; 10 | return ( 11 |
12 | {children} 13 |
14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /src/components/Card/CardMedia.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import clsx from 'clsx'; 3 | import { Flex } from '../Flex'; 4 | 5 | export type CardMediaProps = { 6 | className?: string; 7 | aspectRatio?: 'square' | 'wide'; 8 | color?: string; 9 | image?: string; 10 | }; 11 | 12 | export const CardMedia: React.FC = (props) => { 13 | const { className, aspectRatio = 'square', color, image, children, ...other } = props; 14 | 15 | const bgStyle = { 16 | backgroundColor: color || undefined, 17 | backgroundImage: typeof image === 'string' ? `url('${image}')` : undefined, 18 | }; 19 | 20 | return ( 21 |
33 | {children && ( 34 | 35 | {children} 36 | 37 | )} 38 |
39 | ); 40 | }; 41 | -------------------------------------------------------------------------------- /src/components/Card/CardText.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import clsx from 'clsx'; 3 | 4 | export type CardTextProps = { 5 | className?: string; 6 | }; 7 | 8 | export const CardText: React.FC = (props) => { 9 | const { className, children, ...other } = props; 10 | return ( 11 |
12 | {typeof children === 'string' ?

{children}

: children} 13 |
14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /src/components/Card/CardTitle.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import clsx from 'clsx'; 3 | 4 | export type CardTitleProps = { 5 | className?: string; 6 | title?: React.ReactNode; 7 | subtitle?: React.ReactNode; 8 | center?: boolean; 9 | }; 10 | 11 | export const CardTitle: React.FC = (props) => { 12 | const { className, title, subtitle, center, children, ...other } = props; 13 | return ( 14 |
15 | {title &&
{title}
} 16 | {children && typeof children === 'string' &&
{children}
} 17 | {subtitle &&

{subtitle}

} 18 | {children && typeof children !== 'string' && children} 19 |
20 | ); 21 | }; 22 | -------------------------------------------------------------------------------- /src/components/Card/__tests__/actions.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, cleanup } from '@testing-library/react'; 3 | import { CardActions } from '..'; 4 | 5 | afterEach(cleanup); 6 | 7 | describe('', () => { 8 | it('should render CardActions', () => { 9 | const text = 'myText'; 10 | 11 | const { getByText } = render({text}); 12 | const element = getByText(text); 13 | 14 | expect(element).toBeInTheDocument(); 15 | expect(element).toHaveClass('CardActions'); 16 | }); 17 | 18 | it('should apply direction class', () => { 19 | const text = 'myText'; 20 | 21 | const { getByText } = render({text}); 22 | const element = getByText(text); 23 | 24 | expect(element).toHaveClass('CardActions--column'); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /src/components/Card/__tests__/content.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, cleanup } from '@testing-library/react'; 3 | import { CardContent } from '..'; 4 | 5 | afterEach(cleanup); 6 | 7 | describe('', () => { 8 | it('should render CardContent', () => { 9 | const text = 'myText'; 10 | 11 | const { getByText } = render({text}); 12 | const element = getByText(text); 13 | 14 | expect(element).toBeInTheDocument(); 15 | expect(element).toHaveClass('CardContent'); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /src/components/Card/__tests__/index.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, cleanup } from '@testing-library/react'; 3 | import { Card } from '..'; 4 | 5 | afterEach(cleanup); 6 | 7 | describe('', () => { 8 | it('should render card', () => { 9 | const text = 'myCard'; 10 | 11 | const { getByText } = render({text}); 12 | const element = getByText(text); 13 | 14 | expect(element).toBeInTheDocument(); 15 | expect(element).toHaveClass('Card'); 16 | }); 17 | 18 | it('should apply size class', () => { 19 | const text = 'myCard'; 20 | 21 | const { getByText } = render({text}); 22 | const element = getByText(text); 23 | 24 | expect(element).toHaveClass('Card--sm'); 25 | }); 26 | 27 | it('should be fluid', () => { 28 | const text = 'myCard'; 29 | 30 | const { getByText } = render({text}); 31 | const element = getByText(text); 32 | 33 | expect(element).toHaveClass('Card--fluid'); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /src/components/Card/__tests__/media.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, cleanup } from '@testing-library/react'; 3 | import { CardMedia } from '..'; 4 | 5 | afterEach(cleanup); 6 | 7 | describe('', () => { 8 | it('should render CardMedia', () => { 9 | const text = 'myText'; 10 | 11 | const { getByText } = render({text}); 12 | const element = getByText(text); 13 | 14 | expect(element).toBeInTheDocument(); 15 | }); 16 | 17 | it('should apply aspectRatio class', () => { 18 | const { container } = render(square); 19 | const element = container.querySelector('.CardMedia'); 20 | 21 | expect(element).toHaveClass('CardMedia--square'); 22 | }); 23 | 24 | it('should render color', () => { 25 | const { container } = render(color); 26 | const element = container.querySelector('.CardMedia'); 27 | 28 | expect(element).toHaveStyle({ backgroundColor: 'red' }); 29 | }); 30 | 31 | it('should render image', () => { 32 | const url = '//gw.alicdn.com/tfs/TB17TaySSzqK1RjSZFHXXb3CpXa-80-80.svg'; 33 | const { container } = render(image); 34 | const element = container.querySelector('.CardMedia'); 35 | 36 | expect(element).toHaveStyle({ backgroundImage: `url(${url})` }); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /src/components/Card/__tests__/text.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, cleanup } from '@testing-library/react'; 3 | import { CardText } from '..'; 4 | 5 | afterEach(cleanup); 6 | 7 | describe('', () => { 8 | it('should render CardText', () => { 9 | const { container } = render(myText); 10 | const element = container.querySelector('p'); 11 | 12 | expect(element).toBeInTheDocument(); 13 | }); 14 | 15 | it('should render with children', () => { 16 | const { getByText } = render( 17 | 18 | myText 19 | , 20 | ); 21 | const element = getByText('myText'); 22 | 23 | expect(element).toBeInTheDocument(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/components/Card/index.ts: -------------------------------------------------------------------------------- 1 | export { Card } from './Card'; 2 | export type { CardProps, CardSize } from './Card'; 3 | export { CardMedia } from './CardMedia'; 4 | export type { CardMediaProps } from './CardMedia'; 5 | export { CardContent } from './CardContent'; 6 | export type { CardContentProps } from './CardContent'; 7 | export { CardHeader } from './CardHeader'; 8 | export type { CardHeaderProps } from './CardHeader'; 9 | export { CardTitle } from './CardTitle'; 10 | export type { CardTitleProps } from './CardTitle'; 11 | export { CardText } from './CardText'; 12 | export type { CardTextProps } from './CardText'; 13 | export { CardActions } from './CardActions'; 14 | export type { CardActionsProps } from './CardActions'; 15 | -------------------------------------------------------------------------------- /src/components/Carousel/Item.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface CarouselItemProps { 4 | width: string; 5 | } 6 | 7 | export const CarouselItem: React.FC = (props) => { 8 | const { width, children } = props; 9 | 10 | return ( 11 |
12 | {children} 13 |
14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /src/components/Carousel/style.less: -------------------------------------------------------------------------------- 1 | .Carousel { 2 | position: relative; 3 | overflow: hidden; 4 | 5 | &--draggable { 6 | .Carousel-inner { 7 | touch-action: pan-y; 8 | cursor: grab; 9 | 10 | &:active { 11 | cursor: grabbing; 12 | } 13 | } 14 | } 15 | &--rtl { 16 | direction: rtl; 17 | } 18 | &--dragging { 19 | .Carousel-item { 20 | pointer-events: none; 21 | } 22 | } 23 | &-inner { 24 | display: flex; 25 | will-change: transform; 26 | } 27 | &-dots { 28 | position: absolute; 29 | z-index: 1; 30 | bottom: @carousel-dots-bottom; 31 | left: 50%; 32 | transform: translateX(-50%); 33 | display: flex; 34 | justify-content: center; 35 | list-style-type: none; 36 | margin: 0; 37 | padding: 0; 38 | } 39 | &-dot { 40 | display: block; 41 | width: @carousel-dot-width; 42 | height: @carousel-dot-height; 43 | margin: @carousel-dot-margin; 44 | padding: @carousel-dot-padding; 45 | border: @carousel-dot-border; 46 | border-radius: @carousel-dot-border-radius; 47 | background: var(--color-fill-2); 48 | transition: @carousel-dot-transition; 49 | cursor: pointer; 50 | 51 | &.active { 52 | background: var(--brand-1); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/components/Chat/style.less: -------------------------------------------------------------------------------- 1 | & when (@global-style = true) { 2 | html { 3 | height: 100vh; 4 | 5 | &[data-safari] { 6 | height: calc(100vh - calc(100vh - 100%)); 7 | } 8 | } 9 | 10 | body, 11 | #root { 12 | height: 100%; 13 | } 14 | 15 | body { 16 | overflow: hidden; 17 | margin: 0; 18 | } 19 | 20 | @media (hover: none) { 21 | body { 22 | user-select: none; 23 | -webkit-touch-callout: none; 24 | } 25 | } 26 | } 27 | 28 | .ChatApp { 29 | display: flex; 30 | flex-direction: column; 31 | height: 100%; 32 | background: var(--app-bg); 33 | color: @body-color; 34 | font-family: @font-family-base; 35 | line-height: @line-height-base; 36 | -webkit-tap-highlight-color: transparent; 37 | } 38 | 39 | .S--focusing { 40 | --safe-bottom: 0px; 41 | } 42 | 43 | // only for iOS 44 | @supports (-webkit-touch-callout: none) { 45 | .S--focusing { 46 | .MessageList { 47 | margin-top: 75vh; 48 | } 49 | } 50 | } 51 | 52 | .ChatFooter { 53 | position: relative; 54 | z-index: @zindex-footer; 55 | padding-bottom: var(--safe-bottom); 56 | background: var(--footer-bg); 57 | } 58 | 59 | @media (max-width: 374px) { 60 | :root { 61 | --msg-avatar-gap: 3Px; 62 | --rate-width: 24Px; 63 | } 64 | .MessageList { 65 | padding-left: 6px; 66 | padding-right: 6px; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/components/Checkbox/Checkbox.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import clsx from 'clsx'; 3 | 4 | export type CheckboxValue = string | number | undefined; 5 | 6 | export type CheckboxProps = React.InputHTMLAttributes & { 7 | value?: CheckboxValue; 8 | label?: CheckboxValue; 9 | }; 10 | 11 | export const Checkbox: React.FC = (props) => { 12 | const { className, label, checked, disabled, onChange, ...other } = props; 13 | return ( 14 | 31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /src/components/Checkbox/CheckboxGroup.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import clsx from 'clsx'; 3 | import { Checkbox, CheckboxProps, CheckboxValue } from './Checkbox'; 4 | 5 | export type CheckboxGroupProps = { 6 | className?: string; 7 | options: CheckboxProps[]; 8 | value: CheckboxValue[]; 9 | name?: string; 10 | disabled?: boolean; 11 | block?: boolean; 12 | onChange: (value: CheckboxValue[], event: React.ChangeEvent) => void; 13 | }; 14 | 15 | export const CheckboxGroup: React.FC = (props) => { 16 | const { className, options, value, name, disabled, block, onChange } = props; 17 | 18 | function handleChange(val: CheckboxValue, e: React.ChangeEvent) { 19 | const newValue = e.target.checked ? value.concat(val) : value.filter((item) => item !== val); 20 | onChange(newValue, e); 21 | } 22 | 23 | return ( 24 |
25 | {options.map((item) => ( 26 | { 33 | handleChange(item.value, e); 34 | }} 35 | key={item.value} 36 | /> 37 | ))} 38 |
39 | ); 40 | }; 41 | -------------------------------------------------------------------------------- /src/components/Checkbox/index.ts: -------------------------------------------------------------------------------- 1 | export { Checkbox } from './Checkbox'; 2 | export type { CheckboxProps, CheckboxValue } from './Checkbox'; 3 | export { CheckboxGroup } from './CheckboxGroup'; 4 | export type { CheckboxGroupProps } from './CheckboxGroup'; 5 | -------------------------------------------------------------------------------- /src/components/ClickOutside/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef } from 'react'; 2 | 3 | const doc = document; 4 | const html = doc.documentElement; 5 | 6 | export type ClickOutsideProps = { 7 | onClick: (event: React.MouseEvent) => void; 8 | // mouseEvent?: 'click' | 'mousedown' | 'mouseup' | false; 9 | mouseEvent?: 'click' | 'mousedown' | 'mouseup'; 10 | }; 11 | 12 | export const ClickOutside: React.FC = (props) => { 13 | const { children, onClick, mouseEvent = 'mouseup', ...others } = props; 14 | const wrapper = useRef(null!); 15 | 16 | function handleClick(e: any) { 17 | if (!wrapper.current) return; 18 | 19 | if (html.contains(e.target) && !wrapper.current.contains(e.target)) { 20 | onClick(e); 21 | } 22 | } 23 | 24 | useEffect(() => { 25 | if (mouseEvent) { 26 | doc.addEventListener(mouseEvent, handleClick); 27 | } 28 | return () => { 29 | doc.removeEventListener(mouseEvent, handleClick); 30 | }; 31 | }); 32 | 33 | return ( 34 |
35 | {children} 36 |
37 | ); 38 | }; 39 | -------------------------------------------------------------------------------- /src/components/ComponentsProvider/ComponentsContext.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ComponentsContextInterface } from './interface'; 3 | 4 | export const ComponentsContext = React.createContext({ 5 | addComponent: () => {}, 6 | hasComponent: () => false, 7 | getComponent: () => null, 8 | }); 9 | -------------------------------------------------------------------------------- /src/components/ComponentsProvider/useComponents.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ComponentsContext } from './ComponentsContext'; 3 | 4 | export function useComponents() { 5 | return React.useContext(ComponentsContext); 6 | } 7 | -------------------------------------------------------------------------------- /src/components/Composer/AccessoryWrap.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ClickOutside } from '../ClickOutside'; 3 | 4 | interface AccessoryWrapProps { 5 | onClickOutside: () => void; 6 | children: React.ReactNode; 7 | } 8 | 9 | export const AccessoryWrap = ({ onClickOutside, children }: AccessoryWrapProps) => ( 10 | {children} 11 | ); 12 | -------------------------------------------------------------------------------- /src/components/Composer/Action.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { IconButton, IconButtonProps } from '../IconButton'; 3 | 4 | export const Action = (props: IconButtonProps) => ( 5 |
6 | 7 |
8 | ); 9 | -------------------------------------------------------------------------------- /src/components/Composer/SendButton.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef } from 'react'; 2 | import { Button } from '../Button'; 3 | import { useLocale } from '../ConfigProvider'; 4 | 5 | interface SendButtonProps { 6 | disabled?: boolean; 7 | onClick: (e: React.MouseEvent) => void; 8 | } 9 | 10 | export const SendButton = ({ disabled, onClick }: SendButtonProps) => { 11 | const { trans } = useLocale('Composer'); 12 | const wrapRef = useRef(null); 13 | const btnRef = useRef(null); 14 | 15 | useEffect(() => { 16 | const wrap = wrapRef.current; 17 | const btn = btnRef.current; 18 | if (wrap && btn) { 19 | wrap.style.setProperty('--send-width', `${btn.offsetWidth}px`); 20 | } 21 | }, []) 22 | 23 | return ( 24 |
25 | 34 |
35 | ); 36 | }; 37 | -------------------------------------------------------------------------------- /src/components/Composer/ToolbarItem.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ToolbarItemProps } from '../Toolbar'; 3 | import { Action } from './Action'; 4 | 5 | type IToolbarItem = { 6 | item: ToolbarItemProps; 7 | onClick: (event: React.MouseEvent) => void; 8 | }; 9 | 10 | export const ToolbarItem: React.FC = (props) => { 11 | const { item, onClick } = props; 12 | 13 | return ( 14 | 22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /src/components/Composer/viewportTop.ts: -------------------------------------------------------------------------------- 1 | const rootEl = document.documentElement; 2 | let chatApp: HTMLElement | null; 3 | let requestID: number; 4 | let viewportTop = 0; 5 | 6 | export function setViewportTop(top: number) { 7 | cancelAnimationFrame(requestID); 8 | rootEl.style.setProperty('--viewport-top', `${top}px`); 9 | } 10 | 11 | export function updateViewportTop() { 12 | if (!chatApp) { 13 | chatApp = document.querySelector('.ChatApp'); 14 | } 15 | 16 | if (!chatApp) return; 17 | 18 | const { top } = chatApp.getBoundingClientRect(); 19 | 20 | if (top === 0) { 21 | requestID = requestAnimationFrame(updateViewportTop); 22 | } else { 23 | viewportTop = Math.abs(top); 24 | setViewportTop(viewportTop); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/components/ConfigProvider/locales/ar_EG.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | BackBottom: { 3 | newMsgOne: '{n} رسالة جديدة', 4 | newMsgOther: '{n} رسالة جديدة', 5 | bottom: 'الأسفل', 6 | }, 7 | Time: { 8 | weekdays: 'الأحد_الإثنين_الثلاثاء_الأربعاء_الخميس_الجمعة_السبت'.split('_'), 9 | formats: { 10 | LT: 'HH:mm', 11 | lll: 'YYYY/M/D HH:mm', 12 | WT: 'HH:mm dddd', 13 | YT: 'HH:mm أمس', 14 | }, 15 | }, 16 | Composer: { 17 | send: 'إرسال', 18 | }, 19 | SendConfirm: { 20 | title: 'إرسال صورة', 21 | send: 'أرسل', 22 | cancel: 'إلغاء', 23 | }, 24 | RateActions: { 25 | up: 'التصويت', 26 | down: 'تصويت سلبي', 27 | }, 28 | Recorder: { 29 | hold2talk: 'أستمر بالضغط لتتحدث', 30 | release2send: 'حرر للإرسال', 31 | releaseOrSwipe: 'حرر للإرسال ، اسحب لأعلى للإلغاء', 32 | release2cancel: 'حرر للإلغاء', 33 | }, 34 | Search: { 35 | search: 'يبحث', 36 | }, 37 | }; 38 | -------------------------------------------------------------------------------- /src/components/ConfigProvider/locales/en_US.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | BackBottom: { 3 | newMsgOne: '{n} new message', 4 | newMsgOther: '{n} new messages', 5 | bottom: 'Bottom', 6 | }, 7 | Time: { 8 | weekdays: 'Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday'.split('_'), 9 | formats: { 10 | LT: 'HH:mm', 11 | lll: 'M/D/YYYY HH:mm', 12 | WT: 'dddd HH:mm', 13 | YT: 'Yesterday HH:mm', 14 | }, 15 | }, 16 | Composer: { 17 | send: 'Send', 18 | }, 19 | SendConfirm: { 20 | title: 'Send photo', 21 | send: 'Send', 22 | cancel: 'Cancel', 23 | }, 24 | RateActions: { 25 | up: 'Up vote', 26 | down: 'Down vote', 27 | }, 28 | Recorder: { 29 | hold2talk: 'Hold to Talk', 30 | release2send: 'Release to Send', 31 | releaseOrSwipe: 'Release to send, swipe up to cancel', 32 | release2cancel: 'Release to cancel', 33 | }, 34 | Search: { 35 | search: 'Search', 36 | }, 37 | }; 38 | -------------------------------------------------------------------------------- /src/components/ConfigProvider/locales/fr_FR.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | BackBottom: { 3 | newMsgOne: '{n} nouveau message', 4 | newMsgOther: '{n} nouveau messages', 5 | bottom: 'Fond', 6 | }, 7 | Time: { 8 | weekdays: 'Dimanche_Lundi_Mardi_Mercredi_Jeudi_Vendredi_Samedi'.split('_'), 9 | formats: { 10 | LT: 'HH:mm', 11 | lll: 'D/M/YYYY HH:mm', 12 | WT: 'dddd HH:mm', 13 | YT: 'Hier HH:mm', 14 | }, 15 | }, 16 | Composer: { 17 | send: 'Envoyer', 18 | }, 19 | SendConfirm: { 20 | title: 'Envoyer une photo', 21 | send: 'Envoyer', 22 | cancel: 'Annuler', 23 | }, 24 | RateActions: { 25 | up: 'Voter pour', 26 | down: 'Vote négatif', 27 | }, 28 | Recorder: { 29 | hold2talk: 'Tenir pour parler', 30 | release2send: 'Libérer pour envoyer', 31 | releaseOrSwipe: 'Relâchez pour envoyer, balayez vers le haut pour annuler', 32 | release2cancel: 'Relâcher pour annuler', 33 | }, 34 | Search: { 35 | search: 'Chercher', 36 | }, 37 | }; 38 | -------------------------------------------------------------------------------- /src/components/ConfigProvider/locales/index.ts: -------------------------------------------------------------------------------- 1 | import arEG from './ar_EG'; 2 | import enUS from './en_US'; 3 | import frFR from './fr_FR'; 4 | import zhCN from './zh_CN'; 5 | 6 | export default { 7 | 'ar-EG': arEG, // 阿拉伯 8 | 'fr-FR': frFR, // 法语 9 | 'en-US': enUS, // 英语(美式) 10 | 'zh-CN': zhCN, // 简体中文 11 | } as Record; 12 | -------------------------------------------------------------------------------- /src/components/ConfigProvider/locales/zh_CN.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | BackBottom: { 3 | newMsgOne: '{n}条新消息', 4 | newMsgOther: '{n}条新消息', 5 | bottom: '回到底部', 6 | }, 7 | Time: { 8 | weekdays: '星期日_星期一_星期二_星期三_星期四_星期五_星期六'.split('_'), 9 | formats: { 10 | LT: 'HH:mm', // 00:32 11 | lll: 'YYYY年M月D日 HH:mm', // 2019年2月22日 00:28 12 | WT: 'dddd HH:mm', // 星期一 00:32 13 | YT: '昨天 HH:mm', // 昨天 00:32 14 | }, 15 | }, 16 | Composer: { 17 | send: '发送', 18 | }, 19 | SendConfirm: { 20 | title: '发送图片', 21 | send: '发送', 22 | cancel: '取消', 23 | }, 24 | RateActions: { 25 | up: '赞同', 26 | down: '反对', 27 | }, 28 | Recorder: { 29 | hold2talk: '按住 说话', 30 | release2send: '松开 发送', 31 | releaseOrSwipe: '松开发送,上滑取消', 32 | release2cancel: '松开手指,取消发送', 33 | }, 34 | Search: { 35 | search: '搜索', 36 | }, 37 | }; 38 | -------------------------------------------------------------------------------- /src/components/Divider/__tests__/index.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, cleanup } from '@testing-library/react'; 3 | import { Divider } from '..'; 4 | 5 | afterEach(cleanup); 6 | 7 | describe('', () => { 8 | it('should render the children', () => { 9 | const { getByTestId } = render( 10 | 11 | 12 | , 13 | ); 14 | const wrap = getByTestId('wrap'); 15 | const inside = getByTestId('inside'); 16 | 17 | expect(wrap).toContainElement(inside); 18 | }); 19 | 20 | it('should apply position class', () => { 21 | const { getByTestId } = render( 22 | 23 | testText 24 | , 25 | ); 26 | const wrap = getByTestId('wrap'); 27 | 28 | expect(wrap).toHaveClass('Divider--text-center'); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /src/components/Divider/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import clsx from 'clsx'; 3 | 4 | export type DividerProps = { 5 | className?: string; 6 | position?: 'center' | 'left' | 'right'; 7 | }; 8 | 9 | export const Divider: React.FC = (props) => { 10 | const { className, position = 'center', children, ...other } = props; 11 | return ( 12 |
17 | {children} 18 |
19 | ); 20 | }; 21 | -------------------------------------------------------------------------------- /src/components/Divider/style.less: -------------------------------------------------------------------------------- 1 | .Divider { 2 | display: flex; 3 | align-items: center; 4 | margin: 12px 0; 5 | font-size: @font-size-xs; 6 | color: var(--color-text-3); 7 | 8 | &:before, 9 | &:after { 10 | content: ''; 11 | display: block; 12 | flex: 1; 13 | border-top: 1px solid var(--color-line-1); 14 | 15 | @media (hover: none) { 16 | & { 17 | transform: scaleY(0.5); 18 | } 19 | } 20 | } 21 | } 22 | 23 | .Divider--text-center, 24 | .Divider--text-left, 25 | .Divider--text-right { 26 | &:before { 27 | margin-right: var(--gutter); 28 | } 29 | &:after { 30 | margin-left: var(--gutter); 31 | } 32 | } 33 | 34 | .Divider--text-left { 35 | &:before { 36 | max-width: 10%; 37 | } 38 | } 39 | 40 | .Divider--text-right { 41 | &:after { 42 | max-width: 10%; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/components/Empty/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import clsx from 'clsx'; 3 | import { Flex } from '../Flex'; 4 | 5 | export type EmptyProps = { 6 | className?: string; 7 | type?: 'error' | 'default'; 8 | image?: string; 9 | tip?: string; 10 | }; 11 | 12 | const IMAGE_EMPTY = 'https://gw.alicdn.com/imgextra/i3/O1CN01c0BqGH1Jx6L1ihheM_!!6000000001094-55-tps-280-280.svg'; 13 | const IMAGE_OOPS = 'https://gw.alicdn.com/imgextra/i3/O1CN011bYju01hGYK2LMydz_!!6000000004250-55-tps-280-280.svg'; 14 | 15 | export const Empty: React.FC = (props) => { 16 | const { className, type, image, tip, children } = props; 17 | const imgUrl = image || (type === 'error' ? IMAGE_OOPS : IMAGE_EMPTY); 18 | 19 | return ( 20 | 21 | {tip} 22 | {tip &&

{tip}

} 23 | {children} 24 |
25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /src/components/Empty/style.less: -------------------------------------------------------------------------------- 1 | .Empty { 2 | padding: 30px; 3 | text-align: center; 4 | } 5 | 6 | .Empty-img { 7 | height: 125px; 8 | } 9 | 10 | .Empty-tip { 11 | margin: 20px 0; 12 | color: var(--color-text-2); 13 | } 14 | -------------------------------------------------------------------------------- /src/components/FileCard/__tests__/index.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, cleanup } from '@testing-library/react'; 3 | import { FileCard } from '..'; 4 | 5 | afterEach(cleanup); 6 | 7 | const file = new File(['foo'], 'foo.txt', { type: 'text/plain' }); 8 | 9 | describe('', () => { 10 | it('should render the file', () => { 11 | const { container } = render(); 12 | 13 | expect(container.querySelector('.FileCard-ext')).toHaveTextContent('txt'); 14 | expect(container.querySelector('.FileCard-icon')).toHaveAttribute('data-type', 'txt'); 15 | expect(container.querySelector('.FileCard-name')).toHaveTextContent('foo.txt'); 16 | expect(container.querySelector('.FileCard-size')).toHaveTextContent('3 B'); 17 | }); 18 | 19 | it('should apply the extension', () => { 20 | const { container } = render(); 21 | 22 | expect(container.querySelector('.FileCard-ext')).toHaveTextContent('jpg'); 23 | expect(container.querySelector('.FileCard-icon')).toHaveAttribute('data-type', 'jpg'); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/components/FileCard/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import clsx from 'clsx'; 3 | import { Card } from '../Card'; 4 | import { Flex, FlexItem } from '../Flex'; 5 | import { Icon } from '../Icon'; 6 | import { Text } from '../Text'; 7 | import getExtName from '../../utils/getExtName'; 8 | import prettyBytes from '../../utils/prettyBytes'; 9 | 10 | export interface FileCardProps { 11 | className?: string; 12 | file: File; 13 | extension?: string; 14 | } 15 | 16 | export const FileCard: React.FC = (props) => { 17 | const { className, file, extension, children } = props; 18 | const { name, size } = file; 19 | const ext = extension || getExtName(name); 20 | 21 | return ( 22 | 23 | 24 |
25 | 26 | 27 | {ext} 28 | 29 |
30 | 31 | 32 | {name} 33 | 34 |
35 | {size != null && {prettyBytes(size)}} 36 | {children} 37 |
38 |
39 |
40 |
41 | ); 42 | }; 43 | -------------------------------------------------------------------------------- /src/components/FileCard/style.less: -------------------------------------------------------------------------------- 1 | .FileCard { 2 | padding: 8px; 3 | } 4 | 5 | .FileCard-icon { 6 | position: relative; 7 | height: 60px; 8 | margin-right: 8px; 9 | color: var(--color-text-2); 10 | 11 | &[data-type='pdf'] { 12 | color: var(--red); 13 | } 14 | &[data-type*='doc'] { 15 | color: var(--blue); 16 | } 17 | &[data-type*='ppt'], 18 | &[data-type='key'] { 19 | color: var(--orange); 20 | } 21 | &[data-type*='xls'] { 22 | color: var(--green); 23 | } 24 | &[data-type='rar'], 25 | &[data-type='zip'] { 26 | color: var(--brand-1); 27 | } 28 | .Icon { 29 | font-size: 60px; 30 | } 31 | } 32 | 33 | .FileCard-name { 34 | height: 38px; 35 | margin-bottom: 4px; 36 | line-height: 1.4; 37 | } 38 | 39 | .FileCard-ext { 40 | position: absolute; 41 | left: 20px; 42 | bottom: 15px; 43 | transform-origin: left bottom; 44 | transform: scale(0.5); 45 | max-width: 50px; 46 | font-size: @font-size-md; 47 | font-weight: 700; 48 | text-transform: uppercase; 49 | } 50 | 51 | .FileCard-meta { 52 | color: var(--color-text-3); 53 | font-size: @font-size-xs; 54 | 55 | & > a, 56 | & > span { 57 | margin-right: 10px; 58 | } 59 | a { 60 | color: var(--link-color); 61 | text-decoration: none; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/components/Flex/FlexItem.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import clsx from 'clsx'; 3 | 4 | export interface FlexItemProps extends React.HTMLAttributes { 5 | className?: string; 6 | flex?: string; 7 | alignSelf?: 'auto' | 'flex-start' | 'flex-end' | 'center' | 'baseline' | 'stretch'; 8 | order?: number; 9 | } 10 | 11 | export const FlexItem: React.FC = (props) => { 12 | const { className, flex, alignSelf, order, style, children, ...other } = props; 13 | return ( 14 |
24 | {children} 25 |
26 | ); 27 | }; 28 | -------------------------------------------------------------------------------- /src/components/Flex/index.ts: -------------------------------------------------------------------------------- 1 | export { Flex } from './Flex'; 2 | export type { FlexProps } from './Flex'; 3 | export { FlexItem } from './FlexItem'; 4 | export type { FlexItemProps } from './FlexItem'; 5 | -------------------------------------------------------------------------------- /src/components/Flex/style.less: -------------------------------------------------------------------------------- 1 | .Flex { 2 | display: flex; 3 | } 4 | 5 | .Flex--inline { 6 | display: inline-flex; 7 | } 8 | 9 | .Flex--center { 10 | justify-content: center; 11 | align-items: center; 12 | } 13 | 14 | .Flex--d-r { 15 | flex-direction: row; 16 | } 17 | 18 | .Flex--d-rr { 19 | flex-direction: row-reverse; 20 | } 21 | 22 | .Flex--d-c { 23 | flex-direction: column; 24 | } 25 | 26 | .Flex--d-cr { 27 | flex-direction: column-reverse; 28 | } 29 | 30 | .Flex--w-n { 31 | flex-wrap: nowrap; 32 | } 33 | 34 | .Flex--w-w { 35 | flex-wrap: wrap; 36 | } 37 | 38 | .Flex--w-wr { 39 | flex-wrap: wrap-reverse; 40 | } 41 | 42 | .Flex--jc-fs { 43 | justify-content: flex-start; 44 | } 45 | 46 | .Flex--jc-fe { 47 | justify-content: flex-end; 48 | } 49 | 50 | .Flex--jc-c { 51 | justify-content: center; 52 | } 53 | 54 | .Flex--jc-sb { 55 | justify-content: space-between; 56 | } 57 | 58 | .Flex--jc-sa { 59 | justify-content: space-around; 60 | } 61 | 62 | .Flex--ai-fs { 63 | align-items: flex-start; 64 | } 65 | 66 | .Flex--ai-fe { 67 | align-items: flex-end; 68 | } 69 | 70 | .Flex--ai-c { 71 | align-items: center; 72 | } 73 | 74 | .FlexItem { 75 | flex: 1; 76 | min-width: 0; 77 | min-height: 0; 78 | } 79 | -------------------------------------------------------------------------------- /src/components/Form/Form.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import clsx from 'clsx'; 3 | 4 | export type FormProps = { 5 | className?: string; 6 | /** @deprecated Use ``'s `variant` instead */ 7 | theme?: string; 8 | }; 9 | 10 | export const ThemeContext = React.createContext(''); 11 | 12 | export const Form: React.FC = (props) => { 13 | const { className, theme = '', children, ...other } = props; 14 | return ( 15 | 16 |
17 | {children} 18 |
19 |
20 | ); 21 | }; 22 | -------------------------------------------------------------------------------- /src/components/Form/FormActions.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import clsx from 'clsx'; 3 | 4 | export const FormActions: React.FC = (props) => { 5 | const { children, ...other } = props; 6 | return ( 7 |
8 | {children} 9 |
10 | ); 11 | }; 12 | -------------------------------------------------------------------------------- /src/components/Form/FormItem.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import clsx from 'clsx'; 3 | import { Label } from '../Label'; 4 | import { HelpText } from '../HelpText'; 5 | 6 | export type FormItemProps = { 7 | label?: string | React.ReactNode; 8 | help?: string; 9 | required?: boolean; 10 | invalid?: boolean; 11 | hidden?: boolean; 12 | }; 13 | 14 | export const FormItem: React.FC = (props) => { 15 | const { label, help, required, invalid, hidden, children } = props; 16 | return ( 17 | 22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /src/components/Form/__tests__/actions.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, cleanup } from '@testing-library/react'; 3 | import { FormActions } from '..'; 4 | 5 | afterEach(cleanup); 6 | 7 | describe('', () => { 8 | it('should render a form actions', () => { 9 | const { getByTestId } = render(); 10 | const formActions = getByTestId('formActions'); 11 | 12 | expect(formActions).toHaveClass('FormActions'); 13 | }); 14 | 15 | it('should render children', () => { 16 | const { getByTestId } = render( 17 | 18 | testChild 19 | , 20 | ); 21 | const formActions = getByTestId('formActions'); 22 | 23 | expect(formActions).toContainHTML('testChild'); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/components/Form/__tests__/index.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, cleanup } from '@testing-library/react'; 3 | import { Form } from '..'; 4 | 5 | afterEach(cleanup); 6 | 7 | describe('
', () => { 8 | it('should render a form', () => { 9 | const { getByTestId } = render(); 10 | const form = getByTestId('form'); 11 | 12 | expect(form).toBeInTheDocument(); 13 | }); 14 | 15 | it('should render a form (light)', () => { 16 | const { getByTestId } = render(); 17 | const form = getByTestId('form'); 18 | 19 | expect(form).toHaveClass('is-light'); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /src/components/Form/index.ts: -------------------------------------------------------------------------------- 1 | export { Form, ThemeContext } from './Form'; 2 | export type { FormProps } from './Form'; 3 | export { FormItem } from './FormItem'; 4 | export type { FormItemProps } from './FormItem'; 5 | export { FormActions } from './FormActions'; 6 | -------------------------------------------------------------------------------- /src/components/Form/style.less: -------------------------------------------------------------------------------- 1 | .Form { 2 | background: var(--color-fill-1); 3 | 4 | &.is-light { 5 | // background: var(--gray-7); 6 | 7 | .FormItem { 8 | padding: 0; 9 | } 10 | .Label, 11 | .HelpText { 12 | padding: 0 var(--gutter); 13 | } 14 | } 15 | } 16 | 17 | .FormItem { 18 | position: relative; 19 | padding: 0 var(--gutter); 20 | 21 | & + & { 22 | margin-top: 20px; 23 | } 24 | &.required { 25 | .Label:after { 26 | content: '*'; 27 | display: inline-block; 28 | color: var(--red); 29 | font-size: @font-size-sm; 30 | font-family: SimSun,sans-serif; 31 | line-height: 1; 32 | vertical-align: middle; 33 | } 34 | } 35 | &.is-invalid { 36 | .Label, 37 | .HelpText { 38 | color: var(--red); 39 | } 40 | .Input { 41 | border-color: var(--red); 42 | } 43 | } 44 | .RadioGroup, 45 | .CheckboxGroup { 46 | margin-top: 10px; 47 | } 48 | .Label + .Input { 49 | margin-top: 5px; 50 | } 51 | } 52 | 53 | .FormActions { 54 | display: flex; 55 | padding: 10px var(--gutter); 56 | background: var(--color-fill-1); 57 | 58 | .Btn { 59 | flex: 1; 60 | } 61 | .Btn + .Btn { 62 | margin-left: 6px; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/components/HelpText/__tests__/index.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, cleanup } from '@testing-library/react'; 3 | import { HelpText } from '..'; 4 | 5 | afterEach(cleanup); 6 | 7 | describe('', () => { 8 | it('should render the children', () => { 9 | const { container, getByTestId } = render( 10 | 11 | 12 | , 13 | ); 14 | const wrap = container.querySelector('.HelpText'); 15 | const inside = getByTestId('inside'); 16 | 17 | expect(wrap).toContainElement(inside); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /src/components/HelpText/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const HelpText: React.FC = (props) => { 4 | const { children, ...others } = props; 5 | return ( 6 |
7 | {children} 8 |
9 | ); 10 | }; 11 | -------------------------------------------------------------------------------- /src/components/HelpText/style.less: -------------------------------------------------------------------------------- 1 | .HelpText { 2 | font-size: @font-size-xs; 3 | color: var(--color-text-2); 4 | } 5 | -------------------------------------------------------------------------------- /src/components/Icon/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import clsx from 'clsx'; 3 | 4 | export type IconProps = React.SVGProps & { 5 | type: string; 6 | className?: string; 7 | name?: string; 8 | spin?: boolean; 9 | }; 10 | 11 | export const Icon: React.FC = (props) => { 12 | const { type, className, spin, name, ...other } = props; 13 | const ariaProps = typeof name === 'string' ? { 'aria-label': name } : { 'aria-hidden': true }; 14 | 15 | return ( 16 | 17 | 18 | 19 | ); 20 | }; 21 | -------------------------------------------------------------------------------- /src/components/Icon/style.less: -------------------------------------------------------------------------------- 1 | .Icon { 2 | display: inline-block; 3 | width: 1em; 4 | height: 1em; 5 | stroke-width: 0; 6 | fill: currentColor; 7 | transition: all 0.3s cubic-bezier(0.18, 0.89, 0.32, 1.28); 8 | } 9 | 10 | .is-spin { 11 | animation: spin 1s infinite linear; 12 | } 13 | 14 | @keyframes spin { 15 | 0% { 16 | transform: rotate(0deg); 17 | } 18 | to { 19 | transform: rotate(1turn); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/components/IconButton/__tests__/index.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, cleanup } from '@testing-library/react'; 3 | import { IconButton } from '..'; 4 | 5 | afterEach(cleanup); 6 | 7 | describe('', () => { 8 | it('should render the icon', () => { 9 | const { getByTestId } = render(); 10 | const btn = getByTestId('btn'); 11 | 12 | expect(btn?.querySelector('use')).toHaveAttribute('xlink:href', '#icon-foo'); 13 | }); 14 | 15 | it('should have a custom className', () => { 16 | const { getByTestId } = render( 17 | , 18 | ); 19 | const btn = getByTestId('btn'); 20 | 21 | expect(btn).toHaveClass('testName'); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/components/IconButton/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import clsx from 'clsx'; 3 | import { Button, ButtonProps } from '../Button'; 4 | import { Icon } from '../Icon'; 5 | 6 | export interface IconButtonProps extends ButtonProps { 7 | img?: string; 8 | } 9 | 10 | export const IconButton: React.FC = (props) => { 11 | const { className, icon, img, ...other } = props; 12 | return ( 13 | 17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /src/components/IconButton/style.less: -------------------------------------------------------------------------------- 1 | .IconBtn { 2 | min-width: 0; 3 | padding: 0; 4 | border: 0; 5 | border-radius: @icon-button-border-radius; 6 | background: @icon-button-bg; 7 | color: @icon-button-color; 8 | font-size: @icon-button-size; 9 | 10 | &.Btn--primary { 11 | color: @icon-button-primary-color; 12 | } 13 | &.Btn--lg { 14 | border-radius: @icon-button-lg-border-radius; 15 | font-size: @icon-button-lg-size; 16 | } 17 | & > .Icon { 18 | display: block; 19 | } 20 | & > img { 21 | display: block; 22 | width: 1em; 23 | height: 1em; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/components/Image/style.less: -------------------------------------------------------------------------------- 1 | .Image { 2 | position: relative; 3 | display: inline-block; 4 | overflow: hidden; 5 | border-style: none; 6 | } 7 | 8 | .Image--fluid { 9 | max-width: 100%; 10 | height: auto; 11 | } 12 | -------------------------------------------------------------------------------- /src/components/InfiniteScroll/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import clsx from 'clsx'; 3 | import useForwardRef from '../../hooks/useForwardRef'; 4 | import getToBottom from '../../utils/getToBottom'; 5 | 6 | export interface InfiniteScrollProps extends React.HTMLAttributes { 7 | className?: string; 8 | disabled?: boolean; 9 | distance?: number; 10 | onLoadMore: () => void; 11 | } 12 | 13 | export const InfiniteScroll = React.forwardRef( 14 | (props, ref) => { 15 | const { className, disabled, distance = 0, children, onLoadMore, onScroll, ...other } = props; 16 | const wrapperRef = useForwardRef(ref); 17 | 18 | function handleScroll(e: React.UIEvent) { 19 | if (onScroll) { 20 | onScroll(e); 21 | } 22 | 23 | const el = wrapperRef.current; 24 | if (!el) return; 25 | 26 | const nearBottom = getToBottom(el) <= distance; 27 | 28 | if (nearBottom) { 29 | onLoadMore(); 30 | } 31 | } 32 | 33 | return ( 34 |
41 | {children} 42 |
43 | ); 44 | }, 45 | ); 46 | -------------------------------------------------------------------------------- /src/components/InfiniteScroll/style.less: -------------------------------------------------------------------------------- 1 | .InfiniteScroll { 2 | overflow-y: scroll; 3 | -webkit-overflow-scrolling: touch; 4 | } 5 | -------------------------------------------------------------------------------- /src/components/Label/__tests__/index.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, cleanup } from '@testing-library/react'; 3 | import { Label } from '..'; 4 | 5 | afterEach(cleanup); 6 | 7 | describe('