├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .github └── release.yml ├── .gitignore ├── .npmrc ├── .prettierignore ├── .prettierrc.js ├── .stylelintignore ├── .stylelintrc.js ├── .yarnrc ├── LICENSE ├── README-zh_CN.md ├── README.md ├── TODO.md ├── babel.config.js ├── build ├── build-components.js ├── build-css-non-rem.js ├── build-css-var.js ├── build-entry.js ├── build-icons.js ├── build-lib.js ├── build-mkdir.js ├── build-style-entry.js ├── build-style.js ├── utils │ ├── color-palette │ │ ├── index.js │ │ ├── tinycolor.js │ │ └── util.js │ ├── cssVariable.json │ ├── less2json.js │ └── lessConfig.js ├── version.js ├── webpack.base.js ├── webpack.build.js └── webpack.site.dev.js ├── docs ├── config.ts ├── mobile │ ├── App.tsx │ ├── components │ │ ├── demo-title │ │ │ ├── index.less │ │ │ └── index.tsx │ │ ├── mobile-card │ │ │ ├── index.less │ │ │ └── index.tsx │ │ ├── mobile-header │ │ │ └── index.tsx │ │ └── nav-menu.tsx │ ├── index.tsx │ ├── routes.ts │ ├── style │ │ └── index.less │ ├── template.html │ └── tracking.tsx ├── static │ ├── code.svg │ ├── i18n.svg │ ├── logo.svg │ ├── menu_icons │ │ ├── brand.svg │ │ ├── brand_active.svg │ │ ├── changelog.svg │ │ ├── changelog_active.svg │ │ ├── comp.svg │ │ ├── comp_active.svg │ │ ├── i18n.svg │ │ ├── i18n_active.svg │ │ ├── intro.svg │ │ ├── intro_active.svg │ │ ├── quickstart.svg │ │ ├── quickstart_active.svg │ │ ├── theme.svg │ │ └── theme_active.svg │ ├── pointer.svg │ ├── send.svg │ └── theme.svg ├── type.ts └── utils │ └── index.ts ├── package.json ├── postcss.config.js ├── src ├── common │ ├── popup │ │ ├── context.tsx │ │ ├── index.tsx │ │ └── overlay.tsx │ └── touch.tsx ├── components │ ├── action-sheet │ │ ├── action-sheet-item.tsx │ │ ├── action-sheet.tsx │ │ ├── createActionSheet.tsx │ │ ├── demo │ │ │ └── index.tsx │ │ ├── index.less │ │ └── index.tsx │ ├── badge │ │ ├── demo │ │ │ ├── index.less │ │ │ └── index.tsx │ │ ├── index.less │ │ └── index.tsx │ ├── button │ │ ├── demo │ │ │ ├── index.less │ │ │ └── index.tsx │ │ ├── index.less │ │ └── index.tsx │ ├── calendar │ │ ├── demo │ │ │ └── index.tsx │ │ ├── index.less │ │ └── index.tsx │ ├── cascader-sliding │ │ ├── index.less │ │ └── index.tsx │ ├── cascader │ │ ├── demo │ │ │ └── index.tsx │ │ ├── index.less │ │ └── index.tsx │ ├── cell │ │ ├── demo │ │ │ ├── index.less │ │ │ └── index.tsx │ │ ├── index.less │ │ └── index.tsx │ ├── checkbox │ │ ├── checkbox.tsx │ │ ├── demo │ │ │ ├── index.less │ │ │ └── index.tsx │ │ ├── group.tsx │ │ ├── index.less │ │ ├── index.tsx │ │ └── types.tsx │ ├── col │ │ ├── index.less │ │ └── index.tsx │ ├── collapse │ │ ├── demo │ │ │ ├── index.less │ │ │ └── index.tsx │ │ ├── index.less │ │ └── index.tsx │ ├── date-time-picker │ │ ├── demo │ │ │ └── index.tsx │ │ ├── index.less │ │ └── index.tsx │ ├── dialog │ │ ├── demo │ │ │ └── index.tsx │ │ ├── index.less │ │ └── index.tsx │ ├── dropdown-item │ │ ├── index.less │ │ └── index.tsx │ ├── dropdown │ │ ├── demo │ │ │ ├── index.less │ │ │ └── index.tsx │ │ ├── index.less │ │ └── index.tsx │ ├── field │ │ ├── demo │ │ │ └── index.tsx │ │ ├── index.less │ │ └── index.tsx │ ├── header-tab │ │ ├── index.less │ │ └── index.tsx │ ├── header-tabs │ │ ├── index.less │ │ └── index.tsx │ ├── header │ │ ├── demo │ │ │ ├── index.less │ │ │ └── index.tsx │ │ ├── index.less │ │ └── index.tsx │ ├── icon │ │ ├── all.ts │ │ ├── index.less │ │ ├── index.tsx │ │ └── svg │ │ │ ├── arrow-down.svg │ │ │ ├── arrow-left.svg │ │ │ ├── arrow-right.svg │ │ │ ├── arrow-up.svg │ │ │ ├── attention.svg │ │ │ ├── check-one.svg │ │ │ ├── check.svg │ │ │ ├── circle.svg │ │ │ ├── close-one.svg │ │ │ ├── close.svg │ │ │ ├── default.svg │ │ │ ├── error.svg │ │ │ ├── filter.svg │ │ │ ├── info.svg │ │ │ ├── loading.svg │ │ │ ├── partial-check.svg │ │ │ ├── question-mark.svg │ │ │ ├── radio-uncheck.svg │ │ │ ├── radio.svg │ │ │ ├── square-check-one.svg │ │ │ ├── square-uncheck.svg │ │ │ ├── success.svg │ │ │ ├── triangular.svg │ │ │ └── warning.svg │ ├── image-preview │ │ ├── demo │ │ │ └── index.tsx │ │ ├── index.less │ │ └── index.tsx │ ├── infinite-scroll │ │ ├── demo │ │ │ ├── index.less │ │ │ └── index.tsx │ │ ├── index.less │ │ └── index.tsx │ ├── loading │ │ ├── demo │ │ │ └── index.tsx │ │ ├── icon │ │ │ ├── circle.svg │ │ │ └── default.svg │ │ ├── index.less │ │ └── index.tsx │ ├── locale-context │ │ └── index.tsx │ ├── modal │ │ ├── confirm.tsx │ │ ├── demo │ │ │ └── index.tsx │ │ ├── index.tsx │ │ └── modal-dialog.tsx │ ├── notice-bar │ │ ├── demo │ │ │ ├── index.less │ │ │ └── index.tsx │ │ ├── index.less │ │ └── index.tsx │ ├── overlay │ │ ├── index.less │ │ └── index.tsx │ ├── picker-column │ │ ├── demo │ │ │ └── index.tsx │ │ ├── index.less │ │ └── index.tsx │ ├── picker │ │ ├── demo │ │ │ └── index.tsx │ │ ├── index.less │ │ └── index.tsx │ ├── popup-drill-down-item │ │ └── index.tsx │ ├── popup-drill-down │ │ ├── context.ts │ │ ├── demo │ │ │ ├── index.less │ │ │ └── index.tsx │ │ ├── index.less │ │ └── index.tsx │ ├── popup │ │ ├── demo │ │ │ ├── index.less │ │ │ └── index.tsx │ │ ├── index.less │ │ └── index.tsx │ ├── progress │ │ ├── demo │ │ │ ├── index.less │ │ │ └── index.tsx │ │ ├── index.less │ │ └── index.tsx │ ├── pull-refresh │ │ ├── demo │ │ │ ├── index.less │ │ │ └── index.tsx │ │ ├── index.less │ │ └── index.tsx │ ├── radio │ │ ├── demo │ │ │ ├── index.less │ │ │ └── index.tsx │ │ ├── group.tsx │ │ ├── icon.tsx │ │ ├── index.less │ │ ├── index.tsx │ │ ├── radio.tsx │ │ └── types.tsx │ ├── row │ │ ├── RowContext.tsx │ │ ├── demo │ │ │ ├── index.less │ │ │ └── index.tsx │ │ ├── index.less │ │ └── index.tsx │ ├── search │ │ ├── demo │ │ │ └── index.tsx │ │ ├── index.less │ │ └── index.tsx │ ├── slider │ │ ├── demo │ │ │ └── index.tsx │ │ ├── index.less │ │ └── index.tsx │ ├── sticky │ │ ├── demo │ │ │ ├── index.less │ │ │ └── index.tsx │ │ ├── index.less │ │ └── index.tsx │ ├── swipe-cell │ │ ├── demo │ │ │ ├── index.less │ │ │ └── index.tsx │ │ ├── index.less │ │ └── index.tsx │ ├── swipe │ │ ├── demo │ │ │ ├── index.less │ │ │ └── index.tsx │ │ ├── index.less │ │ └── index.tsx │ ├── switch │ │ ├── demo │ │ │ └── index.tsx │ │ ├── index.less │ │ └── index.tsx │ ├── tab │ │ ├── index.less │ │ └── index.tsx │ ├── table │ │ ├── algorithm.ts │ │ ├── demo │ │ │ ├── index.less │ │ │ └── index.tsx │ │ ├── empty-content.tsx │ │ ├── index.less │ │ ├── index.tsx │ │ ├── indicator.tsx │ │ ├── sort-icon.tsx │ │ └── types.ts │ ├── tabs │ │ ├── demo │ │ │ ├── index.less │ │ │ └── index.tsx │ │ ├── index.less │ │ ├── index.tsx │ │ └── utils.ts │ ├── tag │ │ ├── demo │ │ │ ├── index.less │ │ │ └── index.tsx │ │ ├── index.less │ │ └── index.tsx │ ├── toast │ │ ├── Toast.tsx │ │ ├── demo │ │ │ └── index.tsx │ │ ├── icon │ │ │ ├── error.svg │ │ │ ├── loading.svg │ │ │ ├── success.svg │ │ │ └── warning.svg │ │ ├── index.less │ │ └── index.tsx │ ├── toolbar │ │ ├── index.less │ │ └── index.tsx │ └── tree │ │ ├── demo │ │ └── index.tsx │ │ ├── index.less │ │ └── index.tsx ├── hooks │ ├── callback │ │ ├── const-callback.ts │ │ ├── index.ts │ │ └── ref-callback.ts │ ├── click │ │ └── index.ts │ ├── const.ts │ ├── dom │ │ ├── index.ts │ │ └── resize.ts │ ├── drag │ │ └── index.ts │ ├── event.ts │ ├── index.ts │ ├── input │ │ ├── composition-input.ts │ │ └── index.ts │ ├── state │ │ ├── controlled.ts │ │ ├── index.ts │ │ └── record.ts │ ├── timer.ts │ └── touch │ │ ├── index.ts │ │ └── touch.ts ├── icon │ ├── arrow-left.svg │ ├── arrow-right.svg │ ├── attention.svg │ ├── check-one.svg │ ├── check.svg │ ├── close-one.svg │ ├── close.svg │ └── info.svg ├── index.less ├── index.tsx ├── locale │ ├── en-US.ts │ ├── ja.ts │ └── zh-CN.ts ├── props-type.ts ├── public-api │ └── index.ts ├── style │ ├── animation.less │ ├── color-palette │ │ ├── palette.less │ │ └── tinyColor.less │ ├── common.less │ ├── compat.less │ ├── hairline.less │ ├── index.less │ ├── mixins.less │ ├── mixins │ │ ├── hairline.less │ │ ├── radius.less │ │ └── shadow.less │ ├── rem.less │ ├── reset.less │ ├── root.less │ ├── var.config.json │ └── var.less └── utils │ ├── const.ts │ ├── create │ ├── bem.ts │ ├── createBEM.ts │ ├── i18n.ts │ └── index.ts │ ├── dom │ ├── event.ts │ ├── raf.ts │ ├── scroll.ts │ └── unit.js │ ├── format │ ├── date.ts │ └── string.ts │ ├── index.ts │ ├── math.ts │ ├── motion.ts │ ├── struct │ ├── array.ts │ └── tree.ts │ └── types.ts ├── ssr ├── .npmrc ├── .yarnrc ├── build │ ├── webpack.client.js │ └── webpack.server.js ├── package.json ├── src │ ├── client │ │ ├── App.tsx │ │ ├── index.less │ │ └── index.tsx │ └── server │ │ └── index.tsx ├── tsconfig.json └── yarn.lock ├── tsconfig.json ├── typings └── index.d.ts ├── workflows └── build.yml └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.md] 11 | trim_trailing_whitespace = false 12 | 13 | [*.ts] 14 | indent_size = 2 15 | 16 | [*.tsx] 17 | indent_size = 2 18 | 19 | [*.js] 20 | indent_size = 2 21 | 22 | [*.jsx] 23 | indent_size = 2 24 | 25 | [*.css] 26 | indent_size = 2 27 | 28 | [Makefile] 29 | indent_style = tab 30 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # /node_modules/* and /bower_components/* in the project root are ignored by default 2 | 3 | # Ignore build files 4 | build/* 5 | es/* 6 | lib/* 7 | ssr/** 8 | babel.config.js -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['eslint:recommended', 'plugin:prettier/recommended'], 3 | env: { 4 | browser: true, 5 | commonjs: true, 6 | es6: true, 7 | }, 8 | parserOptions: { 9 | ecmaFeatures: { 10 | experimentalObjectRestSpread: true, 11 | }, 12 | sourceType: 'module', 13 | }, 14 | rules: { 15 | indent: ['error', 2], 16 | 'linebreak-style': ['error', 'unix'], 17 | quotes: ['error', 'single'], 18 | semi: 'off', 19 | 'react/prop-types': [2, { ignore: ['children'] }], 20 | }, 21 | settings: { 22 | react: { 23 | version: 'detect', 24 | }, 25 | }, 26 | overrides: [ 27 | { 28 | files: ['*.tsx', '*.ts'], 29 | parserOptions: { 30 | ecmaFeatures: { 31 | jsx: true, 32 | }, 33 | }, 34 | extends: [ 35 | 'eslint:recommended', 36 | 'plugin:@typescript-eslint/recommended', 37 | // https://www.npmjs.com/package/@typescript-eslint/eslint-plugin 38 | // "plugin:@typescript-eslint/recommended-requiring-type-checking" 39 | 'plugin:react/recommended', 40 | 'plugin:prettier/recommended', 41 | ], 42 | plugins: ['@typescript-eslint', 'react', 'react-hooks'], 43 | rules: { 44 | '@typescript-eslint/no-explicit-any': 0, 45 | '@typescript-eslint/ban-ts-ignore': 0, 46 | 'react-hooks/rules-of-hooks': 'error', 47 | '@typescript-eslint/no-non-null-assertion': 0, 48 | '@typescript-eslint/no-empty-function': 0, 49 | 'react-hooks/exhaustive-deps': 'warn', 50 | }, 51 | }, 52 | ], 53 | }; 54 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | # .github/release.yml 2 | 3 | changelog: 4 | exclude: 5 | labels: 6 | - ignore-for-release 7 | authors: 8 | - octocat 9 | categories: 10 | - title: Breaking Changes 🛠 11 | labels: 12 | - Semver-Major 13 | - breaking-change 14 | - title: Exciting New Features 🎉 15 | labels: 16 | - Semver-Minor 17 | - enhancement 18 | - title: Other Changes 19 | labels: 20 | - "*" 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log* 2 | .cache 3 | .DS_Store 4 | .history 5 | .idea 6 | .vscode 7 | 8 | # npm 9 | node_modules 10 | package-lock.json 11 | 12 | # dist file 13 | es 14 | lib 15 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry= https://registry.npmjs.org -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Ignore artifacts: 2 | build 3 | ssr 4 | es 5 | lib 6 | babel.config.js -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 100, 3 | tabWidth: 2, 4 | useTabs: false, 5 | semi: true, 6 | singleQuote: true, 7 | jsxSingleQuote: false, 8 | trailingComma: 'all', 9 | bracketSpacing: true, 10 | rangeStart: 0, 11 | rangeEnd: Infinity, 12 | proseWrap: 'preserve', 13 | arrowParens: 'avoid', 14 | htmlWhitespaceSensitivity: 'css', 15 | endOfLine: 'lf', 16 | quoteProps: 'as-needed', 17 | vueIndentScriptAndStyle: false, 18 | }; 19 | -------------------------------------------------------------------------------- /.stylelintignore: -------------------------------------------------------------------------------- 1 | # npm 2 | node_modules 3 | 4 | # src/style 5 | src/style/color-palette 6 | src/style/compat.less -------------------------------------------------------------------------------- /.stylelintrc.js: -------------------------------------------------------------------------------- 1 | /** 2 | * We give up using stylelint-prettier. Because stylelint-prettier just runs Prettier. 3 | * And Prettier always lowercasing units. It will transfer 'PX' to 'px', then px2rem will be down. 4 | * https://github.com/stylelint/stylelint/issues/4048 5 | * Comparing star number of eslint-plugin-prettier and styleling-prettier, stylelint-prettier is not valued. 6 | */ 7 | module.exports = { 8 | "extends": [ 9 | "stylelint-config-standard", 10 | "stylelint-config-rational-order" 11 | ], 12 | "rules": { 13 | "order/properties-order": [], 14 | "at-rule-no-unknown": null, 15 | "number-leading-zero": null, 16 | "function-calc-no-invalid": null, 17 | "no-descending-specificity": null, 18 | "declaration-colon-newline-after": null, 19 | "font-family-no-missing-generic-family-keyword": null, 20 | "value-keyword-case": null, 21 | "unit-case": null 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.yarnrc: -------------------------------------------------------------------------------- 1 | "registry" "https://registry.yarnpkg.com" -------------------------------------------------------------------------------- /README-zh_CN.md: -------------------------------------------------------------------------------- 1 |

2 |

3 | logo 4 |

5 | 6 |

OKeeDesign Mobile React

7 | 8 |

OKeeDesign Mobile React 是基于 OKeeDesign 设计体系的移动端组件库。

9 | 10 |

11 | npm version 12 | CI Status 13 | Coverage Status 14 | min size 15 |

16 | 17 | [English](./README.md) | 简体中文 18 | 19 | ## 特性 20 | 21 | * 提供 36 个高质量组件,覆盖移动端各类场景 22 | * 支持国际化,支持 3 种语言 23 | * 支持TypeScript 24 | * 支持主题定制 25 | * 支持按需引入 26 | 27 | ## 快速预览 28 | 29 | 了解更多信息,请参考[快速上手](https://okee.oceanengine.com/mobile/react/#/zh-CN/intro)。可扫下方二维码,在线体验组件功能。 30 | 31 | qr code 32 | 33 | ## 本地运行 node 8+ 34 | 35 | ``` 36 | yarn 37 | yarn dev 38 | ``` 39 | 40 | ## 快速上手 41 | 42 | ``` 43 | npm install @okee-uikit/m-react 44 | ``` 45 | 46 | 全局引入 47 | 48 | ```javascript 49 | import react from 'react'; 50 | import '@okee-uikit/m-react/lib/index.css'; 51 | import { Button } from '@okee-uikit/m-react'; 52 | ``` 53 | 54 | ## 开源协议 55 | OKeeDesign Mobile React 基于MIT协议,可自由参与开源。 56 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 |

3 | logo 4 |

5 | 6 |

OKeeDesign Mobile React

7 | 8 |

OKeeDesign Mobile React is a mobile side component library based on OKeeDesign.

9 | 10 |

11 | npm version 12 | CI Status 13 | Coverage Status 14 | min size 15 |

16 | 17 | English | [简体中文](./README-zh_CN.md) 18 | 19 | ## Features 20 | 21 | * Provide 36 highly qualified components, covering different situations on mobile side 22 | * Support internationalization, support 3 different languages 23 | * Support TypeScript 24 | * Support customizing theme 25 | * Support importing on demand 26 | 27 | ## Quick Preview 28 | 29 | For more information, please refer to [Quick Start](https://okee.oceanengine.com/mobile/react/#/zh-CN/intro). Scan QR code below to experience components function. 30 | 31 | qr code 32 | 33 | ## Run Locally node 8+ 34 | 35 | ``` 36 | yarn 37 | yarn start 38 | ``` 39 | 40 | ## Quick Start 41 | 42 | ``` 43 | npm install @okee-uikit/m-react 44 | ``` 45 | 46 | Global Import 47 | 48 | ```javascript 49 | import React from 'react'; 50 | import '@okee-uikit/m-react/lib/index.css'; 51 | import { Button } from '@okee-uikit/m-react'; 52 | ``` 53 | 54 | ## License 55 | OKeeDesign Mobile React MIT licensed. 56 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | 2 | - [ ] 完善 github README 3 | - [ ] 完善 issue 提交表单及流程 4 | - [ ] 处理新的 lint 规则引入的警告和报错 5 | - [ ] 单元测试 6 | - [ ] 对默认的全局样式进行文档补充 7 | - [ ] 优化打包、发布流程 -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function (api) { 2 | const { BABEL_MODULE } = process.env 3 | const useESModules = BABEL_MODULE !== 'commonjs' 4 | 5 | api && api.cache(false) 6 | return { 7 | presets: [ 8 | [ 9 | '@babel/preset-env', 10 | { 11 | loose: true, 12 | modules: useESModules ? false : 'commonjs' 13 | } 14 | ], 15 | '@babel/preset-typescript', 16 | '@babel/preset-react' 17 | ], 18 | plugins: ['transform-class-properties'] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /build/build-components.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Compile components 3 | */ 4 | const fs = require('fs-extra'); 5 | const path = require('path'); 6 | const babel = require('@babel/core'); 7 | 8 | const esDir = path.join(__dirname, '../es'); 9 | const libDir = path.join(__dirname, '../lib'); 10 | const babelConfig = { 11 | configFile: path.join(__dirname, '../babel.config.js') 12 | }; 13 | 14 | const scriptRegExp = /\.(ts|tsx|js|jsx)$/; 15 | const isDeclaration = path => /(\.d\.ts)$/.test(path); 16 | const isCode = path => !/(demo|test|\.md|\.mdx|\.d\.ts)$/.test(path); 17 | const isDir = dir => fs.lstatSync(dir).isDirectory(); 18 | const isScript = path => scriptRegExp.test(path); 19 | 20 | function compile(dir) { 21 | const files = fs.readdirSync(dir); 22 | 23 | files.forEach(file => { 24 | const filePath = path.join(dir, file); 25 | 26 | // save .d.ts 27 | if (isDeclaration(file)) { 28 | return 29 | } 30 | 31 | // remove unnecessary files 32 | if (!isCode(file)) { 33 | return fs.removeSync(filePath); 34 | } 35 | 36 | // scan dir 37 | if (isDir(filePath)) { 38 | return compile(filePath); 39 | } 40 | 41 | // compile js or ts 42 | if (isScript(file)) { 43 | const { code } = babel.transformFileSync(filePath, babelConfig); 44 | fs.removeSync(filePath); 45 | fs.outputFileSync(filePath.replace(scriptRegExp, '.js'), code); 46 | } 47 | }); 48 | } 49 | 50 | compile(esDir); 51 | 52 | process.env.BABEL_MODULE = 'commonjs'; 53 | compile(libDir); 54 | -------------------------------------------------------------------------------- /build/build-css-non-rem.js: -------------------------------------------------------------------------------- 1 | /** 2 | * build for css no font-size 3 | */ 4 | const fs = require('fs'); 5 | const path = require('path'); 6 | const less = require('less'); 7 | const postcss = require('postcss'); 8 | const postcssrc = require('postcss-load-config'); 9 | const csso = require('csso'); 10 | 11 | const lessIndexPath = path.join(__dirname, '..', 'es/index.less'); 12 | 13 | async function compileLess (lessPath) { 14 | return less.render( 15 | fs.readFileSync(lessPath, 'utf-8'), 16 | { 17 | filename: lessPath, 18 | paths: [path.resolve(__dirname, '..', 'node_modules')], 19 | javascriptEnabled: true, 20 | noIeCompat: true, 21 | modifyVars: { 22 | '@useDefaultFontSize': false 23 | } 24 | } 25 | ) 26 | } 27 | 28 | async function compilePostcss (code, path) { 29 | const postcssConfig = await postcssrc(); 30 | postcssConfig.plugins.splice(1, 1); 31 | return postcss(postcssConfig.plugins).process(code, { from: path }) 32 | } 33 | 34 | async function compile() { 35 | let code = await compileLess(lessIndexPath); 36 | code = await compilePostcss(code.css, lessIndexPath); 37 | code = await csso.minify(code.css).css; 38 | fs.writeFileSync(lessIndexPath.replace('index.less', 'index.non-rem.css'), code); 39 | fs.writeFileSync(lessIndexPath.replace('es/index.less', 'lib/index.non-rem.css'), code); 40 | } 41 | 42 | compile(); 43 | -------------------------------------------------------------------------------- /build/build-css-var.js: -------------------------------------------------------------------------------- 1 | /** 2 | * build for css variables 3 | */ 4 | const fs = require('fs'); 5 | const path = require('path'); 6 | const less = require('less'); 7 | const postcss = require('postcss'); 8 | const postcssrc = require('postcss-load-config'); 9 | const csso = require('csso'); 10 | const LessVarMark = require('@okee-uikit/less-var-mark'); 11 | const { lessVars, lessMap } = require('./utils/lessConfig'); 12 | 13 | const lessIndexPath = path.join(__dirname, '..', 'es/index.less'); 14 | 15 | async function compileLess (lessPath) { 16 | return less.render( 17 | fs.readFileSync(lessPath, 'utf-8'), 18 | { 19 | filename: lessPath, 20 | paths: [path.resolve(__dirname, '..', 'node_modules')], 21 | javascriptEnabled: true, 22 | noIeCompat: true, 23 | modifyVars: lessVars, 24 | plugins: [ 25 | new LessVarMark(lessVars, lessMap) 26 | ] 27 | } 28 | ) 29 | } 30 | 31 | async function compilePostcss (code, path) { 32 | const postcssConfig = await postcssrc(); 33 | postcssConfig.plugins.push(require('@okee-uikit/postcss-var')) 34 | return postcss(postcssConfig.plugins).process(code, { from: path }) 35 | } 36 | 37 | async function compile() { 38 | let code = await compileLess(lessIndexPath); 39 | code = await compilePostcss(code.css, lessIndexPath); 40 | code = await csso.minify(code.css).css; 41 | fs.writeFileSync(lessIndexPath.replace('index.less', 'index.css-variable.css'), code); 42 | fs.writeFileSync(lessIndexPath.replace('es/index.less', 'lib/css_variable.css'), code); 43 | fs.writeFileSync(lessIndexPath.replace('es/index.less', 'lib/index.css-variable.css'), code); 44 | } 45 | 46 | compile(); 47 | -------------------------------------------------------------------------------- /build/build-entry.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 自动生成全局样式入口文件 3 | * index.less && index.tsx 4 | */ 5 | const fs = require('fs'); 6 | const path = require('path'); 7 | const uppercamelize = require('uppercamelcase'); 8 | const packageJson = require('../package.json'); 9 | 10 | const commonPath = '../src/components'; 11 | const components = fs.readdirSync(path.resolve(__dirname, commonPath)); 12 | 13 | const styleList = components 14 | .filter(name => fs.existsSync( 15 | path.resolve(__dirname, `${commonPath}/${name}/index.less`) 16 | )) 17 | .map(name => `@import './components/${name}/index';`); 18 | 19 | fs.writeFileSync(path.join(__dirname, '../src/index.less'), 20 | `// This file is auto gererated by build/build-entry.js 21 | @import './style/index'; 22 | @import './style/root'; 23 | ${styleList.join('\n')} 24 | ` 25 | ); 26 | 27 | const version = process.env.VERSION || packageJson.version; 28 | const exportList = components.map(name => 29 | `export { default as ${uppercamelize(name)} } from './components/${name}/index';` 30 | ); 31 | 32 | fs.writeFileSync(path.join(__dirname, '../src/index.tsx'), 33 | `// This file is auto gererated by build/build-entry.js 34 | const version = '${version}'; 35 | export default { version }; 36 | export * from './props-type'; 37 | ${exportList.join('\n')} 38 | ` 39 | ); 40 | -------------------------------------------------------------------------------- /build/build-icons.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 自动生成 SVG 图标引入文件 3 | */ 4 | const fs = require('fs'); 5 | const path = require('path'); 6 | const uppercamelize = require('uppercamelcase'); 7 | 8 | const iconDir = path.resolve(__dirname, '../src/components/icon'); 9 | const svgDir = path.resolve(iconDir, 'svg'); 10 | 11 | const icons = fs.readdirSync(svgDir).filter(f => f.endsWith('.svg')); 12 | 13 | const content = [ 14 | '/* eslint-disable */', 15 | '// this file is auto generated by build/build-icons.js', 16 | '', 17 | 'export interface IconMap {', 18 | ...icons.map(file => ` ${uppercamelize(file.replace(/\.svg$/, ''))}: string;`), 19 | '}', 20 | '', 21 | 'export const allIcons: IconMap = {', 22 | ...icons.map( 23 | file => 24 | ` ${uppercamelize(file.replace(/\.svg$/, ''))}: \`${fs.readFileSync(path.resolve(svgDir, file))}\`,`, 25 | ), 26 | '};', 27 | '', 28 | ].join('\n'); 29 | 30 | fs.writeFileSync(path.resolve(iconDir, 'all.ts'), content); 31 | -------------------------------------------------------------------------------- /build/build-lib.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Build npm lib 3 | */ 4 | const shell = require('shelljs'); 5 | const signale = require('signale'); 6 | 7 | const { Signale } = signale; 8 | const tasks = [ 9 | 'npm run lint', 10 | 'npm run build:gen-files', 11 | 'npm run build:mkdir', 12 | 'npm run build:declaration', 13 | 'node build/build-components.js --color', 14 | 'node build/build-style.js --color', 15 | 'node build/build-style-entry.js --color', 16 | 'node build/build-css-var.js', 17 | 'node build/build-css-non-rem.js', 18 | 'cross-env NODE_ENV=production webpack --color --config build/webpack.build.js', 19 | 'cross-env NODE_ENV=production webpack -p --color --config build/webpack.build.js', 20 | ]; 21 | 22 | tasks.forEach(task => { 23 | signale.start(task); 24 | const interactive = new Signale({ interactive: true }); 25 | interactive.pending(task); 26 | shell.exec(`${task} --silent`); 27 | interactive.success(task); 28 | }); 29 | -------------------------------------------------------------------------------- /build/build-mkdir.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 创建产出目录 3 | */ 4 | const fs = require('fs-extra'); 5 | const path = require('path'); 6 | 7 | const esDir = path.join(__dirname, '../es'); 8 | const libDir = path.join(__dirname, '../lib'); 9 | const srcDir = path.join(__dirname, '../src'); 10 | 11 | // clear dir 12 | fs.emptyDirSync(esDir); 13 | fs.emptyDirSync(libDir); 14 | 15 | // copy dir 16 | fs.copySync(srcDir, esDir); 17 | fs.copySync(srcDir, libDir); 18 | -------------------------------------------------------------------------------- /build/build-style.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs = require('fs-extra'); 3 | const glob = require('fast-glob'); 4 | const less = require('less'); 5 | const csso = require('csso'); 6 | const postcss = require('postcss'); 7 | const postcssrc = require('postcss-load-config'); 8 | 9 | /** 10 | * 主题定制相关 11 | */ 12 | let modifyVars = {}; 13 | 14 | async function compileLess(lessCodes, paths) { 15 | const outputs = await Promise.all( 16 | lessCodes.map((source, index) => { 17 | return less.render(source, { 18 | paths: [path.resolve(__dirname, 'node_modules')], 19 | filename: paths[index], 20 | javascriptEnabled: true, 21 | modifyVars 22 | }) 23 | } 24 | ) 25 | ).catch(error => { 26 | console.error('compileLess', error) 27 | }); 28 | return outputs.map(item => item.css); 29 | } 30 | 31 | async function compilePostcss(cssCodes, paths) { 32 | const postcssConfig = await postcssrc(); 33 | const outputs = await Promise.all( 34 | cssCodes.map((css, index) => 35 | postcss(postcssConfig.plugins).process(css, { from: paths[index] }) 36 | ) 37 | ).catch(error => { 38 | console.error('compilePostcss', error) 39 | }); 40 | 41 | return outputs.map(item => item.css); 42 | } 43 | 44 | async function compileCsso(cssCodes) { 45 | return cssCodes.map(css => csso.minify(css).css); 46 | } 47 | 48 | async function dest(output, paths) { 49 | await Promise.all( 50 | output.map((css, index) => fs.writeFile(paths[index].replace('.less', '.css'), css)) 51 | ).catch(error => { 52 | console.error('dest', error) 53 | }); 54 | } 55 | 56 | // compile component css 57 | async function compile() { 58 | let codes; 59 | 60 | const paths = await glob( 61 | ['./es/**/*.less', './lib/**/*.less'], 62 | { absolute: true } 63 | ); 64 | 65 | codes = await Promise.all( 66 | paths.map(path => fs.readFile(path, 'utf-8')) 67 | ).catch(error => { 68 | console.error('compile', error) 69 | }); 70 | 71 | codes = await compileLess(codes, paths); 72 | codes = await compilePostcss(codes, paths); 73 | codes = await compileCsso(codes); 74 | 75 | await dest(codes, paths); 76 | } 77 | 78 | compile(); 79 | -------------------------------------------------------------------------------- /build/utils/color-palette/index.js: -------------------------------------------------------------------------------- 1 | const { colorMix, colorPalette } = require('./util'); 2 | 3 | const grayScaleLimit = 11; 4 | 5 | let presetPrimaryColors = { 6 | blue: '#338AFF', 7 | green: '#6ABF40', 8 | red: '#F65656', 9 | yellow: '#FFA900', 10 | cyan: '#10D5D5' 11 | }; 12 | let presetPalettes = {}; 13 | 14 | // 生成色阶 15 | function generate(color){ 16 | const colors = []; 17 | for (let i = 0; i < 11; i++) { 18 | colors.push(i === 5 ? color : colorPalette(color, i + 1)) 19 | } 20 | return colors; 21 | } 22 | 23 | // 生成预设值 24 | function generatePreset(){ 25 | // 彩色 26 | Object.keys(presetPrimaryColors).forEach((key) => { 27 | presetPalettes[key]= generate(presetPrimaryColors[key]) 28 | }); 29 | 30 | // 浅灰色 31 | presetPalettes['light-gray'] = [ 32 | '#FFFFFF', 33 | '#FAFAFA', 34 | '#F5F5F5', 35 | '#F0F0F0', 36 | '#EBEBEB', 37 | '#E0E0E0', 38 | '#D6D6D6', 39 | '#C1C1C1', 40 | '#999999', 41 | '#666666', 42 | '#333333', 43 | ]; 44 | 45 | // 深灰色 46 | presetPalettes['dark-gray'] = []; 47 | for(let j = 1; j <= grayScaleLimit; j++) { 48 | let colorValue = colorMix(presetPrimaryColors['blue'], j); 49 | presetPalettes['dark-gray'].unshift(colorValue) 50 | } 51 | 52 | // 深灰色-原色 53 | presetPalettes['dark-gray-origin'] = []; 54 | for(let j = 1; j <= grayScaleLimit; j++) { 55 | let colorValue = colorMix(presetPrimaryColors['blue'], j, false); 56 | presetPalettes['dark-gray-origin'].unshift(colorValue) 57 | } 58 | } 59 | 60 | generatePreset(); 61 | 62 | // 输出colorMap对象 63 | module.exports = { generate, presetPalettes }; -------------------------------------------------------------------------------- /build/utils/color-palette/util.js: -------------------------------------------------------------------------------- 1 | const tinycolor = require('./tinycolor'); 2 | 3 | const colorMix = function(color, index, mix = true){ 4 | const darkestL = 8; 5 | const midL = 24; 6 | const lightL = 56; 7 | const darkStep = 2; 8 | const midStep = 4; 9 | const lightStep = 20; 10 | let grayL; 11 | if(index <= 9 ){ 12 | grayL = (darkestL + darkStep * (index - 1)) / 100; 13 | }else if(index > 9 && index <= 12){ 14 | grayL = (midL + midStep * (index - 9)) / 100; 15 | }else if(index > 12){ 16 | grayL = (lightL + lightStep * (index - 13)) / 100; 17 | } 18 | 19 | const curGray = tinycolor({ 20 | h: 0, 21 | s: 0, 22 | l: grayL 23 | }).toHexString(); 24 | 25 | if(index >= 12 || !mix){ 26 | return curGray; 27 | }else{ 28 | const mixColor = tinycolor.mix(color, curGray, 96).toHexString(); 29 | return mixColor; 30 | } 31 | }; 32 | 33 | 34 | const colorPalette = function(color, index) { 35 | const darkestL = 16; 36 | const lightestL = 96; 37 | const hsl = tinycolor(color).toHsl(); 38 | const curLight = hsl.l * 100; 39 | 40 | let light; 41 | const darkStep = Math.max((curLight - darkestL) / 5, 0); 42 | const lightStep = Math.max((lightestL - curLight) /5, 0); 43 | if(index === 6){ 44 | light = hsl.l; 45 | }else if(index < 6){ 46 | light = (curLight + (6 - index)*lightStep)/100; 47 | }else if(index > 6){ 48 | light = (curLight - (index - 6)*darkStep)/100; 49 | } 50 | 51 | return tinycolor({ 52 | h: Math.round(hsl.h), 53 | s: hsl.s, 54 | l: light 55 | }).toHexString(); 56 | } 57 | 58 | function mixGray(color, gray) { 59 | return tinycolor.mix(color, curGray, 96) 60 | } 61 | 62 | exports.colorMix = colorMix; 63 | exports.colorPalette = colorPalette; 64 | exports.mixGray = mixGray; 65 | -------------------------------------------------------------------------------- /build/utils/cssVariable.json: -------------------------------------------------------------------------------- 1 | { 2 | "animation-duration-base": "0.3s", 3 | "animation-duration-fast": "0.2s", 4 | "blue": "#0278ff", 5 | "cyan": "#00d2d5", 6 | "orange": "#fec038", 7 | "red": "#ff6767", 8 | "white": "#fff", 9 | "black": "#000", 10 | "primary-color": "#0278ff", 11 | "success-color": "#00d2d5", 12 | "warning-color": "#fec038", 13 | "danger-color": "#ff6767", 14 | "gray-1": "#fafafa", 15 | "gray-2": "#f7f7f7", 16 | "gray-3": "#eeeeee", 17 | "gray-4": "#dcdcdc", 18 | "gray-5": "#c1c1c1", 19 | "text-color-1": "#000000", 20 | "text-color-2": "#333333", 21 | "text-color-3": "#666666", 22 | "text-color-4": "#999999", 23 | "text-color-5": "#c1c1c1", 24 | "text-color-link": "#0278ff", 25 | "background-color-base": "#fff", 26 | "background-color-active": "rgba(153, 153, 153, 0.1)", 27 | "primary-color-1": "#0068de", 28 | "primary-color-2": "#0278ff", 29 | "primary-color-3": "#3191ff", 30 | "primary-color-4": "#8ec2ff", 31 | "primary-color-5": "#ebf4ff", 32 | "success-color-1": "#00b8bb", 33 | "success-color-2": "#00d2d5", 34 | "success-color-3": "#0dfbff", 35 | "success-color-4": "#7cfdff", 36 | "success-color-5": "#ebffff", 37 | "warning-color-1": "#feb10b", 38 | "warning-color-2": "#fec038", 39 | "warning-color-3": "#fecb5c", 40 | "warning-color-4": "#ffe2a3", 41 | "warning-color-5": "#fff9eb", 42 | "danger-color-1": "#ff3030", 43 | "danger-color-2": "#ff6767", 44 | "danger-color-3": "#ff8181", 45 | "danger-color-4": "#ffb6b6", 46 | "danger-color-5": "#ffebeb", 47 | 48 | "picker-confirm-btn-color": "#000" 49 | } -------------------------------------------------------------------------------- /build/utils/less2json.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const { generate } = require('./color-palette'); 4 | 5 | const ignoreKeys = ['primary-color-', 'success-color-', 'warning-color-', 'danger-color-']; 6 | 7 | function checkKey(key) { 8 | return !ignoreKeys.some(item => key.search(item) > -1); 9 | } 10 | 11 | function less2json(varPath) { 12 | const json = {}; 13 | const less = fs.readFileSync(varPath).toString(); 14 | const lines = less.split('\n'); 15 | 16 | lines.forEach(line => { 17 | if (line.indexOf('@') > -1) { 18 | let key = line.split(':')[0]; 19 | if (key && checkKey(key)) { 20 | key = key.replace('@', '').trim(); 21 | let value = line.split(':')[1]; 22 | if (value) { 23 | value = value.split('//')[0].split(';')[0].trim(); 24 | json[key] = value; 25 | } 26 | } 27 | } 28 | }); 29 | 30 | const baseColorKeys = ['primary-color', 'success-color', 'warning-color', 'danger-color']; 31 | 32 | baseColorKeys.forEach(key => { 33 | let value = json[key]; 34 | if (value.indexOf('@') > -1) { 35 | value = value.substring(1); 36 | json[key] = json[value.trim()]; 37 | } 38 | }); 39 | 40 | baseColorKeys.forEach(key => { 41 | var color = json[key]; 42 | if (color) { 43 | const colors = generate(color); 44 | const colorIndex = [6, 5, 4, 2, 0]; 45 | colorIndex.forEach((index, i) => { 46 | json[`${key}-${i + 1}`] = colors[index]; 47 | }); 48 | } 49 | }); 50 | 51 | Object.keys(json).forEach(key => { 52 | let valueArr = json[key].split(' '); 53 | valueArr.forEach((value, index) => { 54 | if (value[0] === '@') { 55 | value = value.substring(1); 56 | if (json[value]) { 57 | valueArr[index] = json[value]; 58 | } 59 | } 60 | }); 61 | 62 | json[key] = valueArr.join(' '); 63 | }); 64 | 65 | // var.config.json 66 | const configJsonPath = path.join(__dirname, '../../src/style/var.config.json'); 67 | fs.writeFileSync(configJsonPath, JSON.stringify(json)); 68 | } 69 | module.exports = less2json; 70 | -------------------------------------------------------------------------------- /build/utils/lessConfig.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 主题定制演示 3 | */ 4 | const fs = require('fs'); 5 | const path = require('path'); 6 | const LessVarMark = require('@okee-uikit/less-var-mark'); 7 | const less2json = require('./less2json'); 8 | 9 | const varJsonPath = path.join(__dirname, '../../src/style/var.config.json'); 10 | const rootLessPath = path.join(__dirname, '../../src/style/root.less'); 11 | const lessVarPath = path.join(__dirname, '../../src/style/var.less'); 12 | 13 | const cssVariableJson = require('./cssVariable.json'); 14 | 15 | let lessVars = {}; 16 | let lessMap = {}; 17 | 18 | // 清空 var.config.json 19 | fs.writeFileSync(varJsonPath, '{}'); 20 | 21 | // 设置 var.config.json 22 | less2json(lessVarPath); 23 | 24 | // 获取 var.config.json 25 | const configJson = require(varJsonPath); 26 | 27 | Object.keys(cssVariableJson).forEach((key) => { 28 | lessVars[key] = configJson[key]; 29 | }); 30 | 31 | // 清空 root.less 32 | fs.writeFileSync(rootLessPath, '//'); 33 | 34 | // 写入root.less 35 | const rootJson = {}; 36 | Object.keys(lessVars).forEach(key => { 37 | rootJson[`--${key}`] = lessVars[key]; 38 | }); 39 | fs.writeFileSync( 40 | rootLessPath, 41 | ` 42 | /* stylelint-disable */ 43 | :root${JSON.stringify(rootJson)} 44 | ` 45 | .replace('}', ';}') 46 | .replace(/,/g, ';') 47 | .replace(/"/g, ''), 48 | ); 49 | 50 | // 设置 lessMap 51 | const lessVarMark = new LessVarMark(); 52 | const lessVarJson = lessVarMark.variables2Json(fs.readFileSync(`${lessVarPath}`, 'utf-8')); 53 | lessMap = lessVarMark.getVariablesMap(lessVars, lessVarJson); 54 | 55 | module.exports = { 56 | lessVars, 57 | lessMap, 58 | }; 59 | -------------------------------------------------------------------------------- /build/version.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let conf; 4 | try { 5 | conf = require('../package.json'); 6 | } catch (e) { 7 | console.log(e); 8 | } 9 | 10 | process.stdout.write(conf.version); -------------------------------------------------------------------------------- /build/webpack.base.js: -------------------------------------------------------------------------------- 1 | /** 2 | * webpack basic config 3 | */ 4 | const path = require('path'); 5 | 6 | module.exports = { 7 | resolve: { 8 | extensions: [".ts", ".tsx", ".js", ".jsx", ".json", '.css', 'less'] 9 | }, 10 | module: { 11 | rules: [{ 12 | test: /\.(ts|tsx|js|jsx)$/, 13 | exclude: /node_modules/, 14 | use: { 15 | loader: 'babel-loader', 16 | options: { 17 | rootMode: 'upward' 18 | } 19 | } 20 | }, 21 | { 22 | test: /\.(css|less)$/, 23 | sideEffects: true, 24 | include: /(node_modules)/, 25 | use: [ 26 | 'style-loader', 27 | 'css-loader', 28 | { 29 | loader: 'less-loader', 30 | options: { 31 | paths: [path.resolve(__dirname, 'node_modules')], 32 | javascriptEnabled: true, 33 | noIeCompat: true, 34 | }, 35 | }, 36 | ], 37 | }, 38 | { 39 | test: /\.(css|less)$/, 40 | sideEffects: true, 41 | exclude: /(node_modules)/, 42 | use: [ 43 | 'style-loader', 44 | 'css-loader', 45 | 'postcss-loader', 46 | { 47 | loader: 'postcss-loader', 48 | options: { 49 | ident: 'postcss', 50 | plugins: [require('@okee-uikit/postcss-var')], 51 | }, 52 | }, 53 | { 54 | loader: 'less-loader', 55 | options: { 56 | paths: [path.resolve(__dirname, 'node_modules')], 57 | javascriptEnabled: true, 58 | noIeCompat: true, 59 | }, 60 | }, 61 | ], 62 | }, 63 | { 64 | test: /\.svg$/, 65 | loader: 'url-loader' 66 | }] 67 | } 68 | }; 69 | -------------------------------------------------------------------------------- /build/webpack.build.js: -------------------------------------------------------------------------------- 1 | /** 2 | * webpack build entry 3 | */ 4 | const path = require('path'); 5 | const merge = require('webpack-merge'); 6 | const config = require('./webpack.base'); 7 | 8 | const isMinify = process.argv.indexOf('-p') !== -1; 9 | 10 | delete config.serve; 11 | 12 | module.exports = merge(config, { 13 | mode: 'production', 14 | entry: './es/index.js', 15 | output: { 16 | path: path.join(__dirname, '../lib'), 17 | library: 'okui', 18 | libraryTarget: 'umd', 19 | umdNamedDefine: true, 20 | filename: isMinify ? '[name].min.js' : '[name].js', 21 | globalObject: 'typeof self !== \'undefined\' ? self : this' 22 | }, 23 | externals: { 24 | 'react': 'React', 25 | 'react-dom': 'ReactDOM' 26 | }, 27 | performance: false, 28 | optimization: { 29 | minimize: isMinify 30 | }, 31 | }); 32 | -------------------------------------------------------------------------------- /build/webpack.site.dev.js: -------------------------------------------------------------------------------- 1 | /** 2 | * webpack mobile 3 | */ 4 | const path = require('path'); 5 | const merge = require('webpack-merge'); 6 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 7 | 8 | const config = require('./webpack.base'); 9 | 10 | function resolve (dir) { 11 | return path.join(__dirname, '..', dir); 12 | } 13 | 14 | module.exports = merge(config, { 15 | mode: 'development', 16 | entry: { 17 | 'index': resolve('docs/mobile/index'), 18 | }, 19 | output: { 20 | path: resolve('/docs/dist'), 21 | publicPath: '/', 22 | chunkFilename: 'async_[name].js' 23 | }, 24 | devtool: 'cheap-module-eval-source-map', 25 | devServer: { 26 | host: '0.0.0.0', 27 | port: 8094, 28 | }, 29 | plugins: [ 30 | new HtmlWebpackPlugin({ 31 | template: 'docs/mobile/template.html', 32 | filename: 'index.html', 33 | inject: false 34 | }) 35 | ], 36 | }); 37 | -------------------------------------------------------------------------------- /docs/mobile/components/demo-title/index.less: -------------------------------------------------------------------------------- 1 | .dome-title { 2 | display: flex; 3 | align-items: center; 4 | color: #333333; 5 | padding: 16px 0; 6 | .svg-icon-pointer { 7 | width: 20px; 8 | height: 20px; 9 | margin-right: 8px; 10 | } 11 | &-word { 12 | font-weight: 500; 13 | font-size: 16px; 14 | line-height: 24px; 15 | } 16 | } -------------------------------------------------------------------------------- /docs/mobile/components/demo-title/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | import pointerSvg from '../../../static/pointer.svg' 3 | import './index.less' 4 | 5 | interface DemoTitleProps { 6 | title: string; 7 | } 8 | const DemoTitle: FC = (props: DemoTitleProps) => { 9 | 10 | const { title } = props 11 | 12 | return ( 13 |
14 | 15 |
{title}
16 |
17 | ) 18 | } 19 | 20 | export { DemoTitle } -------------------------------------------------------------------------------- /docs/mobile/components/mobile-card/index.less: -------------------------------------------------------------------------------- 1 | .demo-cell { 2 | // margin-top: 16px; 3 | &-content-slot { 4 | padding: 16px 0; 5 | } 6 | &-header { 7 | &-title { 8 | display: flex; 9 | justify-content: space-between; 10 | align-items: center; 11 | font-size: 14px; 12 | line-height: 22px; 13 | &__left { 14 | color: #C1C1C1; 15 | font-family: PingFang SC; 16 | font-style: normal; 17 | font-weight: normal; 18 | } 19 | } 20 | &-line { 21 | margin-top: 9px; 22 | height: 1px; 23 | background-color: #ebebeb; 24 | position: relative; 25 | &::before { 26 | display: inline-block; 27 | content: ''; 28 | width: 32px; 29 | height: 3px; 30 | background: #3337c5; 31 | position: absolute; 32 | top: -1px; 33 | } 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /docs/mobile/components/mobile-card/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | import './index.less' 3 | 4 | interface DemoCellProps { 5 | title: string; 6 | children: any; 7 | style?: object; 8 | className?: string; 9 | } 10 | const MobileCard: FC = (props: DemoCellProps) => { 11 | 12 | const { title } = props 13 | const centerEle = props.children || '' 14 | const className = `${props.className || ''} demo-cell-content-slot` 15 | return ( 16 |
17 |
18 |
19 | { title } 20 |
21 |
22 |
23 | { centerEle } 24 |
25 |
26 | ) 27 | } 28 | 29 | export { MobileCard } -------------------------------------------------------------------------------- /docs/mobile/components/mobile-header/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Header } from '../../../../src/index' 3 | 4 | interface DemoHeaderProps { 5 | title: string; 6 | onClickLeft: () => void; 7 | } 8 | 9 | class DemoHeader extends React.Component { 10 | 11 | render(): JSX.Element { 12 | return ( 13 |
14 |
{ this.props.title }
15 |
16 | ) 17 | 18 | } 19 | } 20 | 21 | export default DemoHeader -------------------------------------------------------------------------------- /docs/mobile/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import { Route } from 'react-router' 4 | import { HashRouter } from 'react-router-dom' 5 | 6 | import App from './App' 7 | import routes, { RouteType } from './routes' 8 | 9 | import { LocaleContext } from '../../src/index' 10 | import local from '../../src/locale/zh-CN' 11 | 12 | import Stats from 'stats.js'; 13 | 14 | ReactDOM.render( 15 | 16 | 17 | 18 | { 19 | routes.map((route: RouteType, index: number) => ( 20 | 25 | )) 26 | } 27 | 28 | 29 | , 30 | document.getElementById('root') 31 | ) 32 | 33 | 34 | 35 | // const stats = new Stats(); 36 | // stats.showPanel( 1 ); // 0: fps, 1: ms, 2: mb, 3+: custom 37 | // document.body.appendChild( stats.dom ); 38 | 39 | // function animate() { 40 | 41 | // stats.begin(); 42 | 43 | // // monitored code goes here 44 | 45 | // stats.end(); 46 | 47 | // requestAnimationFrame( animate ); 48 | 49 | // } 50 | 51 | // requestAnimationFrame( animate ); -------------------------------------------------------------------------------- /docs/mobile/routes.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 路由 3 | */ 4 | import { importAll, ImportMap, RequireContext } from '../utils' 5 | import config, { Lang } from '../config' 6 | import { NavsItem } from '../type' 7 | 8 | export interface RouteType { 9 | component: any; 10 | path: string; 11 | } 12 | 13 | const routes: RouteType[] = [] 14 | const componentMap: ImportMap = {} 15 | const packages: RequireContext = require.context( 16 | '../../src/components', 17 | true, 18 | /demo\/index\.tsx$/ 19 | ) 20 | 21 | importAll(componentMap, packages) 22 | 23 | 24 | function findNav (nav: NavsItem, lang: string): void { 25 | if (nav.list) { 26 | nav.list.forEach((item) => findNav(item, lang)) 27 | } else { 28 | let { path } = nav 29 | 30 | if (path) { 31 | path = path.replace('/', '') 32 | 33 | const module = componentMap[`./${path}/demo/index.tsx`] 34 | // @ts-ignore 35 | const component = module ? module.default : null 36 | 37 | if (component) { 38 | routes.push({ 39 | path: `/${lang}/${path}`, 40 | component 41 | }) 42 | } 43 | } 44 | } 45 | } 46 | 47 | Object.keys(config).forEach((key: string) => { 48 | const lang = key as Lang 49 | const navs = config[lang].navs || [] 50 | 51 | navs.forEach((nav) => { 52 | findNav(nav, lang) 53 | }) 54 | }) 55 | 56 | export default routes 57 | -------------------------------------------------------------------------------- /docs/mobile/template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | OKee-Mobile-React-Docs 15 | 16 | 29 | 30 | 38 | 39 | 40 |
41 | 42 | 43 | -------------------------------------------------------------------------------- /docs/mobile/tracking.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * 导航守卫 3 | */ 4 | import { useEffect } from 'react' 5 | import { RouteComponentProps, withRouter } from 'react-router' 6 | 7 | export default withRouter((props: RouteComponentProps) => { 8 | useEffect(() => { 9 | // @ts-ignore 10 | window.collectEvent('predefinePageView', { 11 | route: window.location.hash.slice(1), 12 | type: '0', // PC端窗口 type 为 0 13 | }) 14 | }, [location.hash]) 15 | 16 | return null 17 | }); 18 | 19 | -------------------------------------------------------------------------------- /docs/static/i18n.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /docs/static/menu_icons/comp.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /docs/static/menu_icons/comp_active.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /docs/static/menu_icons/i18n.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/static/menu_icons/i18n_active.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/static/menu_icons/intro.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /docs/static/menu_icons/intro_active.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /docs/static/menu_icons/quickstart.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /docs/static/menu_icons/quickstart_active.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /docs/static/menu_icons/theme.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /docs/static/menu_icons/theme_active.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /docs/static/pointer.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /docs/static/send.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /docs/type.ts: -------------------------------------------------------------------------------- 1 | export interface Anchor { 2 | id: string; 3 | label: string; 4 | } 5 | export interface NavsItem { 6 | label: string; 7 | path?: string; 8 | activeIcon?: string; 9 | defaultIcon?: string; 10 | anchors?: Anchor[]; 11 | list?: NavsItem[]; 12 | } 13 | 14 | export interface ByDocsConfig { 15 | logo?: string; 16 | navs?: NavsItem[]; 17 | onLinkTo?: (item: NavsItem) => string | void; 18 | anchorPos?: 'left' | 'right'; 19 | } 20 | export declare type Config = ByDocsConfig; -------------------------------------------------------------------------------- /docs/utils/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 工具函数 3 | */ 4 | /** 5 | * this interface from '@types/webpack-env' 6 | */ 7 | 8 | 9 | const ua: string = navigator.userAgent; 10 | export const isMobile: boolean = /ios|iphone|ipod|ipad|android/i.test(ua); 11 | export interface RequireContext { 12 | keys(): string[]; 13 | (id: string): any; 14 | (id: string): T; 15 | resolve(id: string): string; 16 | id: string; 17 | } 18 | 19 | export interface ImportMap { 20 | [key: string]: RequireContext; 21 | } 22 | 23 | // https://webpack.docschina.org/guides/dependency-management/ 24 | export function importAll(map: ImportMap, r: RequireContext): void { 25 | r.keys().forEach(key => { 26 | map[key] = r(key) 27 | }) 28 | } 29 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | 'autoprefixer': {}, 4 | 'postcss-px2rem': { 5 | remUnit: 20 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/common/popup/context.tsx: -------------------------------------------------------------------------------- 1 | import { useMemo, useRef } from 'react'; 2 | 3 | export type MountElem = HTMLElement | (() => HTMLElement); 4 | 5 | export type OverlayConfig = { 6 | zIndex: number; 7 | className?: string; 8 | customStyle?: string | object[] | object; 9 | mountElem?: MountElem; 10 | }; 11 | 12 | export interface StackItem { 13 | close: () => void; 14 | config: OverlayConfig; 15 | } 16 | 17 | export const context = { 18 | zIndex: 2000, 19 | lockCount: 0, 20 | stack: [] as StackItem[], 21 | 22 | get top(): StackItem { 23 | return this.stack[this.stack.length - 1]; 24 | }, 25 | }; 26 | 27 | export interface UseZIndexOptions { 28 | update?: boolean; 29 | } 30 | export function useZIndex(props: UseZIndexOptions, ...deps: any[]): number { 31 | const { update = true } = props; 32 | 33 | const zIndexRef = useRef(0); 34 | return useMemo(() => { 35 | if (!zIndexRef.current || update) { 36 | zIndexRef.current = context.zIndex++; 37 | } 38 | return zIndexRef.current; 39 | // eslint-disable-next-line react-hooks/exhaustive-deps 40 | }, [update, ...deps]); 41 | } 42 | -------------------------------------------------------------------------------- /src/common/touch.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Touch 3 | */ 4 | export type TouchFunction = (event: TouchEvent) => void; 5 | 6 | export default class Touch { 7 | static MIN_DISTANCE = 5; 8 | static getDirection = function (x: number, y: number): string { 9 | if (x > y && x > Touch.MIN_DISTANCE) { 10 | return 'horizontal'; 11 | } 12 | 13 | if (y > x && y > Touch.MIN_DISTANCE) { 14 | return 'vertical'; 15 | } 16 | 17 | return ''; 18 | }; 19 | 20 | public startX: number; 21 | public startY: number; 22 | public deltaX: number; 23 | public deltaY: number; 24 | public offsetX: number; 25 | public offsetY: number; 26 | public direction: string; 27 | 28 | constructor() { 29 | this.startX = 0; 30 | this.startY = 0; 31 | this.deltaX = 0; 32 | this.deltaY = 0; 33 | this.offsetX = 0; 34 | this.offsetY = 0; 35 | this.direction = ''; 36 | } 37 | 38 | resetTouchStatus: () => void = () => { 39 | this.direction = ''; 40 | this.deltaX = 0; 41 | this.deltaY = 0; 42 | this.offsetX = 0; 43 | this.offsetY = 0; 44 | }; 45 | 46 | touchStart: TouchFunction = (event: TouchEvent) => { 47 | this.resetTouchStatus(); 48 | this.startX = event.touches[0].clientX; 49 | this.startY = event.touches[0].clientY; 50 | }; 51 | 52 | touchMove: TouchFunction = (event: TouchEvent) => { 53 | const touch = event.touches[0]; 54 | this.deltaX = touch.clientX - this.startX; 55 | this.deltaY = touch.clientY - this.startY; 56 | this.offsetX = Math.abs(this.deltaX); 57 | this.offsetY = Math.abs(this.deltaY); 58 | this.direction = this.direction || Touch.getDirection(this.offsetX, this.offsetY); 59 | }; 60 | } 61 | -------------------------------------------------------------------------------- /src/components/action-sheet/action-sheet-item.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import createBEM, { prefix } from '../../utils/create/createBEM'; 3 | const bem = createBEM('action-sheet'); 4 | 5 | export interface ActionSheetItemType { 6 | name: string; 7 | subname?: string; 8 | loading?: boolean; 9 | active?: boolean; 10 | disabled?: boolean; 11 | className?: string; 12 | callback?: (item: ActionSheetItemType) => void; 13 | } 14 | 15 | export interface ActionSheetItemProps { 16 | item: ActionSheetItemType; 17 | index: number; 18 | onClick: (item: ActionSheetItemType, index: number) => void; 19 | } 20 | 21 | export default function ActionSheetItem(props: ActionSheetItemProps): JSX.Element { 22 | const { onClick, item, index } = props; 23 | const disabled = item.disabled || item.loading; 24 | 25 | function onClickOption(event: React.MouseEvent): void { 26 | event.stopPropagation(); 27 | 28 | if (item.disabled || item.loading) { 29 | return; 30 | } 31 | 32 | if (item.callback) { 33 | item.callback(item); 34 | } 35 | onClick(item, index); 36 | } 37 | 38 | // loading态的样式icon待实现 39 | const OptionContent = item.loading ? ( 40 | 41 | ) : ( 42 | [ 43 | 44 | {item.name} 45 | , 46 | item.subname && ( 47 | 48 | {item.subname} 49 | 50 | ), 51 | ] 52 | ); 53 | 54 | return ( 55 |
61 | {OptionContent} 62 |
63 | ); 64 | } 65 | -------------------------------------------------------------------------------- /src/components/action-sheet/createActionSheet.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | 4 | import { ActionSheetOptions } from './index'; 5 | import ActionSheet, { ActionSheetProps } from './action-sheet'; 6 | 7 | export default function CreateActionSheet(options: ActionSheetOptions): void { 8 | if (typeof window === 'undefined') { 9 | return; 10 | } 11 | 12 | const div: HTMLElement = document.createElement('div'); 13 | document.body.appendChild(div); 14 | 15 | function destroy(): void { 16 | const unmountResult = ReactDOM.unmountComponentAtNode(div); 17 | if (unmountResult && div.parentNode) { 18 | div.parentNode.removeChild(div); 19 | } 20 | } 21 | 22 | function render(options: ActionSheetProps): void { 23 | ReactDOM.render(, div); 24 | } 25 | 26 | function close(): void { 27 | /* eslint-disable @typescript-eslint/no-use-before-define */ 28 | currentOptions = Object.assign({}, currentOptions, { 29 | value: false, 30 | afterClose: destroy, 31 | }); 32 | 33 | render(currentOptions); 34 | } 35 | 36 | let currentOptions = { 37 | ...options, 38 | close, 39 | value: true, 40 | }; 41 | 42 | render(currentOptions); 43 | } 44 | -------------------------------------------------------------------------------- /src/components/action-sheet/index.tsx: -------------------------------------------------------------------------------- 1 | import CreateActionSheet from './createActionSheet'; 2 | import { ActionSheetItemType } from './action-sheet-item'; 3 | 4 | import ActionSheet from './action-sheet'; 5 | import ActionSheetItem from './action-sheet-item'; 6 | 7 | export type { ActionSheetProps } from './action-sheet'; 8 | export type { ActionSheetItemProps, ActionSheetItemType } from './action-sheet-item'; 9 | 10 | export interface ActionSheetOptions { 11 | title?: string; 12 | actions?: ActionSheetItemType[]; 13 | duration?: number; 14 | cancelText?: string; 15 | showClose?: boolean; 16 | closeOnClickAction?: boolean; 17 | safeAreaInsetBottom?: boolean; 18 | className?: string; 19 | onCancel?: () => void; 20 | onClose?: () => void; 21 | onSelect?: (item: ActionSheetItemType, index?: number) => void; 22 | } 23 | 24 | export default { 25 | showActionSheet: CreateActionSheet, 26 | ActionSheet, 27 | ActionSheetItem, 28 | }; 29 | -------------------------------------------------------------------------------- /src/components/badge/demo/index.less: -------------------------------------------------------------------------------- 1 | @import '../../../style/common'; 2 | 3 | .demo-badge { 4 | .@{prefix}badge { 5 | margin-right: 16px; 6 | } 7 | 8 | .@{prefix}cell__title, 9 | .@{prefix}cell__content { 10 | display: flex; 11 | align-items: center; 12 | overflow: visible; 13 | } 14 | 15 | .@{prefix}cell__content { 16 | justify-content: flex-end; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/components/badge/demo/index.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable semi */ 2 | import React, { FC } from 'react'; 3 | import { useHistory } from 'react-router-dom'; 4 | import { Badge, Cell } from '../../../../src/index'; 5 | import { MobileCard } from '../../../../docs/mobile/components/mobile-card/index'; 6 | import DemoHeader from '../../../../docs/mobile/components/mobile-header/index'; 7 | 8 | import './index.less'; 9 | 10 | const DemoBadge: FC<{}> = () => { 11 | const history = useHistory(); 12 | const onClickLeft = (): void => { 13 | history.goBack(); 14 | }; 15 | return ( 16 |
17 | 18 | 19 | 20 | { 22 | return 标题; 23 | }} 24 | value={(): any => { 25 | return ; 26 | }} 27 | isLink 28 | border 29 | size="large" 30 | > 31 | { 33 | return 标题; 34 | }} 35 | value={(): any => { 36 | return ; 37 | }} 38 | isLink 39 | border 40 | size="large" 41 | > 42 | 43 |
44 | ); 45 | }; 46 | 47 | export default DemoBadge; 48 | -------------------------------------------------------------------------------- /src/components/badge/index.less: -------------------------------------------------------------------------------- 1 | @import '../../style/common'; 2 | 3 | .@{prefix}badge { 4 | position: relative; 5 | display: inline-flex; 6 | 7 | &__info { 8 | position: absolute; 9 | top: 0; 10 | right: (@badge-height / 2); 11 | display: flex; 12 | flex-flow: row nowrap; 13 | align-content: center; 14 | align-items: center; 15 | justify-content: center; 16 | box-sizing: border-box; 17 | min-width: @badge-height; 18 | height: @badge-height; 19 | padding: 0 @badge-padding-horizontal; 20 | color: @badge-text-color; 21 | font-size: @badge-font-size; 22 | line-height: 1; 23 | white-space: nowrap; 24 | text-align: center; 25 | background-color: @badge-background-color; 26 | border-radius: (@badge-height / 2); 27 | transform: translate3d(100%, -50%, 0); 28 | 29 | &::after { 30 | position: absolute; 31 | top: -1px; 32 | left: -1px; 33 | z-index: -1; 34 | display: block; 35 | width: 100%; 36 | height: 100%; 37 | border: 1px solid @badge-border-color; 38 | border-radius: (@badge-height / 2) + 1px; 39 | content: ' '; 40 | } 41 | } 42 | 43 | &--is-dot &__info { 44 | right: (@badge-dot-size / 2); 45 | width: @badge-dot-size; 46 | min-width: @badge-dot-size; 47 | height: @badge-dot-size; 48 | padding: 0; 49 | border-radius: 50%; 50 | 51 | &::after { 52 | border-radius: 50%; 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/components/badge/index.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable indent */ 2 | /* eslint-disable semi */ 3 | import React, { AllHTMLAttributes, FC, PropsWithChildren } from 'react'; 4 | 5 | import createBEM, { addClass } from '../../utils/create/createBEM'; 6 | 7 | const bem = createBEM('badge'); 8 | 9 | export interface BadgeProps extends AllHTMLAttributes { 10 | /** 11 | * The number or text content. 12 | */ 13 | value?: number | string; 14 | 15 | /** 16 | * The max limit when value type is number. 17 | */ 18 | max?: number; 19 | 20 | /** 21 | * Display as a dot. 22 | * @default false 23 | */ 24 | isDot?: boolean; 25 | } 26 | 27 | const Badge: FC = (props: PropsWithChildren) => { 28 | const { className, style, value, max, isDot, children, ...attrs } = props; 29 | 30 | const info = isDot ? '' : typeof value === 'number' && max && max < value ? `${max}+` : value; 31 | 32 | const classes = bem([{ 'is-dot': isDot }]); 33 | 34 | return ( 35 |
36 | {children} 37 | {info} 38 |
39 | ); 40 | }; 41 | 42 | Badge.displayName = 'Badge'; 43 | 44 | export default Badge; 45 | -------------------------------------------------------------------------------- /src/components/button/demo/index.less: -------------------------------------------------------------------------------- 1 | @import '../../../style/common'; 2 | 3 | .demo-button { 4 | .@{prefix}-button { 5 | margin-right: 10px; 6 | margin-bottom: 10px; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/components/cell/demo/index.less: -------------------------------------------------------------------------------- 1 | // .demo-comp-cell { 2 | // padding-right: 0; 3 | // padding-left: 0; 4 | 5 | // .demo-cell + .demo-cell { 6 | // margin-top: 16px; 7 | // } 8 | 9 | // h4 { 10 | // margin: 0; 11 | // padding: 16px 16px 8px; 12 | // font-size: 18px; 13 | // } 14 | 15 | // p { 16 | // margin: 0; 17 | // padding: 8px 16px 4px; 18 | // font-size: 16px; 19 | // } 20 | 21 | // h4 + p { 22 | // padding-top: 0; 23 | // } 24 | // } 25 | -------------------------------------------------------------------------------- /src/components/cell/demo/index.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable semi */ 2 | import React from 'react'; 3 | import { Cell } from '../../../../src/index'; 4 | import { MobileCard } from '../../../../docs/mobile/components/mobile-card/index'; 5 | import DemoHeader from '../../../../docs/mobile/components/mobile-header/index'; 6 | 7 | import './index.less'; 8 | 9 | export default class DemoCell extends React.Component { 10 | render(): JSX.Element { 11 | const onClickLeft = (): void => { 12 | this.props.history.goBack(); 13 | }; 14 | const title = '标题'; 15 | const content = '内容'; 16 | 17 | // const longTitle =
sadfaasdfaasdfasdfaasdfasdfasdfasdfasd
; 22 | 23 | return ( 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 |
46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/components/checkbox/demo/index.less: -------------------------------------------------------------------------------- 1 | @import '../../../style/common'; 2 | 3 | .demo.checkbox { 4 | .@{prefix}-checkbox { 5 | margin: 10px 0; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/components/checkbox/group.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Component Checkbox Group 3 | */ 4 | import React, { useState, FC, Context, CSSProperties, MouseEventHandler } from 'react'; 5 | 6 | import createBEM, { addClass } from '../../utils/create/createBEM'; 7 | 8 | import { CheckboxType, CheckboxSize, CheckboxValue } from './types'; 9 | 10 | export interface CheckboxGroupChild { 11 | value: CheckboxValue; 12 | checked: boolean; 13 | } 14 | 15 | export interface CheckboxGroupState { 16 | type?: CheckboxType; 17 | size?: CheckboxSize; 18 | value?: CheckboxValue[]; 19 | onChange?: (value: CheckboxValue[]) => void; 20 | children?: CheckboxGroupChild[]; 21 | } 22 | 23 | export const CheckboxGroupContext: Context = React.createContext({}); 24 | 25 | const bem = createBEM('checkbox-group'); 26 | 27 | export interface CheckboxGroupProps { 28 | className?: string; 29 | style?: CSSProperties; 30 | children?: React.ReactNode; 31 | 32 | /** 33 | * The type of checkbox. 34 | */ 35 | type?: CheckboxType; 36 | 37 | /** 38 | * Size of checkbox. 39 | */ 40 | size?: CheckboxSize; 41 | 42 | /** 43 | * The value of checkbox. 44 | */ 45 | value: CheckboxValue[]; 46 | 47 | /** 48 | * When checkbox value changed. 49 | */ 50 | onChange?: (value: CheckboxValue[]) => void; 51 | 52 | /** 53 | * Optional callback when Radio Group is clicked. 54 | */ 55 | onClick?: MouseEventHandler; 56 | } 57 | 58 | const CheckboxGroup: FC = (props: CheckboxGroupProps) => { 59 | const { className, style, type, size, value, onChange, children } = props; 60 | 61 | const [checkboxGroupChildren] = useState([]); 62 | 63 | return ( 64 | 73 |
74 | {children} 75 |
76 |
77 | ); 78 | }; 79 | 80 | CheckboxGroup.displayName = 'CheckboxGroup'; 81 | 82 | export default React.memo(CheckboxGroup); 83 | -------------------------------------------------------------------------------- /src/components/checkbox/index.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Component Checkbox Entery 3 | */ 4 | import { NamedExoticComponent, PropsWithChildren } from 'react'; 5 | 6 | import InternalCheckbox from './checkbox'; 7 | import CheckboxGroup from './group'; 8 | import type { CheckboxProps } from './checkbox'; 9 | 10 | export type { CheckboxProps }; 11 | export type { CheckboxGroupProps } from './group'; 12 | 13 | export * from './types'; 14 | 15 | export interface Checkbox extends NamedExoticComponent> { 16 | Group: typeof CheckboxGroup; 17 | } 18 | 19 | const Checkbox = InternalCheckbox as Checkbox; 20 | Checkbox.Group = CheckboxGroup; 21 | 22 | export { CheckboxGroup }; 23 | 24 | export default Checkbox; 25 | -------------------------------------------------------------------------------- /src/components/checkbox/types.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * interface 3 | */ 4 | export type CheckboxType = 'default' | 'number' | 'square'; 5 | 6 | export type CheckboxSize = 'small' | 'normal' | 'large'; 7 | 8 | export type CheckboxValue = number | string; 9 | -------------------------------------------------------------------------------- /src/components/col/index.less: -------------------------------------------------------------------------------- 1 | @import '../../style/common'; 2 | 3 | .@{prefix}col { 4 | float: left; 5 | box-sizing: border-box; 6 | } 7 | 8 | .generate-col(@n, @i: 1) when (@i =< @n) { 9 | .@{prefix}col--@{i} { 10 | width: @i * (100% / 24); 11 | } 12 | 13 | .@{prefix}col--offset-@{i} { 14 | margin-left: @i * (100% / 24); 15 | } 16 | 17 | .generate-col(@n, (@i + 1)); 18 | } 19 | 20 | .generate-col(24); 21 | -------------------------------------------------------------------------------- /src/components/col/index.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Component Col 3 | */ 4 | import React, { FC, PropsWithChildren } from 'react'; 5 | 6 | import RowContext from '../row/RowContext'; 7 | 8 | import createBEM from '../../utils/create/createBEM'; 9 | import { value2DomUnit } from '../../utils/dom/unit'; 10 | 11 | const bem = createBEM('col'); 12 | 13 | export interface ColProps { 14 | span: number | string; 15 | offset?: number | string; 16 | } 17 | 18 | export const Col: FC = (props: PropsWithChildren) => { 19 | const { span, offset } = props; 20 | 21 | return ( 22 | 23 | {({ gutter }): JSX.Element => { 24 | const colStyle: React.CSSProperties = {}; 25 | 26 | if (gutter) { 27 | const gap = value2DomUnit(gutter, 0.5); 28 | colStyle.paddingLeft = gap; 29 | colStyle.paddingRight = gap; 30 | } 31 | 32 | return ( 33 |
40 | {props.children} 41 |
42 | ); 43 | }} 44 |
45 | ); 46 | }; 47 | 48 | Col.displayName = 'Col'; 49 | 50 | export default Col; 51 | -------------------------------------------------------------------------------- /src/components/collapse/demo/index.less: -------------------------------------------------------------------------------- 1 | .demo.demo-collapse { 2 | background: white; 3 | } 4 | -------------------------------------------------------------------------------- /src/components/date-time-picker/index.less: -------------------------------------------------------------------------------- 1 | @import '../../style/common.less'; 2 | 3 | .@{prefix}date-time-picker { 4 | font-size: @date-time-picker-font-size; 5 | 6 | &__range { 7 | position: relative; 8 | display: flex; 9 | align-items: center; 10 | justify-content: center; 11 | height: @date-time-picker-range-height; 12 | .@{prefix}button--large { 13 | width: auto; 14 | padding-right: @date-time-picker-padding; 15 | padding-left: @date-time-picker-padding; 16 | } 17 | 18 | &__start { 19 | position: absolute; 20 | left: 0; 21 | } 22 | 23 | &__end { 24 | position: absolute; 25 | right: 0; 26 | } 27 | 28 | &__start, 29 | &__end { 30 | padding-right: @date-time-picker-padding; 31 | padding-left: @date-time-picker-padding; 32 | 33 | &--active { 34 | color: @primary-color; 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/components/dropdown-item/index.less: -------------------------------------------------------------------------------- 1 | @import '../../style/common'; 2 | 3 | .@{prefix}dropdown-item { 4 | position: absolute; 5 | width: 100%; 6 | overflow: hidden; 7 | 8 | &--down { 9 | top: @dropdown-height; 10 | } 11 | 12 | &--up { 13 | top: 0; 14 | transform: translate3d(0, -100%, 0); 15 | } 16 | 17 | &__cell { 18 | padding: 15px 16px; 19 | .@{prefix}cell__content { 20 | width: auto; 21 | line-height: 0; 22 | } 23 | .@{prefix}cell__whitespace { 24 | flex: none; 25 | width: 0; 26 | } 27 | .@{prefix}cell__title { 28 | flex: 1; 29 | min-width: 0; 30 | overflow: hidden; 31 | white-space: nowrap; 32 | text-overflow: ellipsis; 33 | } 34 | } 35 | 36 | &__icon { 37 | width: @dropdown-icon-size; 38 | height: @dropdown-icon-size; 39 | fill: @dropdown-icon-color; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/components/dropdown/demo/index.less: -------------------------------------------------------------------------------- 1 | .demo-dropdown { 2 | padding: 68px 0 20px 0; 3 | 4 | .demo-cell-header-title { 5 | padding: 0 20px; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/components/dropdown/index.less: -------------------------------------------------------------------------------- 1 | @import '../../style/common'; 2 | 3 | .@{prefix}dropdown { 4 | position: relative; 5 | display: flex; 6 | height: @dropdown-height; 7 | background-color: @dropdown-background-color; 8 | user-select: none; 9 | 10 | &__menu { 11 | position: relative; 12 | display: flex; 13 | flex: 1; 14 | align-items: center; 15 | justify-content: center; 16 | min-width: 0; 17 | 18 | &:not(&--first) { 19 | &::after { 20 | position: absolute; 21 | top: (@dropdown-height - @dropdown-line-height) / 2; 22 | bottom: (@dropdown-height - @dropdown-line-height) / 2; 23 | left: 0; 24 | border-left: 1px solid @cell-border-color; 25 | content: ' '; 26 | pointer-events: none; 27 | } 28 | } 29 | 30 | &:active { 31 | opacity: 0.7; 32 | } 33 | 34 | &--disabled { 35 | &:active { 36 | opacity: 1; 37 | } 38 | 39 | .@{prefix}dropdown__title { 40 | color: @dropdown-title-disabled-color !important; 41 | } 42 | } 43 | } 44 | 45 | &__title { 46 | position: relative; 47 | box-sizing: border-box; 48 | max-width: 100%; 49 | padding: 0 18px 0 12px; 50 | color: @dropdown-title-default-color; 51 | font-size: @dropdown-title-font-size; 52 | 53 | &--active { 54 | color: @dropdown-title-active-color; 55 | } 56 | 57 | &::after { 58 | position: absolute; 59 | top: 7px; 60 | right: 2px; 61 | border: 3px solid; /* no */ 62 | border-color: transparent transparent currentColor currentColor; 63 | transform: rotate(-45deg); 64 | opacity: 0.8; 65 | content: ''; 66 | } 67 | 68 | &--down { 69 | &::after { 70 | top: 10px; 71 | transform: rotate(135deg); 72 | } 73 | } 74 | } 75 | 76 | &__ellipsis { 77 | overflow: hidden; 78 | white-space: nowrap; 79 | text-overflow: ellipsis; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/components/header-tab/index.less: -------------------------------------------------------------------------------- 1 | @import '../../style/common'; 2 | 3 | .@{prefix}header-tab { 4 | position: relative; 5 | display: inline-flex; 6 | flex: 1; 7 | height: @header-height; 8 | font-size: @header-tab-font-size; 9 | text-align: center; 10 | 11 | &__box { 12 | position: relative; 13 | display: flex; 14 | flex-flow: row nowrap; 15 | align-content: center; 16 | align-items: center; 17 | justify-content: center; 18 | margin: 0 auto; 19 | color: @header-tab-color; 20 | opacity: 0.8; 21 | } 22 | 23 | &--active { 24 | font-size: @header-tab-active-font-size; 25 | } 26 | 27 | &--active &__box { 28 | color: @header-tab-active-color; 29 | font-weight: 500; 30 | opacity: 1; 31 | } 32 | 33 | &--primary &__box { 34 | color: @white; 35 | } 36 | 37 | &--active&--primary &__box { 38 | font-weight: 500; 39 | opacity: 1; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/components/header-tab/index.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable semi */ 2 | import React, { FC, PropsWithChildren, CSSProperties, useContext } from 'react'; 3 | 4 | import createBEM from '../../utils/create/createBEM'; 5 | import { RenderFunction } from '../../utils/types'; 6 | 7 | import { HeaderContext } from '../header'; 8 | import { HeaderTabsContext } from '../header-tabs'; 9 | 10 | const bem = createBEM('header-tab'); 11 | 12 | export interface HeaderTabProps { 13 | className?: string; 14 | style?: CSSProperties; 15 | 16 | /** 17 | * The title of the header tab. 18 | */ 19 | title?: string | JSX.Element | RenderFunction; 20 | 21 | /** 22 | * The id name of the header tab. 23 | */ 24 | name?: string | number | boolean; 25 | } 26 | 27 | const HeaderTab: FC = (props: PropsWithChildren) => { 28 | const { className, style, title, name = '' } = props; 29 | 30 | const { type } = useContext(HeaderContext); 31 | const { activeName, setActiveName } = useContext(HeaderTabsContext); 32 | 33 | const active = name === activeName; 34 | 35 | let classes = bem([type, { active }]); 36 | if (className) { 37 | classes = `${classes} ${className}`; 38 | } 39 | 40 | return ( 41 |
setActiveName(name)} 46 | > 47 |
{typeof title === 'function' ? title({}) : title}
48 |
49 | ); 50 | }; 51 | 52 | HeaderTab.displayName = 'HeaderTab'; 53 | 54 | export default HeaderTab; 55 | -------------------------------------------------------------------------------- /src/components/header-tabs/index.less: -------------------------------------------------------------------------------- 1 | @import '../../style/common'; 2 | 3 | .@{prefix}header-tabs { 4 | position: relative; 5 | width: 100%; 6 | height: @header-height; 7 | background-color: transparent; 8 | 9 | &__box { 10 | position: relative; 11 | display: flex; 12 | } 13 | 14 | &__line { 15 | position: absolute; 16 | bottom: @header-line-bottom; 17 | left: 0; 18 | height: @header-line-height; 19 | background-color: @header-line-color; 20 | transition: @animation-duration-fast ease-in-out; 21 | transition-property: width, transform; 22 | will-change: width, transform; 23 | .okui-border-radius(1); 24 | } 25 | 26 | &--primary &__line { 27 | background-color: @white; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/components/header/demo/index.less: -------------------------------------------------------------------------------- 1 | .demo-header { 2 | .demo-fixed-placeholder { 3 | height: 44px; 4 | border: 1px dashed #ccc; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/components/header/index.less: -------------------------------------------------------------------------------- 1 | @import '../../style/common'; 2 | 3 | .@{prefix}header { 4 | position: relative; 5 | display: flex; 6 | flex-flow: row nowrap; 7 | align-content: center; 8 | align-items: center; 9 | justify-content: center; 10 | width: 100%; 11 | height: @header-height; 12 | background-color: @header-background-color; 13 | 14 | &--fixed { 15 | position: fixed; 16 | top: 0; 17 | left: 0; 18 | z-index: 1999; 19 | width: 100%; 20 | } 21 | 22 | &__icon { 23 | position: relative; 24 | width: @header-icon-size; 25 | height: @header-icon-size; 26 | vertical-align: middle; 27 | fill: @header-icon-fill; 28 | } 29 | 30 | &__left, 31 | &__right, 32 | &__title { 33 | position: relative; 34 | display: flex; 35 | flex: 2; 36 | flex-flow: row nowrap; 37 | align-content: center; 38 | align-items: center; 39 | height: 100%; 40 | } 41 | 42 | &__left { 43 | justify-content: flex-start; 44 | padding-left: @header-padding-horizontal; 45 | } 46 | 47 | &__right { 48 | justify-content: flex-end; 49 | padding-right: @header-padding-horizontal; 50 | } 51 | 52 | &__title { 53 | position: relative; 54 | display: flex; 55 | flex: 8; 56 | justify-content: center; 57 | overflow: hidden; 58 | color: @header-title-text-color; 59 | font-weight: @header-title-font-weight; 60 | font-size: @header-title-font-size; 61 | letter-spacing: 0; 62 | white-space: nowrap; 63 | text-overflow: ellipsis; 64 | } 65 | 66 | &--primary { 67 | background-color: @primary-color; 68 | } 69 | 70 | &--primary &__icon { 71 | fill: @white; 72 | } 73 | 74 | &--primary &__title { 75 | color: @white; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/components/icon/index.less: -------------------------------------------------------------------------------- 1 | @import '../../style/common'; 2 | 3 | .@{prefix}icon { 4 | display: inline-block; 5 | font-size: 0; 6 | } 7 | -------------------------------------------------------------------------------- /src/components/icon/index.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Component Icon 3 | */ 4 | import React, { FC, CSSProperties, HTMLAttributes } from 'react'; 5 | 6 | import createBEM, { addClass } from '../../utils/create/createBEM'; 7 | 8 | import { value2DomUnit } from '../../utils/dom/unit'; 9 | 10 | import { allIcons } from './all'; 11 | 12 | import type { IconMap } from './all'; 13 | 14 | export type { IconMap }; 15 | 16 | export type IconName = keyof IconMap; 17 | 18 | const bem = createBEM('icon'); 19 | 20 | export interface IconProps extends HTMLAttributes { 21 | className?: string; 22 | style?: CSSProperties; 23 | 24 | /** 25 | * Necessary 26 | */ 27 | name: IconName; 28 | 29 | /** 30 | * size. 31 | */ 32 | size?: string | number; 33 | 34 | /** 35 | * fill color 36 | */ 37 | fill?: string; 38 | 39 | /** 40 | * stroke color 41 | */ 42 | stroke?: string; 43 | } 44 | 45 | const Icon: FC = (props: IconProps) => { 46 | const { className, style = {}, name, size, fill, stroke, ...attributes } = props; 47 | 48 | if (size) { 49 | style.width = value2DomUnit(size); 50 | style.height = value2DomUnit(size); 51 | } 52 | 53 | if (fill) { 54 | style.fill = fill; 55 | } 56 | if (stroke) { 57 | style.stroke = stroke; 58 | } 59 | 60 | return ( 61 | 69 | ); 70 | }; 71 | 72 | Icon.displayName = 'Icon'; 73 | 74 | export default Icon; 75 | -------------------------------------------------------------------------------- /src/components/icon/svg/arrow-down.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/icon/svg/arrow-left.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/icon/svg/arrow-right.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/icon/svg/arrow-up.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/icon/svg/attention.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/icon/svg/check-one.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/icon/svg/check.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/icon/svg/circle.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/icon/svg/close-one.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/icon/svg/close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/icon/svg/default.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/icon/svg/error.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/icon/svg/filter.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/components/icon/svg/info.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/icon/svg/loading.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/icon/svg/partial-check.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/icon/svg/question-mark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/components/icon/svg/radio-uncheck.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/icon/svg/radio.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/icon/svg/square-check-one.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/components/icon/svg/square-uncheck.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/components/icon/svg/success.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/icon/svg/triangular.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/icon/svg/warning.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/infinite-scroll/demo/index.less: -------------------------------------------------------------------------------- 1 | .demo-infinite-scroll.demo { 2 | padding-right: 0; 3 | padding-bottom: 0; 4 | padding-left: 0; 5 | 6 | .demo-cell { 7 | .custom-infinite-scroll { 8 | li { 9 | position: relative; 10 | display: flex; 11 | align-items: center; 12 | height: 44px; 13 | padding: 0 16px; 14 | list-style: none; 15 | } 16 | 17 | li:first-of-type::before, 18 | > li::after { 19 | position: absolute; 20 | left: 0; 21 | width: 100%; 22 | height: 1px; 23 | background-color: #c8c7cc; 24 | transform: scaleY(0.5); 25 | content: ''; 26 | } 27 | 28 | li:first-of-type::before { 29 | top: 0; 30 | transform-origin: 50% 0%; 31 | } 32 | 33 | > li::after { 34 | bottom: 0; 35 | transform-origin: 50% 100%; 36 | } 37 | } 38 | 39 | h4 { 40 | padding-right: 10px; 41 | padding-left: 10px; 42 | } 43 | 44 | &--region, 45 | &--combine { 46 | height: calc(100vh - 68px); 47 | 48 | .custom-infinite-scroll { 49 | height: calc(100vh - 68px); 50 | } 51 | } 52 | } 53 | } 54 | 55 | .app.demo-infinite-scroll { 56 | width: 100%; 57 | height: auto; 58 | overflow: visible; 59 | } 60 | -------------------------------------------------------------------------------- /src/components/infinite-scroll/index.less: -------------------------------------------------------------------------------- 1 | @import '../../style/common'; 2 | 3 | .@{prefix}infinite-scroll { 4 | overflow-y: auto; 5 | 6 | &__preload { 7 | display: flex; 8 | align-items: center; 9 | justify-content: center; 10 | height: @infinite-scroll-preload-height; 11 | user-select: none; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/components/loading/demo/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Loading } from '../../../../src/index'; 3 | import DemoHeader from '../../../../docs/mobile/components/mobile-header/index'; 4 | import { MobileCard } from '../../../../docs/mobile/components/mobile-card/index'; 5 | 6 | class DemoLoading extends React.Component { 7 | render(): JSX.Element { 8 | const onClickLeft = (): void => { 9 | this.props.history.goBack(); 10 | }; 11 | return ( 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 加载中... 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 |
36 | ); 37 | } 38 | } 39 | 40 | export default DemoLoading; 41 | -------------------------------------------------------------------------------- /src/components/loading/icon/circle.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/loading/icon/default.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/loading/index.less: -------------------------------------------------------------------------------- 1 | @import '../../style/common'; 2 | 3 | .@{prefix}loading { 4 | display: inline-flex; 5 | align-items: center; 6 | justify-content: center; 7 | font-size: 0; 8 | 9 | &__icon { 10 | display: inline-block; 11 | width: @loading-icon-height; 12 | height: @loading-icon-height; 13 | fill: @primary-color; 14 | } 15 | 16 | &__text { 17 | display: inline-block; 18 | margin-left: 8px; 19 | color: @text-color-4; 20 | font-size: @loading-font-size; 21 | } 22 | 23 | &--vertical { 24 | flex-direction: column; 25 | 26 | .okee-loading__text { 27 | margin: 8px 0 0; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/components/loading/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createNamespace } from '../../utils/create'; 3 | import { value2DomUnit } from '../../utils/dom/unit'; 4 | 5 | import { addClass, prefix } from '../../utils/create/createBEM'; 6 | 7 | import Icon from '../icon'; 8 | import type { IconName } from '../icon'; 9 | import { upperCamelize } from '../../utils/format/string'; 10 | 11 | const [bem] = createNamespace('loading'); 12 | 13 | export type LoadingType = 'default' | 'circle'; 14 | 15 | export interface LoadingProps { 16 | type?: LoadingType; 17 | text?: string; 18 | size?: string | number; 19 | vertical?: boolean; 20 | className?: string; 21 | onClick?: React.MouseEventHandler; 22 | children?: any; 23 | style?: React.CSSProperties; 24 | } 25 | 26 | function Loading(props: LoadingProps): JSX.Element { 27 | const { type, text, vertical, size, children, style, className } = props; 28 | 29 | const loadingStyle = style || {}; 30 | 31 | if (size) { 32 | loadingStyle.width = value2DomUnit(size); 33 | loadingStyle.height = value2DomUnit(size); 34 | } 35 | 36 | const message = children ? children : text; 37 | 38 | return ( 39 |
40 | 45 | {message &&
{message}
} 46 |
47 | ); 48 | } 49 | 50 | const defaultProps: LoadingProps = { 51 | type: 'default', 52 | }; 53 | 54 | Loading.defaultProps = defaultProps; 55 | 56 | export default Loading; 57 | -------------------------------------------------------------------------------- /src/components/locale-context/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import local from '../../locale/zh-CN'; 3 | 4 | export type Local = 'dialog' | 'picker' | 'datetimePicker' | 'field'; 5 | 6 | export type LocalType = Partial>; 7 | 8 | const localContext: LocalType = local; 9 | 10 | const LocaleContext = React.createContext(localContext); 11 | 12 | export default LocaleContext; 13 | -------------------------------------------------------------------------------- /src/components/modal/confirm.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | 4 | import { ModalProps } from './index'; 5 | import ModalDialog, { ModalDialogProps } from './modal-dialog'; 6 | 7 | export default function withConfirm(showCancelButton: boolean): (options: ModalProps) => void { 8 | return function (options: ModalProps): void { 9 | const { title, message } = options; 10 | 11 | if (!title && !message) { 12 | console.log('Must specify either an alert title, or message, or both'); 13 | return; 14 | } 15 | 16 | if (typeof window === 'undefined') { 17 | return; 18 | } 19 | const div: HTMLElement = document.createElement('div'); 20 | document.body.appendChild(div); 21 | 22 | function destroy(): void { 23 | const unmountResult = ReactDOM.unmountComponentAtNode(div); 24 | if (unmountResult && div.parentNode) { 25 | div.parentNode.removeChild(div); 26 | } 27 | } 28 | 29 | function render(options: ModalDialogProps): void { 30 | ReactDOM.render(, div); 31 | } 32 | 33 | function close(): void { 34 | /* eslint-disable @typescript-eslint/no-use-before-define */ 35 | currentOptions = Object.assign({}, currentOptions, { 36 | value: false, 37 | afterClose: destroy, 38 | }); 39 | 40 | render(currentOptions); 41 | } 42 | 43 | let currentOptions = { 44 | ...options, 45 | close, 46 | value: true, 47 | showConfirmButton: true, 48 | showCancelButton, 49 | }; 50 | 51 | render(currentOptions); 52 | }; 53 | } 54 | -------------------------------------------------------------------------------- /src/components/modal/demo/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Modal, Cell } from '../../../../src/index'; 3 | import DemoHeader from '../../../../docs/mobile/components/mobile-header/index'; 4 | import { MobileCard } from '../../../../docs/mobile/components/mobile-card/index'; 5 | 6 | const { alert, confirm } = Modal; 7 | 8 | class DemoModal extends React.Component { 9 | onClick(): void { 10 | alert({ 11 | message: 'OKUI-轻量、可靠的React 组件库', 12 | }); 13 | } 14 | 15 | onClick1(): void { 16 | confirm({ 17 | message: 'OKUI-轻量、可靠的React 组件库', 18 | onCancel: (): void => {}, 19 | }); 20 | } 21 | 22 | onClick2(): void { 23 | alert({ 24 | message: 'OKUI-轻量、可靠的React 组件库', 25 | title: 'OKUI', 26 | }); 27 | } 28 | 29 | onClick3(): void { 30 | confirm({ 31 | message: 'OKUI-轻量、可靠的React 组件库', 32 | title: 'OKUI', 33 | }); 34 | } 35 | 36 | render(): JSX.Element { 37 | const onClickLeft = (): void => { 38 | this.props.history.goBack(); 39 | }; 40 | return ( 41 |
42 | 43 | 44 | 45 | this.onClick()}> 46 | this.onClick1()}> 47 | 48 | 49 | 50 | this.onClick2()}> 51 | this.onClick3()}> 52 | 53 |
54 | ); 55 | } 56 | } 57 | 58 | export default DemoModal; 59 | -------------------------------------------------------------------------------- /src/components/modal/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Diloag from '../dialog'; 3 | import type { DialogProps } from '../dialog'; 4 | 5 | import withConfirm from './confirm'; 6 | 7 | export type ModalProps = DialogProps; 8 | 9 | function Modal(props: ModalProps): JSX.Element { 10 | return ; 11 | } 12 | 13 | Modal.alert = withConfirm(false); 14 | Modal.confirm = withConfirm(true); 15 | 16 | export default Modal; 17 | -------------------------------------------------------------------------------- /src/components/modal/modal-dialog.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Diloag from '../dialog'; 3 | 4 | import { ModalProps } from './index'; 5 | 6 | export interface ModalDialogProps extends ModalProps { 7 | value: boolean; 8 | close: (...args: any[]) => void; 9 | afterClose?: () => void; 10 | showConfirmButton?: boolean; 11 | showCancelButton?: boolean; 12 | } 13 | 14 | function ModalDialog(props: ModalDialogProps): JSX.Element { 15 | const { 16 | value, 17 | title, 18 | message, 19 | showConfirmButton, 20 | showCancelButton, 21 | close, 22 | onConfirm, 23 | onCancel, 24 | afterClose, 25 | ...extra 26 | } = props; 27 | 28 | return ( 29 | { 36 | close(); 37 | onConfirm && onConfirm(); 38 | }} 39 | onCancel={(): void => { 40 | close(); 41 | onCancel && onCancel(); 42 | }} 43 | afterClose={afterClose} 44 | {...extra} 45 | /> 46 | ); 47 | } 48 | 49 | export default ModalDialog; 50 | -------------------------------------------------------------------------------- /src/components/notice-bar/demo/index.less: -------------------------------------------------------------------------------- 1 | .notice-bar-demo { 2 | margin-bottom: 10px; 3 | 4 | .right-slot { 5 | display: inline-flex; 6 | align-items: center; 7 | 8 | img { 9 | margin-right: 8px; 10 | } 11 | } 12 | 13 | .omui-notice-bar__content { 14 | flex: 1; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/components/notice-bar/index.less: -------------------------------------------------------------------------------- 1 | @import '../../style/common'; 2 | 3 | .@{prefix}notice-bar { 4 | position: relative; 5 | display: flex; 6 | padding: @notice-bar-padding; 7 | color: @notice-bar-color; 8 | font-size: @notice-bar-font-size; 9 | background: @warning-color-5; 10 | 11 | &__content { 12 | display: -webkit-box; 13 | overflow: hidden; 14 | line-height: @notice-bar-height; 15 | text-overflow: ellipsis; 16 | word-wrap: break-word; 17 | word-break: break-all; 18 | -webkit-box-orient: vertical; 19 | 20 | &--right { 21 | margin-right: 24px; 22 | } 23 | } 24 | 25 | &__left { 26 | flex-shrink: 0; 27 | width: 16px; 28 | height: 16px; 29 | margin-right: 8px; 30 | fill: @warning-color; 31 | } 32 | 33 | &--none { 34 | display: none; 35 | } 36 | 37 | &--primary { 38 | background: @primary-color-5; 39 | 40 | .@{prefix}notice-bar__left { 41 | fill: @primary-color; 42 | } 43 | } 44 | 45 | &--success { 46 | background: @success-color-5; 47 | 48 | .@{prefix}notice-bar__left { 49 | fill: @success-color; 50 | } 51 | } 52 | 53 | &--danger { 54 | background: @danger-color-5; 55 | 56 | .@{prefix}notice-bar__left { 57 | fill: @danger-color; 58 | } 59 | } 60 | 61 | &__right { 62 | position: absolute; 63 | top: 11px; 64 | right: 16px; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/components/overlay/index.less: -------------------------------------------------------------------------------- 1 | @import '../../style/common'; 2 | 3 | .@{prefix}overlay { 4 | position: fixed; 5 | top: 0; 6 | left: 0; 7 | width: 100%; 8 | height: 100%; 9 | background-color: @overlay-background-color; 10 | } 11 | -------------------------------------------------------------------------------- /src/components/overlay/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { CSSTransition } from 'react-transition-group'; 3 | 4 | import createBEM, { addClass, prefix } from '../../utils/create/createBEM'; 5 | 6 | export interface OverlayProps { 7 | show?: boolean; 8 | zIndex?: number | string; 9 | className?: any; 10 | customStyle?: any; 11 | onClick?: React.MouseEventHandler; 12 | } 13 | 14 | const bem = createBEM('overlay'); 15 | 16 | function Overlay(props: OverlayProps): any { 17 | const { show, zIndex, className, customStyle, onClick } = props; 18 | 19 | const style: { [key: string]: any } = { 20 | zIndex: zIndex, 21 | ...customStyle, 22 | }; 23 | 24 | return ( 25 | 26 |
27 | 28 | ); 29 | } 30 | 31 | export default Overlay; 32 | -------------------------------------------------------------------------------- /src/components/picker-column/demo/index.tsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oceanengine/okeedesign-mobile-react/4b873fa5f335aedc192daa744dd78b452f6d26ed/src/components/picker-column/demo/index.tsx -------------------------------------------------------------------------------- /src/components/picker-column/index.less: -------------------------------------------------------------------------------- 1 | @import '../../style/common.less'; 2 | 3 | .@{prefix}picker-column { 4 | flex: 1; 5 | min-width: 0; 6 | overscroll-behavior: none; 7 | 8 | &__wrapper { 9 | height: 100%; 10 | } 11 | 12 | &__item { 13 | display: flex; 14 | align-items: center; 15 | justify-content: center; 16 | height: @picker-column-option-height; 17 | padding: @picker-column-item-padding; 18 | color: @picker-column-color; 19 | font-weight: @picker-column-font-weight; 20 | font-size: @picker-column-font-size; 21 | text-align: center; 22 | 23 | &--active { 24 | color: @picker-column-active-color; 25 | font-weight: @picker-column-active-font-weight; 26 | font-size: @picker-column-active-font-size; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/components/picker/index.less: -------------------------------------------------------------------------------- 1 | @import '../../style/common.less'; 2 | 3 | .@{prefix}picker { 4 | background-color: white; 5 | 6 | &__content { 7 | position: relative; 8 | display: flex; 9 | overflow: hidden; 10 | } 11 | 12 | &__mask { 13 | position: absolute; 14 | z-index: 2; 15 | width: 100%; 16 | height: 100%; 17 | background-image: linear-gradient(to bottom, @white 0%, rgba(255, 255, 255, 0.2) 100%), 18 | linear-gradient(to top, @white 0%, rgba(255, 255, 255, 0.2) 100%); 19 | background-repeat: no-repeat; 20 | background-position: top, bottom; 21 | background-size: 100% calc((100% - @picker-indicator-height) / 2); 22 | transform: translate3d(0, 0, 0); 23 | pointer-events: none; 24 | } 25 | 26 | &__indicator { 27 | position: absolute; 28 | top: 50%; 29 | left: -50%; 30 | z-index: 3; 31 | width: 200%; 32 | height: (@picker-indicator-height * 2); 33 | border-top: 1px solid @picker-indicator-color; 34 | border-bottom: 1px solid @picker-indicator-color; 35 | transform: translate3d(0, -50%, 0) scale(0.5); 36 | pointer-events: none; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/components/popup-drill-down-item/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { HTMLAttributes, useContext } from 'react'; 2 | import createBEM, { addClass } from '../../utils/create/createBEM'; 3 | import { RenderFunction } from '../../utils/types'; 4 | import { PopupDrillDownContext, PopupDrillDownState } from '../popup-drill-down/context'; 5 | 6 | export interface PopupDrillDownItemProps extends Omit, 'title'> { 7 | title?: string | JSX.Element | RenderFunction; 8 | 9 | left?: string | JSX.Element | RenderFunction; 10 | right?: string | JSX.Element | RenderFunction<(() => void) | void>; 11 | 12 | active?: boolean; 13 | target?: boolean; 14 | children?: string | JSX.Element | RenderFunction; 15 | } 16 | 17 | const bem = createBEM('popup-drill-down-item'); 18 | 19 | function PopupDrillDownItem(props: PopupDrillDownItemProps): JSX.Element { 20 | const { children, className, active = false, target = false } = props; 21 | 22 | const { ...methods } = useContext(PopupDrillDownContext); 23 | 24 | const childProps = { 25 | ...methods, 26 | } as PopupDrillDownState; 27 | 28 | return ( 29 |
30 | {active &&
} 31 | {typeof children === 'function' ? children(childProps) : children} 32 |
33 | ); 34 | } 35 | 36 | export default PopupDrillDownItem; 37 | -------------------------------------------------------------------------------- /src/components/popup-drill-down/context.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export type PopupDrillDownState = { 4 | forward: () => void; 5 | back: () => void; 6 | closePopup: () => void; 7 | }; 8 | 9 | export const PopupDrillDownContext = React.createContext({ 10 | forward() {}, 11 | back() {}, 12 | closePopup() {}, 13 | }); 14 | -------------------------------------------------------------------------------- /src/components/popup-drill-down/demo/index.less: -------------------------------------------------------------------------------- 1 | .shopping-drill-down { 2 | height: 80%; 3 | 4 | .shopping-drill-down-item { 5 | display: flex; 6 | flex-direction: column; 7 | align-items: center; 8 | height: 100%; 9 | overflow: auto; 10 | 11 | &-placeholder { 12 | flex: none; 13 | width: 100%; 14 | height: 100px; 15 | background-color: pink; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/components/popup/demo/index.less: -------------------------------------------------------------------------------- 1 | .demo-popup-basic { 2 | padding: 38px; 3 | font-size: 18px; 4 | border-radius: 4px; 5 | 6 | .popup-img { 7 | width: 124px; 8 | } 9 | } 10 | 11 | .demo-popup-vh { 12 | height: 100vh; 13 | line-height: 100vh; 14 | } 15 | 16 | .demo.popup { 17 | position: relative; 18 | margin: 0; 19 | padding: 0 16px; 20 | 21 | .popup-box { 22 | .okee-button { 23 | margin-right: 10px; 24 | } 25 | } 26 | } 27 | 28 | .demo-popup-top, 29 | .demo-popup-bottom { 30 | height: 40vh; 31 | font-size: 0; 32 | line-height: 40vh; 33 | text-align: center; 34 | 35 | img { 36 | vertical-align: middle; 37 | } 38 | } 39 | 40 | .demo-popup-left, 41 | .demo-popup-right { 42 | display: flex; 43 | align-items: center; 44 | justify-content: center; 45 | width: 60vw; 46 | height: inherit; 47 | } 48 | -------------------------------------------------------------------------------- /src/components/progress/demo/index.less: -------------------------------------------------------------------------------- 1 | .demo.demo-progress { 2 | .demo-cell { 3 | > div + div { 4 | margin-top: 4px; 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/components/pull-refresh/demo/index.less: -------------------------------------------------------------------------------- 1 | .demo-pull-refresh { 2 | .content { 3 | display: flex; 4 | flex-direction: column; 5 | align-items: center; 6 | height: 30vh; 7 | padding: 32px 0 16px 0; 8 | } 9 | 10 | .omui-pull-refresh { 11 | background: rgb(255, 255, 255); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/components/radio/demo/index.less: -------------------------------------------------------------------------------- 1 | .demo.radio { 2 | .omui-radio { 3 | justify-content: flex-end; 4 | margin: 8px 0; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/components/radio/group.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Component Radio Group 3 | */ 4 | import React, { FC, Context, CSSProperties, MouseEventHandler } from 'react'; 5 | 6 | import createBEM, { addClass } from '../../utils/create/createBEM'; 7 | 8 | import { RadioType, RadioSize, RadioLabelPosition, RadioValue } from './types'; 9 | 10 | export interface RadioGroupState { 11 | type?: RadioType; 12 | size?: RadioSize; 13 | value?: RadioValue; 14 | labelPosition?: RadioLabelPosition; 15 | onChange?: (value: RadioValue) => void; 16 | } 17 | 18 | export const RadioGroupContext: Context = React.createContext({}); 19 | 20 | const bem = createBEM('radio-group'); 21 | 22 | export interface RadioGroupProps { 23 | className?: string; 24 | style?: CSSProperties; 25 | children?: React.ReactNode; 26 | 27 | /** 28 | * The type of shape. 29 | */ 30 | type?: RadioType; 31 | 32 | /** 33 | * Size of radio 34 | */ 35 | size?: RadioSize; 36 | 37 | /** 38 | * The pisition of label 39 | */ 40 | labelPosition?: RadioLabelPosition; 41 | 42 | /** 43 | * The value of radio. 44 | */ 45 | value: RadioValue; 46 | 47 | /** 48 | * When radio value changed. 49 | */ 50 | onChange?: (value: RadioValue) => void; 51 | 52 | /** 53 | * Optional callback when Radio Group is clicked. 54 | */ 55 | onClick?: MouseEventHandler; 56 | } 57 | 58 | const RadioGroup: FC = (props: RadioGroupProps) => { 59 | const { type, size, labelPosition, value, onChange } = props; 60 | 61 | return ( 62 | 71 |
72 | {props.children} 73 |
74 |
75 | ); 76 | }; 77 | 78 | RadioGroup.displayName = 'RadioGroup'; 79 | 80 | export default React.memo(RadioGroup); 81 | -------------------------------------------------------------------------------- /src/components/radio/icon.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Component Radio Icon 3 | */ 4 | import React, { FC } from 'react'; 5 | 6 | import Icon from '../icon'; 7 | 8 | import { RadioType } from './types'; 9 | 10 | export interface RadioIconProps { 11 | className?: string; 12 | 13 | /** 14 | * type of shape. 15 | */ 16 | type: RadioType; 17 | 18 | /** 19 | * value of radio. 20 | */ 21 | checked: boolean; 22 | } 23 | 24 | const RadioIcon: FC = (props: RadioIconProps) => { 25 | const { className, type, checked } = props; 26 | 27 | if (type === 'dot') { 28 | return checked ? ( 29 | 30 | ) : ( 31 | 32 | ); 33 | } 34 | 35 | if (type === 'hook') { 36 | return checked ? : ; 37 | } 38 | 39 | if (type === 'circle') { 40 | return checked ? ( 41 | 42 | ) : ( 43 | 44 | ); 45 | } 46 | 47 | return checked ? : ; 48 | }; 49 | 50 | RadioIcon.displayName = 'RadioIcon'; 51 | 52 | export default RadioIcon; 53 | -------------------------------------------------------------------------------- /src/components/radio/index.less: -------------------------------------------------------------------------------- 1 | @import '../../style/common'; 2 | 3 | .@{prefix}radio { 4 | display: flex; 5 | align-items: center; 6 | overflow: hidden; 7 | user-select: none; 8 | 9 | &__input { 10 | display: none; 11 | } 12 | 13 | &__label { 14 | flex: 1; 15 | margin-left: @radio-normal-label-margin; 16 | color: @radio-checked-label-color; 17 | font-size: @radio-normal-font-size; 18 | line-height: @radio-normal-size; 19 | } 20 | 21 | &__icon { 22 | display: block; 23 | flex-shrink: 0; 24 | width: @radio-normal-size; 25 | height: @radio-normal-size; 26 | fill: @radio-checked-icon-color; 27 | } 28 | 29 | &--checked &__icon { 30 | fill: @radio-checked-icon-active-color; 31 | } 32 | 33 | &--checked &__label { 34 | color: @radio-checked-label-active-color; 35 | } 36 | 37 | &--disabled &__icon { 38 | fill: @radio-disabled-icon-color; 39 | } 40 | 41 | &--disabled &__label { 42 | color: @radio-disabled-label-color; 43 | } 44 | 45 | &--disabled&--checked &__icon { 46 | fill: @radio-disabled-icon-active-color; 47 | } 48 | 49 | &--small &__icon { 50 | width: @radio-small-size; 51 | height: @radio-small-size; 52 | } 53 | 54 | &--small &__label { 55 | margin-left: @radio-small-label-margin; 56 | font-size: @radio-small-font-size; 57 | line-height: @radio-small-size; 58 | } 59 | 60 | &--large &__icon { 61 | width: @radio-large-size; 62 | height: @radio-large-size; 63 | } 64 | 65 | &--large &__label { 66 | margin-left: @radio-large-label-margin; 67 | font-size: @radio-large-font-size; 68 | line-height: @radio-large-size; 69 | } 70 | 71 | &--left &__label { 72 | margin-right: @radio-normal-label-margin; 73 | margin-left: 0; 74 | text-align: right; 75 | } 76 | 77 | &--small&--left &__label { 78 | margin-right: @radio-small-label-margin; 79 | margin-left: 0; 80 | } 81 | 82 | &--large&--left &__label { 83 | margin-right: @radio-large-label-margin; 84 | margin-left: 0; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/components/radio/index.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Component Radio Entery 3 | */ 4 | import { NamedExoticComponent, PropsWithChildren } from 'react'; 5 | 6 | export * from './types'; 7 | 8 | import InternalRadio from './radio'; 9 | import type { RadioProps } from './radio'; 10 | import RadioGroup from './group'; 11 | 12 | export type { RadioGroupProps } from './group'; 13 | 14 | export type { RadioProps }; 15 | 16 | export interface Radio extends NamedExoticComponent> { 17 | Group: typeof RadioGroup; 18 | } 19 | 20 | const Radio = InternalRadio as Radio; 21 | Radio.Group = RadioGroup; 22 | 23 | export { RadioGroup }; 24 | 25 | export default Radio; 26 | -------------------------------------------------------------------------------- /src/components/radio/types.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * interface 3 | */ 4 | export type RadioType = 'default' | 'primary' | 'dot' | 'hook' | 'circle'; 5 | 6 | export type RadioSize = 'small' | 'normal' | 'large'; 7 | 8 | export type RadioLabelPosition = 'left' | 'right'; 9 | 10 | export type RadioValue = number | string; 11 | -------------------------------------------------------------------------------- /src/components/row/RowContext.tsx: -------------------------------------------------------------------------------- 1 | import React, { Context } from 'react'; 2 | 3 | export interface RowContextState { 4 | gutter?: number | string; 5 | } 6 | 7 | const RowContext: Context = React.createContext({}); 8 | 9 | export default RowContext; 10 | -------------------------------------------------------------------------------- /src/components/row/demo/index.less: -------------------------------------------------------------------------------- 1 | .demo.row { 2 | .omui-row { 3 | .omui-col { 4 | height: 32px; 5 | margin-bottom: 8px; 6 | color: #fff; 7 | font-size: 16px; 8 | line-height: 2; 9 | text-align: center; 10 | background-clip: content-box; 11 | 12 | &:nth-child(odd) { 13 | background-color: #58a0ff; 14 | } 15 | 16 | &:nth-child(even) { 17 | background-color: #a1caff; 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/components/row/index.less: -------------------------------------------------------------------------------- 1 | @import '../../style/common'; 2 | 3 | .@{prefix}row { 4 | &::after { 5 | display: table; 6 | clear: both; 7 | content: ""; 8 | } 9 | 10 | &--flex { 11 | display: flex; 12 | 13 | &::after { 14 | display: none; 15 | } 16 | } 17 | 18 | &--justify-center { 19 | justify-content: center; 20 | } 21 | 22 | &--justify-end { 23 | justify-content: flex-end; 24 | } 25 | 26 | &--justify-space-between { 27 | justify-content: space-between; 28 | } 29 | 30 | &--justify-space-around { 31 | justify-content: space-around; 32 | } 33 | 34 | &--align-center { 35 | align-items: center; 36 | } 37 | 38 | &--align-bottom { 39 | align-items: flex-end; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/components/row/index.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Component Row 3 | */ 4 | import React, { FC, PropsWithChildren } from 'react'; 5 | 6 | import RowContext from './RowContext'; 7 | 8 | import createBEM from '../../utils/create/createBEM'; 9 | import { value2DomUnit } from '../../utils/dom/unit'; 10 | 11 | const bem = createBEM('row'); 12 | 13 | export interface RowProps { 14 | type?: 'flex'; 15 | align?: string; 16 | justify?: string; 17 | gutter?: number | string; 18 | } 19 | 20 | export const Row: FC = (props: PropsWithChildren) => { 21 | const { type, align, justify, gutter } = props; 22 | 23 | const flex = type === 'flex'; 24 | 25 | const rowStyle: React.CSSProperties = {}; 26 | 27 | if (gutter) { 28 | const gap = value2DomUnit(gutter, 0.5); 29 | 30 | rowStyle.marginLeft = `-${gap}`; 31 | rowStyle.marginRight = `-${gap}`; 32 | } 33 | 34 | return ( 35 | 36 |
44 | {props.children} 45 |
46 |
47 | ); 48 | }; 49 | 50 | Row.displayName = 'Row'; 51 | 52 | export default Row; 53 | -------------------------------------------------------------------------------- /src/components/search/demo/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { useHistory } from 'react-router-dom'; 3 | import { Search } from '../../../../src/index'; 4 | import DemoHeader from '../../../../docs/mobile/components/mobile-header/index'; 5 | import { MobileCard } from '../../../../docs/mobile/components/mobile-card/index'; 6 | 7 | function DemoSearch(): JSX.Element { 8 | const history = useHistory(); 9 | const onClickLeft = (): void => { 10 | history.goBack(); 11 | }; 12 | const [baseValue, setBaseValue] = useState(''); 13 | const [cancelValue, setCancelValue] = useState(''); 14 | const [styleValue, setStyleValue] = useState(''); 15 | // const onSearch = (value: string) => { 16 | // alert('search ' + value); 17 | // } 18 | return ( 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 34 | 35 | 36 | 37 | 38 | 39 |
40 | ); 41 | } 42 | 43 | export default DemoSearch; 44 | -------------------------------------------------------------------------------- /src/components/search/index.less: -------------------------------------------------------------------------------- 1 | @import '../../style/common'; 2 | 3 | .@{prefix}search { 4 | display: flex; 5 | align-items: center; 6 | box-sizing: border-box; 7 | width: 100%; 8 | height: @search-height; 9 | padding: 0 @search-padding; 10 | background-color: @white; 11 | 12 | &__content { 13 | position: relative; 14 | display: flex; 15 | flex: 1; 16 | align-items: center; 17 | height: @search-content-height; 18 | padding: 0 @search-content-padding; 19 | color: @search-color; 20 | font-size: @search-font-size; 21 | background-color: @search-content-background-color; 22 | 23 | &--round { 24 | border-radius: @search-content-border-radius; 25 | } 26 | 27 | &--square { 28 | border-radius: @search-content-square-border-radius; 29 | } 30 | } 31 | 32 | &__btn { 33 | flex: none; 34 | margin-left: @search-content-padding; 35 | } 36 | 37 | &__icon { 38 | width: @search-icon-height; 39 | height: @search-icon-height; 40 | margin-right: @search-icon-margin; 41 | } 42 | 43 | // override search style 44 | // https://stackoverflow.com/questions/9421551/how-do-i-remove-all-default-webkit-search-field-styling 45 | &__input { 46 | &::-webkit-search-decoration, 47 | &::-webkit-search-cancel-button, 48 | &::-webkit-search-results-button, 49 | &::-webkit-search-results-decoration { 50 | appearance: none; 51 | } 52 | 53 | box-sizing: border-box; 54 | width: 100%; 55 | padding-right: @search-icon-height; 56 | line-height: @search-line-height; 57 | background-color: transparent; 58 | border: none; 59 | outline: none; 60 | caret-color: @search-caret-color; 61 | 62 | ::placeholder { 63 | color: @search-placeholder-color; 64 | } 65 | } 66 | 67 | &__clear { 68 | position: absolute; 69 | right: @search-content-padding; 70 | width: @search-font-size; 71 | height: @search-font-size; 72 | fill: @search-icon-fill; 73 | } 74 | 75 | &--large { 76 | .@{prefix}search { 77 | &__content { 78 | height: @search-content-large-height; 79 | } 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/components/sticky/demo/index.less: -------------------------------------------------------------------------------- 1 | 2 | .demo-button { 3 | .omui-button { 4 | margin-right: 10px; 5 | margin-bottom: 10px; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/components/sticky/demo/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useMemo } from 'react'; 2 | import { Sticky, Button, NoticeBar } from '../../../../src/index'; 3 | import { useHistory } from 'react-router-dom'; 4 | import DemoHeader from '../../../../docs/mobile/components/mobile-header/index'; 5 | import { MobileCard } from '../../../../docs/mobile/components/mobile-card/index'; 6 | import './index.less'; 7 | 8 | function DemoSticky(): JSX.Element { 9 | const history = useHistory(); 10 | const onClickLeft = (): void => { 11 | history.goBack(); 12 | }; 13 | 14 | const StickyContent = useMemo(() => { 15 | // return ; 16 | return Sticky Header; 17 | }, []); 18 | 19 | const target = useCallback(() => { 20 | return document.querySelector('.app'); 21 | }, []); 22 | 23 | return ( 24 |
25 | {/* */} 26 | 27 |
28 |
29 | 30 | 31 | 32 | {StickyContent} 33 | 34 | 35 | 36 |
37 |
38 |
39 |
40 | ); 41 | } 42 | 43 | export default DemoSticky; 44 | -------------------------------------------------------------------------------- /src/components/sticky/index.less: -------------------------------------------------------------------------------- 1 | @import '../../style/common'; 2 | @import '../../style/compat.less'; 3 | -------------------------------------------------------------------------------- /src/components/swipe-cell/demo/index.less: -------------------------------------------------------------------------------- 1 | .demo-swipe-cell { 2 | // h4 { 3 | // margin: 0; 4 | // padding: 16px 0; 5 | // } 6 | 7 | // .demo-cell { 8 | // margin-bottom: 16px; 9 | // } 10 | 11 | // .commands { 12 | // display: flex; 13 | // flex-flow: row nowrap; 14 | // justify-content: space-between; 15 | // padding: 8px 0; 16 | // } 17 | .icon { 18 | width: 24px; 19 | height: 24px; 20 | fill: rgb(102, 102, 102); 21 | } 22 | 23 | .left-two { 24 | display: flex; 25 | align-items: center; 26 | width: 80px; 27 | height: 100%; 28 | } 29 | 30 | .icon-box { 31 | display: flex; 32 | flex: 1; 33 | align-items: center; 34 | justify-content: center; 35 | width: 40px; 36 | height: 100%; 37 | text-align: center; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/components/swipe-cell/index.less: -------------------------------------------------------------------------------- 1 | @import '../../style/common'; 2 | 3 | .@{prefix}swipe-cell { 4 | position: relative; 5 | overflow: hidden; 6 | 7 | &__wrapper { 8 | position: relative; 9 | transform: translate3d(0, 0, 0); 10 | will-change: transform; 11 | } 12 | 13 | &__left, 14 | &__right { 15 | position: absolute; 16 | top: 0; 17 | bottom: 0; 18 | height: 100%; 19 | } 20 | 21 | &__left { 22 | left: 0; 23 | transform: translate3d(-100%, 0, 0); 24 | } 25 | 26 | &__right { 27 | right: 0; 28 | transform: translate3d(100%, 0, 0); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/components/swipe/demo/index.less: -------------------------------------------------------------------------------- 1 | 2 | .demo-swipe { 3 | .swipe-item { 4 | height: 80px; 5 | line-height: 80px; 6 | text-align: center; 7 | } 8 | 9 | .omui-swipe__track { 10 | .swipe-item { 11 | color: #fff; 12 | } 13 | 14 | &:nth-child(odd) { 15 | .swipe-item { 16 | background: #58a0ff; 17 | } 18 | } 19 | 20 | &:nth-child(even) { 21 | .swipe-item { 22 | background: #a1caff; 23 | } 24 | } 25 | } 26 | 27 | .custom-indicator { 28 | position: absolute; 29 | right: 0; 30 | bottom: 0; 31 | padding: 4px 6px; 32 | background: rgba(255, 255, 255, 0.6); 33 | } 34 | 35 | .button-wrap { 36 | display: flex; 37 | flex-flow: row nowrap; 38 | justify-content: center; 39 | margin-top: 16px; 40 | 41 | > *:not(:last-child) { 42 | margin-right: 4px; 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/components/swipe/index.less: -------------------------------------------------------------------------------- 1 | @import '../../style/common'; 2 | 3 | .@{prefix}swipe { 4 | position: relative; 5 | overflow: hidden; 6 | 7 | &__track { 8 | position: relative; 9 | display: flex; 10 | flex-flow: row nowrap; 11 | align-content: stretch; 12 | align-items: stretch; 13 | justify-content: flex-start; 14 | width: 100%; 15 | height: 100%; 16 | transform: translate3d(0, 0, 0); 17 | will-change: transform; 18 | } 19 | 20 | &__indicators { 21 | position: absolute; 22 | right: 0; 23 | bottom: @swipe-indicators-bottom; 24 | left: 0; 25 | display: flex; 26 | flex-flow: row nowrap; 27 | align-content: center; 28 | align-items: center; 29 | justify-content: center; 30 | } 31 | 32 | &__indicator { 33 | position: relative; 34 | flex: none; 35 | width: @swipe-indicator-width; 36 | height: @swipe-indicator-height; 37 | background-color: @swipe-indicator-color; 38 | border-radius: @swipe-indicator-border-radius; 39 | opacity: @swipe-indicator-opacity; 40 | 41 | &:not(:last-child) { 42 | margin-right: @swipe-indicator-gap; 43 | } 44 | 45 | &--active { 46 | background-color: @swipe-indicator-color-active; 47 | opacity: @swipe-indicator-opacity-active; 48 | } 49 | } 50 | } 51 | 52 | .@{prefix}swipe-item { 53 | position: relative; 54 | flex: none; 55 | width: 100%; 56 | height: 100%; 57 | overflow: hidden; 58 | } 59 | -------------------------------------------------------------------------------- /src/components/switch/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useState } from 'react'; 2 | import { createNamespace } from '../../utils/create'; 3 | import Loading from '../loading'; 4 | 5 | import { addClass } from '../../utils/create/createBEM'; 6 | 7 | const [bem] = createNamespace('switch'); 8 | 9 | export type SwitchSize = 'normal' | 'large'; 10 | 11 | export type SwitchProps = { 12 | size?: SwitchSize; 13 | value: boolean; 14 | disabled?: boolean; 15 | loading?: boolean; 16 | className?: string; 17 | name?: string; 18 | onClick?: (value: boolean) => void; 19 | onChange?: (value: boolean) => void; 20 | }; 21 | 22 | export const Switch: FC = (props: SwitchProps) => { 23 | const { size, value, disabled, loading, className, name } = props; 24 | 25 | const [switchValue, setSwitchValue] = useState(value); 26 | 27 | function onClick(): void { 28 | if (!disabled && !loading) { 29 | setSwitchValue(!switchValue); 30 | if (props.onClick) props.onClick(!switchValue); 31 | } 32 | } 33 | 34 | function onChange(): void { 35 | if (!disabled && !loading) { 36 | if (props.onChange) props.onChange(!switchValue); 37 | } 38 | } 39 | 40 | const switchClassName = 41 | bem([ 42 | { 43 | disabled, 44 | checked: value, 45 | }, 46 | ]) + 47 | ' ' + 48 | size; 49 | 50 | return ( 51 |
52 | 61 |
62 | {loading && ( 63 | 64 | )} 65 |
66 |
67 | ); 68 | }; 69 | 70 | const defaultProps: SwitchProps = { 71 | size: 'normal', 72 | value: false, 73 | name: '', 74 | }; 75 | 76 | Switch.defaultProps = defaultProps; 77 | 78 | export default Switch; 79 | -------------------------------------------------------------------------------- /src/components/tab/index.less: -------------------------------------------------------------------------------- 1 | @import '../../style/common'; 2 | 3 | .@{prefix}tab { 4 | &__pane { 5 | flex-shrink: 0; 6 | width: 100%; 7 | 8 | &--inactive { 9 | height: 0; 10 | padding: 0; 11 | pointer-events: none; 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/components/tab/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useMemo, useRef } from 'react'; 2 | import createBEM from '../../utils/create/createBEM'; 3 | 4 | const bem = createBEM('tab'); 5 | 6 | export interface TabProps { 7 | title: string | React.ReactElement; 8 | name?: number | string; 9 | disabled?: boolean; 10 | active?: boolean; 11 | children?: any; 12 | lazyRender?: boolean; 13 | animated?: boolean; 14 | swipeable?: boolean; 15 | } 16 | 17 | export const Tab: FC = (props: TabProps) => { 18 | const { 19 | name, 20 | children, 21 | active = true, 22 | lazyRender = true, 23 | animated = false, 24 | swipeable = false, 25 | } = props; 26 | 27 | const inited = useRef(false); 28 | 29 | if (!inited.current && active) { 30 | inited.current = true; 31 | } 32 | 33 | // inited effected by active 34 | const isRenderChildren = useMemo(() => { 35 | return animated || swipeable || inited.current || !lazyRender; 36 | // eslint-disable-next-line react-hooks/exhaustive-deps 37 | }, [active, lazyRender, animated, swipeable]); 38 | 39 | return ( 40 |
41 | {isRenderChildren && children} 42 |
43 | ); 44 | }; 45 | 46 | Tab.displayName = 'Tab'; 47 | 48 | export default Tab; 49 | -------------------------------------------------------------------------------- /src/components/table/demo/index.less: -------------------------------------------------------------------------------- 1 | body.demo-table { 2 | overflow: hidden; 3 | } 4 | 5 | .custom-table-cell, 6 | .custom-table-th-cell { 7 | display: inline-flex; 8 | align-items: center; 9 | vertical-align: bottom; 10 | 11 | &__icon { 12 | width: 20px; 13 | height: 20px; 14 | margin-right: 4px; 15 | fill: #0278ff; 16 | } 17 | } 18 | 19 | .custom-table-th-cell__icon { 20 | width: 16px; 21 | height: 16px; 22 | fill: #0278ff; 23 | } 24 | -------------------------------------------------------------------------------- /src/components/table/indicator.tsx: -------------------------------------------------------------------------------- 1 | import { HTMLAttributes, useMemo } from 'react'; 2 | import * as React from 'react'; 3 | import createBEM from '../../utils/create/createBEM'; 4 | 5 | export type TableIndicatorShape = 'round'; 6 | 7 | export interface TableIndicatorProps extends HTMLAttributes { 8 | value: number[]; 9 | shape?: TableIndicatorShape; 10 | length: number; 11 | } 12 | 13 | const bem = createBEM('table-indicator'); 14 | 15 | function TableIndicator(props: TableIndicatorProps): JSX.Element { 16 | const { length, value = [] } = props; 17 | 18 | const Items = useMemo(() => { 19 | return Array(length) 20 | .fill(0) 21 | .map((theValue, index) => { 22 | return
; 23 | }); 24 | }, [value, length]); 25 | 26 | return
{Items}
; 27 | } 28 | 29 | export default TableIndicator; 30 | -------------------------------------------------------------------------------- /src/components/table/sort-icon.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import createBEM, { addClass } from '../../utils/create/createBEM'; 3 | import { TableSortType } from './types'; 4 | 5 | export interface SortIconProps extends React.HTMLAttributes { 6 | sortType: TableSortType; 7 | } 8 | 9 | const bem = createBEM('sort-icon'); 10 | 11 | function SortIcon(props: SortIconProps): JSX.Element { 12 | const { className = '', sortType } = props; 13 | 14 | return ( 15 | 23 | 28 | 33 | 34 | ); 35 | } 36 | 37 | export default SortIcon; 38 | -------------------------------------------------------------------------------- /src/components/table/types.ts: -------------------------------------------------------------------------------- 1 | import type { DefaultTextAlignType, RenderContent } from '../../utils/types'; 2 | 3 | export type TableFixedType = 'left' | 'right'; 4 | export type TableCellClassType = string | Array; 5 | export type TableColumnWidthType = number | string; 6 | export type TableSortType = 'asc' | 'desc' | undefined; 7 | 8 | export type TableDataPropType = { 9 | id?: string; 10 | children?: string; 11 | }; 12 | 13 | export type TableRenderTdCellType = (rowData: unknown, column: TableColumnProps) => RenderContent; 14 | 15 | export type TableRenderThCellType = (column: TableColumnProps) => RenderContent; 16 | 17 | export type BaseTableColumnProps = { 18 | dataProp: string; 19 | minWidth?: TableColumnWidthType; 20 | maxWidth?: TableColumnWidthType; 21 | width?: TableColumnWidthType; 22 | cellClass?: TableCellClassType; 23 | thCellClass?: TableCellClassType; 24 | tdCellClass?: TableCellClassType; 25 | fixed?: TableFixedType; 26 | textAlign?: DefaultTextAlignType; 27 | resizable?: boolean; 28 | }; 29 | 30 | export interface TableColumnProps extends BaseTableColumnProps { 31 | renderCell?: TableRenderTdCellType; 32 | renderThCell?: TableRenderThCellType; 33 | sortable?: boolean; 34 | filterable?: boolean; 35 | colspan?: number; 36 | title?: string; 37 | children?: Array; 38 | } 39 | 40 | export type TableDataProps = Record; 41 | 42 | export interface TableExpandOptionsType extends BaseTableColumnProps { 43 | contentClass?: TableCellClassType; 44 | type?: 'expand'; 45 | trigger?: 'icon' | 'row'; 46 | showIconColumn?: boolean; 47 | renderIcon?: (rowData: Record) => RenderContent; 48 | // 展开的区域内容的自定义渲染函数 49 | renderContent: (rowData: Record) => RenderContent; 50 | } 51 | 52 | export type TableCeilingOptionsType = { 53 | scrollBoundary: HTMLElement | Document; 54 | top?: number | string; 55 | }; 56 | -------------------------------------------------------------------------------- /src/components/tabs/demo/index.less: -------------------------------------------------------------------------------- 1 | .demo-tabs { 2 | .demo-cell { 3 | margin: 0 0 10px 0; 4 | 5 | h4 { 6 | margin: 20px; 7 | } 8 | 9 | .content { 10 | padding: 16px; 11 | font-size: 16px; 12 | text-align: center; 13 | background: #fff; 14 | } 15 | 16 | .omui-tabs--card { 17 | margin: 0 10px; 18 | 19 | .content { 20 | background: transparent; 21 | } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/components/tabs/utils.ts: -------------------------------------------------------------------------------- 1 | import { raf } from '../../utils/dom/raf'; 2 | 3 | export function scrollLeftTo(el: HTMLElement, to: number, duration: number): void { 4 | let count = 0; 5 | const from = el.scrollLeft; 6 | const frames = duration === 0 ? 1 : Math.round((duration * 1000) / 16); 7 | 8 | function animate(): void { 9 | el.scrollLeft += (to - from) / frames; 10 | 11 | el.scrollLeft = to - ((frames - count - 1) * (to - from)) / frames; 12 | 13 | if (++count < frames) { 14 | raf(animate); 15 | } 16 | } 17 | 18 | animate(); 19 | } 20 | -------------------------------------------------------------------------------- /src/components/tag/demo/index.less: -------------------------------------------------------------------------------- 1 | @import '../../../style/common'; 2 | 3 | .demo-tag { 4 | .omui-cell__title, 5 | .omui-cell__content { 6 | overflow: visible; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/components/toast/icon/error.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/toast/icon/loading.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/toast/icon/success.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/toast/icon/warning.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/toast/index.less: -------------------------------------------------------------------------------- 1 | @import '../../style/common'; 2 | 3 | .@{prefix}toast { 4 | position: fixed; 5 | top: 50%; 6 | display: flex; 7 | flex-direction: column; 8 | align-items: center; 9 | justify-content: center; 10 | box-sizing: content-box; 11 | padding: @toast-padding; 12 | color: @white; 13 | font-size: @toast-font-size; 14 | line-height: @toast-line-height; 15 | white-space: pre-wrap; 16 | text-align: center; 17 | word-break: break-all; 18 | background-color: @toast-background-color; 19 | transform: translate3d(calc(50vw - 50%), -50%, 0); 20 | 21 | .okui-border-radius(3); 22 | 23 | // should not add pointer-events: none directly to body tag 24 | // that will cause unexpected tap-highlight-color in mobile safari 25 | &--unclickable { 26 | * { 27 | pointer-events: none; 28 | } 29 | } 30 | 31 | &--text { 32 | width: fit-content; 33 | min-width: @toast-text-min-width; 34 | min-height: unset; 35 | padding: @toast-text-padding; 36 | 37 | .@{prefix}toast__text { 38 | margin-top: 0; 39 | } 40 | } 41 | 42 | &__text { 43 | min-width: @toast-text-min-width; 44 | max-width: @toast-text-max-width; 45 | } 46 | 47 | &__icon { 48 | margin-bottom: 12px; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/components/toolbar/index.less: -------------------------------------------------------------------------------- 1 | @import '../../style/common.less'; 2 | 3 | .@{prefix}toolbar { 4 | &__toolbar { 5 | position: relative; 6 | display: flex; 7 | align-items: center; 8 | justify-content: center; 9 | height: @toolbar-height; 10 | .@{prefix}button--large { 11 | width: auto; 12 | padding-right: @toolbar-padding-horizontal; 13 | padding-left: @toolbar-padding-horizontal; 14 | } 15 | } 16 | 17 | &__title { 18 | color: @toolbar-title-color; 19 | font-weight: @toolbar-title-font-weight; 20 | font-size: @toolbar-title-font-size; 21 | } 22 | 23 | &__button--cancel { 24 | position: absolute; 25 | left: 0; 26 | height: @toolbar-height; 27 | color: @toolbar-cancel-btn-color; 28 | font-weight: @toolbar-cancel-btn-font-weight; 29 | font-size: @toolbar-cancel-btn-font-size; 30 | line-height: @toolbar-height; 31 | 32 | &:active { 33 | color: @toolbar-cancel-btn-active-color; 34 | } 35 | } 36 | 37 | &__button--confirm { 38 | position: absolute; 39 | right: 0; 40 | height: @toolbar-height; 41 | color: @toolbar-confirm-btn-color; 42 | font-weight: @toolbar-confirm-btn-font-weight; 43 | font-size: @toolbar-confirm-btn-font-size; 44 | line-height: @toolbar-height; 45 | 46 | &:active { 47 | color: @toolbar-confirm-btn-active-color; 48 | } 49 | } 50 | 51 | &__header--showBottomLine { 52 | .@{prefix}toolbar__toolbar { 53 | &::after { 54 | .hairline-bottom(@toolbar-border-color); 55 | } 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/hooks/callback/const-callback.ts: -------------------------------------------------------------------------------- 1 | import { useRef } from 'react'; 2 | 3 | /** 4 | * Hook to ensure a callback function always has the same identify. 5 | * Unlike `React.useCallback`, this is guaranteed to always return the same value. 6 | * 7 | * @param callback The callback function. Only the first value passed is respected. 8 | * @returns The callback function. The identify of the callback will always be the same. 9 | */ 10 | // eslint-disable-next-line @typescript-eslint/no-explicit-any,space-before-function-paren 11 | export function useConstCallback any>(callback: T): T { 12 | const ref = useRef(); 13 | if (!ref.current) { 14 | ref.current = callback; 15 | } 16 | return ref.current; 17 | } 18 | -------------------------------------------------------------------------------- /src/hooks/callback/index.ts: -------------------------------------------------------------------------------- 1 | export * from './const-callback'; 2 | export * from './ref-callback'; 3 | -------------------------------------------------------------------------------- /src/hooks/callback/ref-callback.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import { useRef } from 'react'; 3 | import { useConstCallback } from './const-callback'; 4 | 5 | /** 6 | * Hook to wrap callback function, ensure the wrapper always has the same identify, 7 | * meanwhile, always call the latest callback passed when call the wrapper. 8 | * 9 | * @param callback The callback function. Always use the latest value passed. 10 | * @returns The wrapper function. The identify of the wrapper always be the same. 11 | */ 12 | export function useRefCallback any>(callback: T): T; 13 | 14 | export function useRefCallback any>( 15 | callback: T | undefined, 16 | ): (...args: Parameters) => ReturnType | undefined; 17 | 18 | /** 19 | * Hook to wrap callback function, ensure the wrapper always has the same identify, 20 | * meanwhile, always call the latest callback passed when call the wrapper. 21 | * 22 | * @param callback The callback function. Always use the latest value passed. 23 | * @returns The wrapper function. The identify of the wrapper always be the same. 24 | */ 25 | // eslint-disable-next-line space-before-function-paren 26 | export function useRefCallback any>( 27 | callback: T | undefined, 28 | ): T | ((...args: Parameters) => ReturnType | undefined) { 29 | const callbackRef = useRef(); 30 | callbackRef.current = callback; 31 | const wrapper = useConstCallback((...args) => callbackRef.current?.(...args)); 32 | return wrapper; 33 | } 34 | -------------------------------------------------------------------------------- /src/hooks/click/index.ts: -------------------------------------------------------------------------------- 1 | import { RefObject, useEffect } from 'react'; 2 | import { off, on } from '../../utils/dom/event'; 3 | import { useRefCallback } from '../callback'; 4 | 5 | export type ClickOutsideConfig = { 6 | container: RefObject; 7 | method: 'click' | 'touchstart'; 8 | callback: () => void; 9 | closeOnClickOutside: boolean; 10 | }; 11 | 12 | export function useClickOutside(config: ClickOutsideConfig): void { 13 | const listener = useRefCallback(function (event: MouseEvent | TouchEvent) { 14 | if (config.closeOnClickOutside && !config.container.current?.contains(event.target as Node)) { 15 | config.callback(); 16 | } 17 | }); 18 | useEffect(() => { 19 | on(document, config.method, listener); 20 | return (): void => { 21 | off(document, config.method, listener); 22 | }; 23 | }, []); 24 | } 25 | -------------------------------------------------------------------------------- /src/hooks/const.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable semi */ 2 | import { useRef } from 'react'; 3 | 4 | /** 5 | * Returns a constant value or uses to ensure the same reference. 6 | * If provides reference type value (object, array...), it will always return same reference from the first initial, even if re-rendered. 7 | * @param initialValue 8 | */ 9 | export function useConst(initialValue: T | (() => T)): T { 10 | const ref = useRef<{ value: T }>(); 11 | if (ref.current === undefined) { 12 | ref.current = { 13 | value: typeof initialValue === 'function' ? (initialValue as () => T)() : initialValue, 14 | }; 15 | } 16 | return ref.current.value; 17 | } 18 | -------------------------------------------------------------------------------- /src/hooks/dom/index.ts: -------------------------------------------------------------------------------- 1 | export * from './resize'; 2 | -------------------------------------------------------------------------------- /src/hooks/dom/resize.ts: -------------------------------------------------------------------------------- 1 | import { MutableRefObject, useEffect } from 'react'; 2 | import { useRefCallback } from '../callback'; 3 | 4 | type ResizeHandler = () => void; 5 | 6 | export type useResizeOptions = { 7 | onResize?: ResizeHandler; 8 | }; 9 | export function useResize(options: useResizeOptions): void { 10 | const { onResize } = options; 11 | 12 | const onResizeValue = useRefCallback(onResize); 13 | 14 | useEffect(() => { 15 | if (window) { 16 | window.addEventListener('resize', onResizeValue); 17 | } 18 | return (): void => { 19 | if (window) { 20 | window.removeEventListener('resize', onResizeValue); 21 | } 22 | }; 23 | // eslint-disable-next-line react-hooks/exhaustive-deps 24 | }, []); 25 | } 26 | 27 | export type useContainerResizeOptions = { 28 | containerRef: MutableRefObject; 29 | callback: (...args: any[]) => void; 30 | removeCallback?: (...args: any[]) => void; 31 | }; 32 | export function useContainerResize(options: useContainerResizeOptions): void { 33 | const { 34 | containerRef, 35 | callback: propsCallback, 36 | removeCallback: propsRemoveCallback = (): void => {}, 37 | } = options; 38 | 39 | const callback = useRefCallback(propsCallback); 40 | const removecallback = useRefCallback(propsRemoveCallback); 41 | 42 | useEffect(() => { 43 | callback(); 44 | 45 | return (): void => { 46 | removecallback(); 47 | }; 48 | }, [callback, removecallback]); 49 | 50 | useEffect(() => { 51 | let resizeObserver: ResizeObserver; 52 | const container = containerRef.current; 53 | 54 | if (ResizeObserver && container) { 55 | resizeObserver = new ResizeObserver(() => { 56 | callback(); 57 | }); 58 | resizeObserver.observe(container); 59 | } 60 | 61 | return (): void => { 62 | if (container && resizeObserver) { 63 | resizeObserver.unobserve(container); 64 | removecallback(); 65 | } 66 | }; 67 | // eslint-disable-next-line react-hooks/exhaustive-deps 68 | }, [callback]); 69 | } 70 | -------------------------------------------------------------------------------- /src/hooks/event.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable semi */ 2 | import { RefObject, useRef, useEffect } from 'react'; 3 | 4 | export function useEventListener( 5 | element: RefObject | Element | Window | Document, 6 | eventName: string, 7 | listener: (event: Event) => void, 8 | passive?: boolean, 9 | ): void { 10 | const listenerRef = useRef(listener); 11 | listenerRef.current = listener; 12 | 13 | useEffect(() => { 14 | const actualElement = element && 'current' in element ? element.current : element; 15 | if (!actualElement) { 16 | return; 17 | } 18 | const finalListener = (event: Event): void => listenerRef.current(event); 19 | actualElement.addEventListener(eventName, finalListener, { passive }); 20 | return (): void => actualElement.removeEventListener(eventName, finalListener); 21 | }, [element, eventName, passive]); 22 | } 23 | -------------------------------------------------------------------------------- /src/hooks/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable semi */ 2 | export * from './state'; 3 | export * from './callback'; 4 | export * from './input'; 5 | export * from './touch'; 6 | export * from './click'; 7 | export * from './drag'; 8 | -------------------------------------------------------------------------------- /src/hooks/input/index.ts: -------------------------------------------------------------------------------- 1 | export * from './composition-input'; 2 | -------------------------------------------------------------------------------- /src/hooks/state/controlled.ts: -------------------------------------------------------------------------------- 1 | import { Dispatch, SetStateAction, useEffect, useState } from 'react'; 2 | 3 | export function useControlled( 4 | value: T | undefined, 5 | defaultValue: T, 6 | ): [T, Dispatch>] { 7 | const [activeValue, setActiveValue] = useState(() => { 8 | return typeof value !== 'undefined' ? value : defaultValue; 9 | }); 10 | 11 | useEffect(() => { 12 | if (typeof value === 'undefined') return; 13 | setActiveValue(value!); 14 | }, [value]); 15 | 16 | return [activeValue, setActiveValue]; 17 | } 18 | -------------------------------------------------------------------------------- /src/hooks/state/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable semi */ 2 | export * from './record'; 3 | export * from './controlled'; 4 | -------------------------------------------------------------------------------- /src/hooks/state/record.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-non-null-assertion */ 2 | /* eslint-disable semi */ 3 | import { Dispatch, SetStateAction, useRef, useState } from 'react'; 4 | 5 | /** 6 | * Returns a stateful value in record like object type, a function to update it. 7 | * The new state object can be partial, the hook will merge new and old object automatically. 8 | */ 9 | export function useRecord>( 10 | initState: S | (() => S), 11 | ): [S, Dispatch>>] { 12 | const ref = useRef<{ value: S }>(); 13 | if (!ref.current) { 14 | ref.current = { 15 | value: typeof initState === 'function' ? initState() : initState, 16 | }; 17 | } 18 | const [record, setState] = useState(initState); 19 | const callbackRef = useRef>>>(partial => { 20 | const newRecord = { 21 | ...ref.current!.value, 22 | ...partial, 23 | }; 24 | ref.current!.value = newRecord; 25 | setState(newRecord); 26 | }); 27 | return [record, callbackRef.current]; 28 | } 29 | -------------------------------------------------------------------------------- /src/hooks/timer.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable semi */ 2 | import { useConst } from './const'; 3 | import { useEffect } from 'react'; 4 | 5 | export interface UseSetTimeoutReturnType { 6 | setTimeout(callback: () => void, delay: number): number; 7 | clearTimeout(id: number): void; 8 | } 9 | 10 | /** 11 | * Returns a wrapper function for `setTimeout` which automatically cleanup. 12 | */ 13 | export function useSetTimeout(): UseSetTimeoutReturnType { 14 | const idMap = useConst>({}); 15 | 16 | useEffect( 17 | () => (): void => { 18 | for (const id of Object.keys(idMap)) { 19 | clearTimeout(id as any); 20 | } 21 | }, 22 | [idMap], 23 | ); 24 | 25 | return useConst({ 26 | setTimeout(callback, delay) { 27 | const id = setTimeout(callback, delay) as unknown as number; 28 | idMap[id] = 1; 29 | return id; 30 | }, 31 | clearTimeout(id) { 32 | delete idMap[id]; 33 | clearTimeout(id); 34 | }, 35 | }); 36 | } 37 | 38 | export interface UseSetIntervalReturnType { 39 | setInterval(callback: () => void, interval: number): number; 40 | clearInterval(id: number): void; 41 | } 42 | 43 | /** 44 | * Returns a wrapper function for `setInterval` which automatically cleanup. 45 | */ 46 | export function useSetInterval(): UseSetIntervalReturnType { 47 | const idMap = useConst>({}); 48 | 49 | useEffect( 50 | () => (): void => { 51 | for (const id of Object.keys(idMap)) { 52 | clearInterval(id as any); 53 | } 54 | }, 55 | [idMap], 56 | ); 57 | 58 | return useConst({ 59 | setInterval(callback, interval) { 60 | const id = setInterval(callback, interval) as unknown as number; 61 | idMap[id] = 1; 62 | return id; 63 | }, 64 | clearInterval(id) { 65 | delete idMap[id]; 66 | clearInterval(id); 67 | }, 68 | }); 69 | } 70 | -------------------------------------------------------------------------------- /src/hooks/touch/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable semi */ 2 | export * from './touch'; 3 | -------------------------------------------------------------------------------- /src/icon/arrow-left.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icon/arrow-right.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icon/attention.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icon/check-one.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icon/check.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icon/close-one.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/icon/close.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icon/info.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/index.less: -------------------------------------------------------------------------------- 1 | // This file is auto gererated by build/build-entry.js 2 | @import './style/index'; 3 | @import './style/root'; 4 | @import './components/action-sheet/index'; 5 | @import './components/badge/index'; 6 | @import './components/button/index'; 7 | @import './components/calendar/index'; 8 | @import './components/cascader/index'; 9 | @import './components/cascader-sliding/index'; 10 | @import './components/cell/index'; 11 | @import './components/checkbox/index'; 12 | @import './components/col/index'; 13 | @import './components/collapse/index'; 14 | @import './components/date-time-picker/index'; 15 | @import './components/dialog/index'; 16 | @import './components/dropdown/index'; 17 | @import './components/dropdown-item/index'; 18 | @import './components/field/index'; 19 | @import './components/header/index'; 20 | @import './components/header-tab/index'; 21 | @import './components/header-tabs/index'; 22 | @import './components/icon/index'; 23 | @import './components/image-preview/index'; 24 | @import './components/infinite-scroll/index'; 25 | @import './components/loading/index'; 26 | @import './components/notice-bar/index'; 27 | @import './components/overlay/index'; 28 | @import './components/picker/index'; 29 | @import './components/picker-column/index'; 30 | @import './components/popup/index'; 31 | @import './components/popup-drill-down/index'; 32 | @import './components/progress/index'; 33 | @import './components/pull-refresh/index'; 34 | @import './components/radio/index'; 35 | @import './components/row/index'; 36 | @import './components/search/index'; 37 | @import './components/slider/index'; 38 | @import './components/sticky/index'; 39 | @import './components/swipe/index'; 40 | @import './components/swipe-cell/index'; 41 | @import './components/switch/index'; 42 | @import './components/tab/index'; 43 | @import './components/table/index'; 44 | @import './components/tabs/index'; 45 | @import './components/tag/index'; 46 | @import './components/toast/index'; 47 | @import './components/toolbar/index'; 48 | @import './components/tree/index'; 49 | -------------------------------------------------------------------------------- /src/locale/en-US.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | dialog: { 3 | cancel: 'Cancel', 4 | confirm: 'Confirm', 5 | }, 6 | picker: { 7 | cancel: 'Cancel', 8 | confirm: 'Confirm', 9 | }, 10 | datetimePicker: { 11 | title: 'Please Select', 12 | year: '', 13 | month: '', 14 | day: '', 15 | hour: '', 16 | minute: '', 17 | second: '', 18 | }, 19 | calendar: { 20 | 0: '日', 21 | 1: '一', 22 | 2: '二', 23 | 3: '三', 24 | 4: '四', 25 | 5: '五', 26 | 6: '六', 27 | }, 28 | field: { 29 | placeholder: 'Please enter content', 30 | }, 31 | loading: { 32 | loading: 'Loading', 33 | }, 34 | toast: { 35 | loading: 'loading...', 36 | }, 37 | toolbar: { 38 | cancel: 'Cancel', 39 | confirm: 'Confirm', 40 | title: 'Please Select', 41 | }, 42 | }; 43 | -------------------------------------------------------------------------------- /src/locale/ja.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | dialog: { 3 | cancel: 'キャンセル', 4 | confirm: '確認', 5 | }, 6 | picker: { 7 | cancel: 'キャンセル', 8 | confirm: '確認', 9 | }, 10 | datetimePicker: { 11 | title: '選択してください', 12 | year: '年', 13 | month: '月', 14 | day: '日', 15 | hour: '時', 16 | minute: '分', 17 | second: '秒', 18 | }, 19 | calendar: { 20 | 0: '日', 21 | 1: '一', 22 | 2: '二', 23 | 3: '三', 24 | 4: '四', 25 | 5: '五', 26 | 6: '六', 27 | }, 28 | field: { 29 | placeholder: '请输入内容', 30 | }, 31 | loading: { 32 | loading: '加载显示', 33 | }, 34 | toast: { 35 | loading: '加载中...', 36 | }, 37 | toolbar: { 38 | cancel: 'キャンセル', 39 | confirm: '確認', 40 | title: '選んでください', 41 | }, 42 | }; 43 | -------------------------------------------------------------------------------- /src/locale/zh-CN.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | dialog: { 3 | cancel: '取消', 4 | confirm: '确定', 5 | }, 6 | picker: { 7 | cancel: '取消', 8 | confirm: '确定', 9 | }, 10 | datetimePicker: { 11 | title: '选择时间', 12 | year: '年', 13 | month: '月', 14 | day: '日', 15 | hour: '时', 16 | minute: '分', 17 | second: '秒', 18 | }, 19 | calendar: { 20 | 0: '日', 21 | 1: '一', 22 | 2: '二', 23 | 3: '三', 24 | 4: '四', 25 | 5: '五', 26 | 6: '六', 27 | year: '年', 28 | month: '月', 29 | }, 30 | field: { 31 | placeholder: '请输入内容', 32 | }, 33 | loading: { 34 | loading: '加载显示', 35 | }, 36 | toast: { 37 | loading: '加载中...', 38 | }, 39 | toolbar: { 40 | cancel: '取消', 41 | confirm: '确定', 42 | title: '请选择', 43 | }, 44 | }; 45 | -------------------------------------------------------------------------------- /src/public-api/index.ts: -------------------------------------------------------------------------------- 1 | import { context } from '../common/popup/context'; 2 | 3 | export function getZIndex(): number { 4 | return context.zIndex; 5 | } 6 | 7 | export function setZIndex(zIndex: number): void { 8 | context.zIndex = zIndex; 9 | } 10 | 11 | export { useZIndex } from '../common/popup/context'; 12 | 13 | export { getMonthWeekByDate, getDateByMonthWeek } from '../utils/format/date'; 14 | -------------------------------------------------------------------------------- /src/style/animation.less: -------------------------------------------------------------------------------- 1 | @import './var'; 2 | @import './common.less'; 3 | 4 | @keyframes omui-fade-in { 5 | from { 6 | opacity: 0; 7 | } 8 | 9 | to { 10 | opacity: 1; 11 | } 12 | } 13 | 14 | @keyframes omui-fade-out { 15 | from { 16 | opacity: 1; 17 | } 18 | 19 | to { 20 | opacity: 0; 21 | } 22 | } 23 | 24 | .@{prefix}fade { 25 | &-enter-active { 26 | animation: @animation-duration-base omui-fade-in; 27 | animation-fill-mode: forwards; 28 | } 29 | 30 | &-enter-done { 31 | display: block; 32 | } 33 | 34 | &-exit-active { 35 | animation: @animation-duration-base omui-fade-out; 36 | animation-fill-mode: forwards; 37 | } 38 | 39 | &-exit-done { 40 | display: none; 41 | } 42 | } 43 | 44 | @keyframes omui-loading-circle { 45 | from { 46 | transform: rotate(0); 47 | } 48 | 49 | to { 50 | transform: rotate(360deg); 51 | } 52 | } 53 | 54 | .@{prefix}loading-circle { 55 | display: inline-block; 56 | transform-origin: center; 57 | animation: omui-loading-circle 1s linear infinite; 58 | } 59 | -------------------------------------------------------------------------------- /src/style/color-palette/palette.less: -------------------------------------------------------------------------------- 1 | @import './tinyColor.less'; 2 | 3 | .colorPaletteMixin() { 4 | @functions: ~`(function(){ 5 | this.colorPalette = function(color, index) { 6 | 7 | var darkestL = 16; 8 | var lightestL = 96; 9 | var hsl = tinycolor(color).toHsl(); 10 | var curLight = hsl.l * 100; 11 | 12 | var light = void 0; 13 | var darkStep = (curLight - darkestL) / 5; 14 | var lightStep = (lightestL - curLight) / 5; 15 | if (index === 6) { 16 | light = hsl.l; 17 | } else if (index < 6) { 18 | light = (curLight + (6 - index) * lightStep) / 100; 19 | } else if (index > 6) { 20 | light = (curLight - (index - 6) * darkStep) / 100; 21 | } 22 | 23 | return tinycolor({ 24 | h: Math.round(hsl.h), 25 | s: hsl.s, 26 | l: light 27 | }).toHexString(); 28 | } 29 | })()`; 30 | } 31 | .colorPaletteMixin(); 32 | -------------------------------------------------------------------------------- /src/style/common.less: -------------------------------------------------------------------------------- 1 | @prefix: omui-; 2 | 3 | @import './var'; 4 | @import './mixins'; 5 | @import './hairline.less'; 6 | -------------------------------------------------------------------------------- /src/style/compat.less: -------------------------------------------------------------------------------- 1 | .remCompatMixin() { 2 | @functions: ~`(function(){ 3 | this.remcompat = function(pxvalue) { 4 | return pxvalue.replace(/px/g, 'PX'); 5 | } 6 | })()`; 7 | } 8 | .remCompatMixin(); -------------------------------------------------------------------------------- /src/style/hairline.less: -------------------------------------------------------------------------------- 1 | @import './mixins/hairline'; 2 | @import './common.less'; 3 | 4 | [class*='@{prefix}hairline'] { 5 | position: relative; 6 | 7 | &::after { 8 | .hairline(); 9 | } 10 | } 11 | 12 | .@{prefix}hairline { 13 | &--top::after { 14 | border-top-width: 1PX; /* no */ 15 | } 16 | 17 | &--left::after { 18 | border-left-width: 1PX; /* no */ 19 | } 20 | 21 | &--right::after { 22 | border-right-width: 1PX; /* no */ 23 | } 24 | 25 | &--bottom::after { 26 | border-bottom-width: 1PX; /* no */ 27 | } 28 | 29 | &--top-bottom::after { 30 | border-width: 1PX 0; /* no */ 31 | } 32 | 33 | &--surround::after { 34 | border-width: 1PX; /* no */ 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/style/index.less: -------------------------------------------------------------------------------- 1 | @import './compat'; 2 | @import './rem'; 3 | @import './reset'; 4 | @import './common.less'; 5 | @import './animation'; 6 | -------------------------------------------------------------------------------- /src/style/mixins.less: -------------------------------------------------------------------------------- 1 | @import './mixins/hairline'; 2 | @import './mixins/radius'; 3 | @import './mixins/shadow'; 4 | -------------------------------------------------------------------------------- /src/style/mixins/hairline.less: -------------------------------------------------------------------------------- 1 | /** 2 | * 一像素线 3 | */ 4 | .hairline-common() { 5 | position: absolute; 6 | z-index: 1; 7 | box-sizing: border-box; 8 | content: ' '; 9 | pointer-events: none; 10 | } 11 | 12 | .hairline(@gray-3: #ebedf0) { 13 | .hairline-common(); 14 | 15 | top: -50%; 16 | left: -50%; 17 | width: 200%; 18 | height: 200%; 19 | border: 0 solid @gray-3; 20 | transform: scale(0.5); 21 | will-change: transform; 22 | } 23 | 24 | .hairline-bottom(@gray-3: #ebedf0, @horizontal: 0) { 25 | .hairline-common(); 26 | 27 | right: 0; 28 | right: @horizontal; 29 | bottom: 0; 30 | left: @horizontal; 31 | border-bottom: 1px solid @gray-3; 32 | transform: scaleY(0.5); 33 | will-change: transform; 34 | } 35 | -------------------------------------------------------------------------------- /src/style/mixins/radius.less: -------------------------------------------------------------------------------- 1 | /** 2 | * 六个层级的圆角设置 3 | */ 4 | .okui-border-radius(@level:1) when (@level = 1) { 5 | border-radius: 2PX; /* no */ 6 | } 7 | .okui-border-radius(@level) when (@level = 2) { 8 | border-radius: 4PX; /* no */ 9 | } 10 | .okui-border-radius(@level) when (@level = 3) { 11 | border-radius: 8PX; /* no */ 12 | } 13 | .okui-border-radius(@level) when (@level = 4) { 14 | border-radius: 12PX; /* no */ 15 | } 16 | .okui-border-radius(@level) when (@level = 5) { 17 | border-radius: 16PX; /* no */ 18 | } 19 | .okui-border-radius(@level) when (@level = 6) { 20 | border-radius: 18PX; /* no */ 21 | } 22 | -------------------------------------------------------------------------------- /src/style/mixins/shadow.less: -------------------------------------------------------------------------------- 1 | /** 2 | * 五个层级的阴影设置 3 | */ 4 | .okui-box-shadow(@level:1) when (@level = 1) { 5 | box-shadow: 0 2px 6px 0 rgba(#a6c6ec, 0.1); 6 | } 7 | .okui-box-shadow(@level) when (@level = 2) { 8 | box-shadow: 0 4px 12px 0 rgba(#a6c6ec, 0.1); 9 | } 10 | .okui-box-shadow(@level) when (@level = 3) { 11 | box-shadow: 0 6px 18px 0 rgba(#a6c6ec, 0.1); 12 | } 13 | .okui-box-shadow(@level) when (@level = 4) { 14 | box-shadow: 0 8px 24px 0 rgba(#a6c6ec, 0.1); 15 | } 16 | .okui-box-shadow(@level) when (@level = 5) { 17 | box-shadow: 0 10px 46px 0 rgba(#a6c6ec, 0.18); 18 | } 19 | .okui-box-shadow(@level) when (@level = 6) { 20 | box-shadow: 0 10px 46px 0 rgba(#000, 0.1); 21 | } 22 | -------------------------------------------------------------------------------- /src/style/rem.less: -------------------------------------------------------------------------------- 1 | @import './compat.less'; 2 | 3 | // 4 | // Rem 5 | // -------------------------------------------------- 6 | // Vertical screen 7 | // 375屏幕为 20px,以此为基础计算出每一种宽度的字体大小 8 | 9 | @useDefaultFontSize: true; 10 | 11 | @baseWidth: 375PX; /* no */ 12 | @baseFont: 20PX; /* no */ 13 | @bps: 320PX, 360PX, 375PX, 400PX, 414PX, 440PX, 480PX, 520PX, 560PX, 600PX, 640PX, 680PX, 720PX, 14 | 760PX, 800PX, 960PX; /* no */ 15 | 16 | .setFontSize () when (@useDefaultFontSize = true) { 17 | html { 18 | font-size: ~`remcompat('@{baseFont}') `; /* no */ 19 | } 20 | .loop(@i: 1) when (@i <= length(@bps)) { 21 | @bp: extract(@bps, @i); 22 | @font: (@bp / @baseWidth) * @baseFont; 23 | 24 | @media only screen and (min-width: @bp) { 25 | html { 26 | font-size: ~`remcompat('@{font}') ` !important; /* no */ 27 | } 28 | } 29 | .loop((@i + 1)); 30 | } 31 | .loop; 32 | } 33 | 34 | .setFontSize(); 35 | -------------------------------------------------------------------------------- /src/style/root.less: -------------------------------------------------------------------------------- 1 | 2 | /* stylelint-disable */ 3 | :root{--animation-duration-base:0.3s;--animation-duration-fast:0.2s;--blue:#0278ff;--cyan:#00d2d5;--orange:#fec038;--red:#ff6767;--white:#fff;--black:#000;--primary-color:#0278ff;--success-color:#00d2d5;--warning-color:#fec038;--danger-color:#ff6767;--gray-1:#fafafa;--gray-2:#f7f7f7;--gray-3:#eeeeee;--gray-4:#dcdcdc;--gray-5:#c1c1c1;--text-color-1:#000000;--text-color-2:#333333;--text-color-3:#666666;--text-color-4:#999999;--text-color-5:#c1c1c1;--text-color-link:#0278ff;--background-color-base:#fff;--background-color-active:rgba(153; 153; 153; 0.1);--primary-color-1:#0068de;--primary-color-2:#0278ff;--primary-color-3:#3191ff;--primary-color-4:#8ec2ff;--primary-color-5:#ebf4ff;--success-color-1:#00b8bb;--success-color-2:#00d2d5;--success-color-3:#0dfbff;--success-color-4:#7cfdff;--success-color-5:#ebffff;--warning-color-1:#feb10b;--warning-color-2:#fec038;--warning-color-3:#fecb5c;--warning-color-4:#ffe2a3;--warning-color-5:#fff9eb;--danger-color-1:#ff3030;--danger-color-2:#ff6767;--danger-color-3:#ff8181;--danger-color-4:#ffb6b6;--danger-color-5:#ffebeb;--picker-confirm-btn-color:#0278ff;} 4 | -------------------------------------------------------------------------------- /src/utils/const.ts: -------------------------------------------------------------------------------- 1 | import { prefix } from './create/createBEM'; 2 | 3 | /** 4 | * 样式相关常量收敛 5 | */ 6 | 7 | // border 8 | export const BORDER = `${prefix}hairline`; 9 | export const BORDER_TOP = `${BORDER}--top`; 10 | export const BORDER_LEFT = `${BORDER}--left`; 11 | export const BORDER_RIGHT = `${BORDER}--right`; 12 | export const BORDER_BOTTOM = `${BORDER}--bottom`; 13 | export const BORDER_SURROUND = `${BORDER}--surround`; 14 | export const BORDER_TOP_BOTTOM = `${BORDER}--top-bottom`; 15 | -------------------------------------------------------------------------------- /src/utils/create/bem.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * bem helper 3 | * bem() // 'button' 4 | * bem('text') // 'button__text' 5 | * bem('text', 'color') // 'button__text button__text--color' 6 | * bem({ disabled }) // 'button button--disabled' 7 | * bem('text', { disabled }) // 'button__text button__text--disabled' 8 | * bem(['primary', 'large']) // 'button button--primary button--large' 9 | */ 10 | 11 | type Mod = string | { [key: string]: any }; 12 | type Mods = Mod | Mod[]; 13 | 14 | const ELEMENT = '__'; 15 | const MODS = '--'; 16 | 17 | function join(name: string, el?: string | undefined, symbol?: string): string { 18 | return el ? name + symbol + el : name; 19 | } 20 | 21 | function prefix(name: string, mods: Mods): string { 22 | if (typeof mods === 'string') { 23 | return join(name, mods, MODS); 24 | } 25 | 26 | if (Array.isArray(mods)) { 27 | return mods.map(item => prefix(name, item)).join(' '); 28 | } 29 | 30 | const ret: string[] = []; 31 | if (mods) { 32 | Object.keys(mods).forEach(key => { 33 | if (mods[key]) { 34 | ret.push(name + MODS + key); 35 | } 36 | }); 37 | } 38 | 39 | return ret.join(' '); 40 | } 41 | 42 | export function createBEM(name: string) { 43 | return function (el?: Mods, mods?: Mods): string { 44 | if (el && typeof el !== 'string') { 45 | mods = el; 46 | el = ''; 47 | } 48 | 49 | el = join(name, el as string | undefined, ELEMENT); 50 | 51 | if (mods) { 52 | return `${el} ${prefix(el, mods)}`.trim(); 53 | } else { 54 | return el; 55 | } 56 | }; 57 | } 58 | -------------------------------------------------------------------------------- /src/utils/create/createBEM.ts: -------------------------------------------------------------------------------- 1 | import { createBEM } from './bem'; 2 | 3 | /** 4 | * 全局bem创建函数 5 | */ 6 | export const prefix = 'omui-'; 7 | 8 | export default function create(name: string): ReturnType { 9 | return createBEM(`${prefix}${name}`); 10 | } 11 | 12 | export function addClass( 13 | base: string, 14 | ...classNames: (string | undefined | null | boolean | Array)[] 15 | ): string { 16 | let result = base; 17 | for (let i = 0; i < classNames.length; i++) { 18 | const className = classNames[i]; 19 | if (className) { 20 | if (Array.isArray(className)) { 21 | result = addClass(result, className); 22 | } else { 23 | result = `${result} ${className}`; 24 | } 25 | } 26 | } 27 | return result; 28 | } 29 | -------------------------------------------------------------------------------- /src/utils/create/i18n.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | import LocaleContext from '../../components/locale-context'; 3 | 4 | import { get } from '..'; 5 | import { camelize, translate } from '../format/string'; 6 | 7 | export function createI18N(name: string): (path: string) => string { 8 | const prefix = name ? camelize(name) + '.' : ''; 9 | 10 | return (path: string, ...args: any[]): string => 11 | translate(get(useContext(LocaleContext), prefix + path), args); 12 | } 13 | -------------------------------------------------------------------------------- /src/utils/create/index.ts: -------------------------------------------------------------------------------- 1 | import { createBEM } from './bem'; 2 | import { createI18N } from './i18n'; 3 | 4 | /** 5 | * 全局函数 6 | */ 7 | const prefix = 'omui-'; 8 | 9 | type CreateNamespaceReturn = [ReturnType, ReturnType]; 10 | 11 | export function createNamespace(name: string): CreateNamespaceReturn { 12 | return [createBEM(`${prefix}${name}`), createI18N(name)]; 13 | } 14 | -------------------------------------------------------------------------------- /src/utils/dom/raf.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * requestAnimationFrame polyfill 3 | */ 4 | 5 | import { isServer } from '..'; 6 | 7 | let prev = Date.now(); 8 | 9 | function fallback(fn: FrameRequestCallback): number { 10 | const curr = Date.now(); 11 | const ms = Math.max(0, 16 - (curr - prev)); 12 | const id = setTimeout(fn, ms); 13 | prev = curr + ms; 14 | return id; 15 | } 16 | 17 | const root = (isServer ? global : window) as Window; 18 | 19 | const iRaf = root.requestAnimationFrame || fallback; 20 | 21 | const iCancel = root.cancelAnimationFrame || root.clearTimeout; 22 | 23 | export function raf(fn: FrameRequestCallback): number { 24 | return iRaf.call(root, fn); 25 | } 26 | 27 | export function doubleRaf(fn: FrameRequestCallback): void { 28 | raf(() => { 29 | raf(fn); 30 | }); 31 | } 32 | 33 | export function cancelRaf(id: number): void { 34 | iCancel.call(root, id); 35 | } 36 | -------------------------------------------------------------------------------- /src/utils/dom/scroll.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | */ 4 | type ScrollElement = HTMLElement | Window; 5 | 6 | const overflowScrollReg = /scroll|auto/i; 7 | 8 | export function getScrollEventTarget( 9 | element: HTMLElement, 10 | rootParent: ScrollElement = window, 11 | ): ScrollElement { 12 | let node = element; 13 | 14 | while (node && node.tagName !== 'HTML' && node.nodeType === 1 && node !== rootParent) { 15 | const { overflowY } = window.getComputedStyle(node); 16 | 17 | if (overflowScrollReg.test(overflowY as string)) { 18 | if (node.tagName !== 'BODY') { 19 | return node; 20 | } 21 | 22 | const { overflowY: htmlOverflowY } = window.getComputedStyle(node.parentNode as Element); 23 | 24 | if (overflowScrollReg.test(htmlOverflowY as string)) { 25 | return node; 26 | } 27 | } 28 | node = node.parentNode as HTMLElement; 29 | } 30 | 31 | return rootParent; 32 | } 33 | 34 | export function getScrollBottom(element: ScrollElement): number { 35 | if (typeof window === 'undefined') { 36 | return 0; 37 | } 38 | let scrollElement; 39 | if (element === window) { 40 | scrollElement = window.document.documentElement; 41 | } else { 42 | scrollElement = element as HTMLElement; 43 | } 44 | return scrollElement.scrollHeight - scrollElement.scrollTop - scrollElement.clientHeight; 45 | } 46 | 47 | export function getScrollTop(element: ScrollElement): number { 48 | return 'scrollTop' in element ? element.scrollTop : element.pageYOffset; 49 | } 50 | 51 | export function setScrollTop(element: ScrollElement, value: number): void { 52 | 'scrollTop' in element ? (element.scrollTop = value) : element.scrollTo(element.scrollX, value); 53 | } 54 | 55 | export function getRootScrollTop(): number { 56 | if (typeof window === 'undefined') { 57 | return 0; 58 | } 59 | return window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0; 60 | } 61 | 62 | export function setRootScrollTop(value: number): void { 63 | if (typeof window === 'undefined') { 64 | return; 65 | } 66 | setScrollTop(window, value); 67 | setScrollTop(document.body, value); 68 | } 69 | -------------------------------------------------------------------------------- /src/utils/dom/unit.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.__esModule = true; 4 | exports.value2DomUnit = value2DomUnit; 5 | 6 | /** 7 | * 串转px或者rem 8 | */ 9 | var unit = 'px'; 10 | 11 | function value2DomUnit(value, multiple) { 12 | if (value === void 0) { 13 | value = ''; 14 | } 15 | 16 | if (multiple === void 0) { 17 | multiple = 1; 18 | } 19 | 20 | if (value === '') { 21 | return ''; 22 | } 23 | 24 | if (typeof value === 'number') { 25 | return value * multiple + 'px'; 26 | } else { 27 | unit = value.indexOf('rem') > -1 ? 'rem' : 'px'; 28 | value = value.replace(/[^\d.]/g, '').trim(); 29 | return '' + Number(value) * multiple + unit; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/utils/format/string.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 驼峰处理 3 | * b(okee-button) // 'okeeButton' 4 | */ 5 | export function camelize(str: string): string { 6 | return str.replace(/-(\w)/g, (_match, p1) => p1.toUpperCase()); 7 | } 8 | 9 | export function upperCamelize(str: string): string { 10 | return camelize(str).replace(/^(\w)/, (_match, p1) => p1.toUpperCase()); 11 | } 12 | 13 | /** 14 | * 多语言文案翻译 15 | * @param msg 源文案 16 | * @param options 格式化选项 17 | * @example 18 | * translate('normal text') // 'normal text' 19 | * translate('已选 {num} 项', { num: 10 }) // '已选 10 项' 20 | * translate('escape {{ brace }} ') // 'escape brace' 21 | */ 22 | const RE_NARGS = /(%|)\{([0-9a-zA-Z_]+)\}/g; 23 | 24 | /** 25 | * 获取字符长度 26 | * 数字英文算一个字符长度 汉字算两个字符长度 27 | */ 28 | export function getStringLen(str: string): number { 29 | // eslint-disable-next-line no-control-regex 30 | return !str ? 0 : str.replace(/[^\x00-\xff]/g, 'oe').length; 31 | } 32 | 33 | export function translate(msg: string, options: any): string { 34 | if (options.length === 1 && typeof options[0] === 'object') { 35 | options = options[0]; 36 | } 37 | 38 | if (!options || !options.hasOwnProperty) { 39 | options = {}; 40 | } 41 | 42 | return msg.replace(RE_NARGS, (match, prefix, i, index) => { 43 | let result; 44 | 45 | if (msg[index - 1] === '{' && msg[index + match.length] === '}') { 46 | return i; 47 | } else { 48 | result = Object.prototype.hasOwnProperty.call(options, i) ? options[i] : null; 49 | if (result === null || result === undefined) { 50 | return ''; 51 | } 52 | 53 | return result; 54 | } 55 | }); 56 | } 57 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 全局运行环境判断 3 | */ 4 | export const isServer = typeof window === 'undefined'; 5 | 6 | /** 7 | * 定义判断函数 8 | */ 9 | export function isDef(value: any): boolean { 10 | return value !== undefined && value !== null; 11 | } 12 | 13 | /** 14 | * 根据path取object中的value 15 | */ 16 | let result: any = {}; 17 | let keys: string[] = []; 18 | 19 | export function get(object: any, path: string): any { 20 | result = object; 21 | keys = path.split('.'); 22 | 23 | keys.forEach(key => { 24 | result = isDef(result[key]) ? result[key] : ''; 25 | }); 26 | return result; 27 | } 28 | -------------------------------------------------------------------------------- /src/utils/math.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable semi */ 2 | 3 | /** 4 | * Limit the value in specified range. 5 | * @param min specify minimum value 6 | * @param max specify maximum value 7 | */ 8 | export function clamp(value: number, min: number, max: number): number { 9 | if (value < min) { 10 | return min; 11 | } 12 | if (value > max) { 13 | return max; 14 | } 15 | return value; 16 | } 17 | 18 | /** 19 | * Create a function to limit the value in specified range. 20 | * @param min specify minimum value 21 | * @param max specify maximum value 22 | */ 23 | export function createClamp(min: number, max: number): (value: number) => number { 24 | return function (value: number): number { 25 | return clamp(value, min, max); 26 | }; 27 | } 28 | 29 | /** 30 | * Transform a integer number to fit array index in loop. 31 | * @param index the raw index 32 | * @param length the length of array 33 | */ 34 | export function clampIndexLoop(index: number, length: number): number { 35 | if (!index) { 36 | return index; 37 | } 38 | 39 | const indexInt = Math.floor(index); 40 | const lengthInt = Math.floor(length); 41 | 42 | if (indexInt > 0) { 43 | return indexInt % lengthInt; 44 | } 45 | 46 | if (length === 1) { 47 | return 0; 48 | } 49 | 50 | return (indexInt % lengthInt) + lengthInt; 51 | } 52 | 53 | /** 54 | * Create a function to transform a integer number to fit array index in loop. 55 | * @param length the length of array 56 | */ 57 | export function createClampIndexLoop(length: number): (value: number) => number { 58 | return function (value: number): number { 59 | return clampIndexLoop(value, length); 60 | }; 61 | } 62 | 63 | /** 64 | * Get correct index in a array. 65 | * If the array is empty (length 0), return -1. 66 | * @param length The length of the array 67 | * @param rawIndex The raw index to process 68 | */ 69 | export function getCorrectIndexInArray(length: number, rawIndex: number): number { 70 | if (length <= 0) { 71 | return -1; 72 | } 73 | const index = rawIndex % length; 74 | if (index < 0) { 75 | return length + index; 76 | } 77 | return index; 78 | } 79 | -------------------------------------------------------------------------------- /src/utils/struct/array.ts: -------------------------------------------------------------------------------- 1 | export function findClosestNumber(value: number, set: number[]): number { 2 | if (!set.length) return value; 3 | 4 | let closestValue = set[0]; 5 | for (let i = 0; i < set.length; i++) { 6 | if (Math.abs(value - set[i]) < Math.abs(value - closestValue)) { 7 | closestValue = set[i]; 8 | } 9 | } 10 | return closestValue; 11 | } 12 | -------------------------------------------------------------------------------- /src/utils/struct/tree.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * as tree is a component of library, use prefix "Struct" to distinguish 3 | */ 4 | 5 | export type StructTreeValue = string | number | object; 6 | 7 | export type StructTreeOption = { 8 | value: StructTreeValue; 9 | label: string; 10 | }; 11 | export interface StructTreeNode extends StructTreeOption { 12 | children?: StructTreeNode[]; 13 | needLoad?: boolean; 14 | showAll?: boolean; 15 | } 16 | export type StructTree = StructTreeNode[]; 17 | 18 | export type StructFlatTree = StructTreeOption[][]; 19 | 20 | export type TreeNodeOperator = (node: StructTreeNode) => T; 21 | 22 | export function findTreeNode(tree: StructTree, value: StructTreeValue): StructTreeNode | null { 23 | let matchedNode = null; 24 | tree.some(treeNode => { 25 | if (treeNode.value === value) { 26 | matchedNode = treeNode; 27 | return true; 28 | } 29 | }); 30 | return matchedNode; 31 | } 32 | 33 | /** 34 | * trace the tree by value 35 | */ 36 | export function traceTreeByValue( 37 | tree: StructTree, 38 | value: StructTreeValue[], 39 | operator: TreeNodeOperator, 40 | ): void { 41 | value.forEach(singleValue => { 42 | const node = findTreeNode(tree, singleValue); 43 | if (node) { 44 | operator(node); 45 | tree = node.children || []; 46 | } 47 | }); 48 | } 49 | 50 | /** 51 | * walk the tree along first branch 52 | */ 53 | export function walkTree(tree: StructTree, operator: TreeNodeOperator): void { 54 | while (tree.length) { 55 | const node = tree[0]; 56 | if (node) { 57 | operator(node); 58 | tree = node.children || []; 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/utils/types.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable semi */ 2 | /* 3 | * utils 公有 type 4 | */ 5 | 6 | export type RenderFunction = (props: T) => JSX.Element | null; 7 | 8 | export type RenderContent = 9 | | string 10 | | number 11 | | JSX.Element 12 | | RenderFunction; 13 | 14 | export declare type UnionOmit = T & Omit; 15 | 16 | export function render( 17 | renderFunction: string | JSX.Element | RenderFunction, 18 | props?: T, 19 | ): string | JSX.Element | null { 20 | if (typeof renderFunction === 'function') { 21 | return renderFunction(props); 22 | } 23 | return renderFunction; 24 | } 25 | 26 | export type DefaultTextAlignType = 'start' | 'end' | 'left' | 'right' | 'center'; 27 | -------------------------------------------------------------------------------- /ssr/.npmrc: -------------------------------------------------------------------------------- 1 | registry= https://registry.npmjs.org -------------------------------------------------------------------------------- /ssr/.yarnrc: -------------------------------------------------------------------------------- 1 | "registry" "https://registry.yarnpkg.com" -------------------------------------------------------------------------------- /ssr/build/webpack.server.js: -------------------------------------------------------------------------------- 1 | /** 2 | * webpack server config 3 | */ 4 | const path = require('path'); 5 | const nodeExternals = require('webpack-node-externals'); 6 | 7 | function resolve (dir) { 8 | return path.join(__dirname, '..', dir); 9 | } 10 | 11 | module.exports = { 12 | target: 'node', 13 | mode: 'development', 14 | entry: resolve('src/server/index'), 15 | output: { 16 | filename: 'index.js', 17 | path: resolve('lib/server'), 18 | }, 19 | externals: [nodeExternals()], 20 | resolve: { 21 | extensions: [ '.tsx', '.ts', '.js' ], 22 | }, 23 | module: { 24 | rules: [ 25 | { 26 | test: /\.tsx?$/, 27 | loader: 'babel-loader', 28 | options: { 29 | presets: [ 30 | [ 31 | '@babel/preset-env', 32 | { 33 | loose: true, 34 | modules: 'commonjs', 35 | targets: { 36 | esmodules: false, 37 | node: 'current', 38 | }, 39 | useBuiltIns: 'usage', 40 | } 41 | ], 42 | '@babel/preset-react', 43 | '@babel/preset-typescript', 44 | ], 45 | 'plugins': [ 46 | [ 47 | '@babel/plugin-transform-runtime' 48 | ], 49 | [ 50 | 'transform-class-properties' 51 | ] 52 | ] 53 | } 54 | }, 55 | { 56 | test: /\.less$/, 57 | use: [ 58 | 'null-loader' 59 | ] 60 | }, 61 | { 62 | test: /\.svg$/, 63 | loader: 'url-loader' 64 | }, 65 | ] 66 | } 67 | } -------------------------------------------------------------------------------- /ssr/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ssr", 3 | "version": "1.0.0", 4 | "description": "demo for ssr", 5 | "main": "index.js", 6 | "publishConfig": { 7 | "registry": "https://registry.npmjs.org" 8 | }, 9 | "scripts": { 10 | "start": "concurrently \"npm run dev:client\" \"npm run dev:server\" \"npm run dev:start\"", 11 | "dev:client": "webpack --config build/webpack.client.js --watch", 12 | "dev:server": "webpack --config build/webpack.server.js --watch", 13 | "dev:start": "nodemon --watch build --exec node lib/server/index.js" 14 | }, 15 | "keywords": [ 16 | "ssr" 17 | ], 18 | "author": "Totoo", 19 | "license": "MIT", 20 | "devDependencies": { 21 | "@babel/core": "^7.12.10", 22 | "@babel/plugin-transform-runtime": "^7.12.10", 23 | "@babel/preset-env": "^7.12.11", 24 | "@babel/preset-react": "^7.12.10", 25 | "@babel/preset-typescript": "^7.13.0", 26 | "@babel/runtime": "^7.12.5", 27 | "@types/koa": "^2.11.6", 28 | "@types/koa-static": "^4.0.1", 29 | "@types/node": "^14.14.16", 30 | "babel": "^6.23.0", 31 | "babel-loader": "^8.2.2", 32 | "concurrently": "^6.0.1", 33 | "install": "^0.13.0", 34 | "isomorphic-style-loader": "^5.1.0", 35 | "koa": "^2.13.0", 36 | "koa-static": "^5.0.0", 37 | "less": "^4.1.1", 38 | "less-loader": "^8.1.0", 39 | "nodemon": "^2.0.7", 40 | "npm": "^6.14.10", 41 | "null-loader": "^4.0.1", 42 | "typescript": "^4.1.3", 43 | "webpack": "^5.11.1", 44 | "webpack-cli": "^4.3.0", 45 | "webpack-node-externals": "^2.5.2" 46 | }, 47 | "dependencies": { 48 | "core-js": "3", 49 | "dayjs": "^1.10.4" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /ssr/src/client/App.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * App 3 | */ 4 | import React, {FC} from 'react'; 5 | 6 | import ButtonDemo from '../../../src/components/tabs/demo'; 7 | 8 | const App: FC = () => { 9 | return ( 10 |
11 | 12 |
13 | ); 14 | } 15 | 16 | export default App; 17 | -------------------------------------------------------------------------------- /ssr/src/client/index.less: -------------------------------------------------------------------------------- 1 | * { 2 | background-color: red; 3 | } -------------------------------------------------------------------------------- /ssr/src/client/index.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * client index 3 | */ 4 | import React from 'react'; 5 | import ReactDom from 'react-dom' 6 | import App from './App' 7 | 8 | import '../../../src/index.less'; 9 | 10 | ReactDom.hydrate( 11 | , 12 | document.getElementById('root') 13 | ) -------------------------------------------------------------------------------- /ssr/src/server/index.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * server index 3 | */ 4 | import path from 'path'; 5 | import Koa from 'koa'; 6 | import KoaStatic from 'koa-static'; 7 | import React from 'react'; 8 | import ReactDOMServer from 'react-dom/server'; 9 | import App from '../client/App'; 10 | 11 | const app = new Koa(); 12 | 13 | app.use(KoaStatic(path.join(__dirname, '..', 'client'))) 14 | 15 | app.use(async ctx => { 16 | if (ctx.request.url === '/') { 17 | ctx.body = ` 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 |
${ReactDOMServer.renderToString()}
26 | 27 | 28 | 29 | `; 30 | } 31 | }); 32 | 33 | app.listen(8080); 34 | 35 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "jsx": "react", 5 | "target": "esnext", 6 | "module": "commonjs", 7 | "moduleResolution": "node", 8 | "strict": true, 9 | "allowJs": true, 10 | "noEmit": true, 11 | "skipLibCheck": true, 12 | "noImplicitThis": true, 13 | "esModuleInterop": true 14 | }, 15 | "include": ["./typings/index.d.ts", "docs/**/*", "src/**/*"], 16 | "exclude": ["node_modules"] 17 | } 18 | -------------------------------------------------------------------------------- /typings/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.css' 2 | declare module '*.less' 3 | declare module '*.svg' 4 | 5 | -------------------------------------------------------------------------------- /workflows/build.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ main ] 9 | pull_request: 10 | branches: [ main ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [14.x] 20 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 21 | 22 | steps: 23 | - uses: actions/checkout@v2 24 | - name: Use Node.js ${{ matrix.node-version }} 25 | uses: actions/setup-node@v2 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | # - run: npm ci 29 | - run: npm i -g yarn && yarn 30 | - run: yarn build 31 | --------------------------------------------------------------------------------