├── .editorconfig
├── .eslintignore
├── .eslintrc
├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ ├── custom.md
│ └── feature_request.md
└── workflows
│ ├── publish.yml
│ └── test.yml
├── .gitignore
├── .prettierrc
├── .storybook
├── main.js
└── netlify.toml
├── CODE_OF_CONDUCT.md
├── CONTRIBUTION.md
├── LICENSE
├── README.md
├── SUMMARY.md
├── changelog.md
├── package.json
├── src
├── assets
│ └── icons
│ │ ├── loaders
│ │ ├── Circular
│ │ │ ├── index.scss
│ │ │ └── index.tsx
│ │ └── Spinner
│ │ │ ├── index.scss
│ │ │ └── index.tsx
│ │ └── vant-icons
│ │ ├── config.js
│ │ ├── encode.scss
│ │ ├── index.scss
│ │ ├── vant-icon-db1de1.ttf
│ │ ├── vant-icon-db1de1.woff
│ │ └── vant-icon-db1de1.woff2
├── components
│ ├── Button
│ │ ├── helper.tsx
│ │ ├── index.scss
│ │ ├── index.stories.tsx
│ │ ├── index.tsx
│ │ └── types.ts
│ ├── Cell
│ │ ├── index.scss
│ │ ├── index.stories.tsx
│ │ ├── index.tsx
│ │ └── types.ts
│ ├── Checkbox
│ │ ├── index.scss
│ │ ├── index.stories.tsx
│ │ └── index.tsx
│ ├── Divider
│ │ ├── index.scss
│ │ ├── index.stories.tsx
│ │ └── index.tsx
│ ├── Field
│ │ ├── index.scss
│ │ ├── index.stories.tsx
│ │ ├── index.tsx
│ │ └── types.ts
│ ├── Icons
│ │ ├── index.scss
│ │ ├── index.stories.js
│ │ └── index.tsx
│ ├── Image
│ │ ├── index.scss
│ │ ├── index.stories.tsx
│ │ └── index.tsx
│ ├── Loading
│ │ ├── index.scss
│ │ ├── index.stories.tsx
│ │ ├── index.tsx
│ │ └── types.ts
│ ├── Navbar
│ │ ├── index.scss
│ │ ├── index.stories.tsx
│ │ └── index.tsx
│ ├── Popup
│ │ ├── index.scss
│ │ ├── index.stories.tsx
│ │ ├── index.tsx
│ │ └── types.ts
│ ├── Radio
│ │ ├── index.scss
│ │ ├── index.stories.tsx
│ │ ├── index.tsx
│ │ └── types.ts
│ ├── Rate
│ │ ├── index.scss
│ │ ├── index.stories.tsx
│ │ ├── index.tsx
│ │ ├── subcomponents
│ │ │ └── rate-icon.tsx
│ │ └── types.ts
│ ├── Search
│ │ ├── index.scss
│ │ ├── index.stories.tsx
│ │ ├── index.tsx
│ │ └── types.ts
│ ├── Slider
│ │ ├── index.scss
│ │ ├── index.stories.tsx
│ │ ├── index.tsx
│ │ └── types.ts
│ ├── Stepper
│ │ ├── index.scss
│ │ ├── index.stories.tsx
│ │ └── index.tsx
│ ├── Switch
│ │ ├── index.scss
│ │ ├── index.stories.tsx
│ │ ├── index.tsx
│ │ └── types.ts
│ ├── Tag
│ │ ├── index.scss
│ │ ├── index.stories.tsx
│ │ └── index.tsx
│ ├── Toast
│ │ ├── CreateToast.tsx
│ │ ├── Toast.tsx
│ │ ├── ToastContainer.tsx
│ │ ├── index.scss
│ │ ├── index.stories.tsx
│ │ ├── index.ts
│ │ └── types.ts
│ └── template
│ │ ├── index.scss
│ │ ├── index.stories.tsx
│ │ └── index.tsx
├── index.tsx
├── react-app-env.d.ts
├── styles
│ ├── animation.scss
│ ├── colors.scss
│ ├── global.scss
│ ├── opacity.scss
│ ├── spacing.scss
│ ├── stories.scss
│ ├── style.scss
│ ├── typography.scss
│ └── variables.scss
├── types
│ ├── positions.ts
│ └── shapes.ts
├── typings.d.ts
└── utils
│ ├── base.ts
│ ├── classNames.ts
│ ├── format
│ └── unit.ts
│ ├── index.ts
│ └── validate
│ └── number.ts
├── tsconfig.json
└── tsconfig.test.json
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | indent_style = space
6 | indent_size = 2
7 | end_of_line = lf
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | build/
2 | dist/
3 | node_modules/
4 | .snapshots/
5 | *.min.js
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "@typescript-eslint/parser",
3 | "extends": [
4 | "standard",
5 | "standard-react",
6 | "plugin:prettier/recommended",
7 | "prettier/standard",
8 | "prettier/react",
9 | "plugin:@typescript-eslint/eslint-recommended"
10 | ],
11 | "overrides": [
12 | {
13 | "files": ["**/*.ts", "**/*.tsx"],
14 | "rules": {
15 | "no-unused-vars": ["off"],
16 | "no-undef": ["off"]
17 | }
18 | }
19 | ],
20 | "env": {
21 | "node": true
22 | },
23 | "parserOptions": {
24 | "ecmaVersion": 2020,
25 | "ecmaFeatures": {
26 | "legacyDecorators": true,
27 | "jsx": true
28 | }
29 | },
30 | "settings": {
31 | "react": {
32 | "version": "16"
33 | }
34 | },
35 | "rules": {
36 | "space-before-function-paren": 0,
37 | "react/prop-types": 0,
38 | "react/jsx-handler-names": 0,
39 | "react/jsx-fragments": 0,
40 | "react/no-unused-prop-types": 0,
41 | "import/export": 2,
42 | "no-extra-boolean-cast": 0
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **Desktop (please complete the following information):**
27 | - OS: [e.g. iOS]
28 | - Browser [e.g. chrome, safari]
29 | - Version [e.g. 22]
30 |
31 | **Smartphone (please complete the following information):**
32 | - Device: [e.g. iPhone6]
33 | - OS: [e.g. iOS8.1]
34 | - Browser [e.g. stock browser, safari]
35 | - Version [e.g. 22]
36 |
37 | **Additional context**
38 | Add any other context about the problem here.
39 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/custom.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Custom issue template
3 | about: Describe this issue template's purpose here.
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 |
11 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: Publish CI
2 |
3 | on:
4 | release:
5 | types: [published]
6 |
7 | jobs:
8 | build:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: actions/checkout@v1
12 | - uses: actions/setup-node@v1
13 | with:
14 | node-version: 12
15 | registry-url: https://registry.npmjs.org/
16 | - run: yarn install
17 | - run: npm publish --access public
18 | env:
19 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
20 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Test CI
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | test:
7 | runs-on: ubuntu-latest
8 | steps:
9 | - uses: actions/checkout@v1
10 | - uses: actions/setup-node@v1
11 | with:
12 | node-version: 12
13 | registry-url: https://registry.npmjs.org/
14 | - run: yarn install
15 | - run: yarn test
16 | build:
17 | runs-on: ubuntu-latest
18 | steps:
19 | - uses: actions/checkout@v1
20 | - uses: actions/setup-node@v1
21 | with:
22 | node-version: 12
23 | registry-url: https://registry.npmjs.org/
24 | - run: yarn install
25 | - run: yarn run build-storybook
26 | - run: yarn run build
27 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | build/
2 | dist/
3 | demo/
4 | node_modules/
5 | **/node_modules/
6 | .snapshots/
7 | *.min.js
8 | .idea/
9 | .vscode/
10 | .vs/
11 | **/.DS_Store
12 |
13 | # testing
14 | /coverage
15 | /storybook-static
16 |
17 | # production
18 | /build
19 |
20 | # misc
21 | .DS_Store
22 | .env.local
23 | .env.development.local
24 | .env.test.local
25 | .env.production.local
26 |
27 | package-lock.json
28 | yarn.lock
29 |
30 | npm-debug.log*
31 | yarn-debug.log*
32 | yarn-error.log*
33 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "jsxSingleQuote": true,
4 | "semi": true,
5 | "tabWidth": 2,
6 | "bracketSpacing": true,
7 | "jsxBracketSameLine": false,
8 | "arrowParens": "always",
9 | "trailingComma": "none",
10 | "endOfLine":"auto"
11 | }
12 |
--------------------------------------------------------------------------------
/.storybook/main.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | stories: ['../src/**/*.stories.js', '../src/**/*.stories.tsx'],
3 | addons: [
4 | '@storybook/preset-create-react-app',
5 | '@storybook/addon-actions',
6 | '@storybook/addon-links'
7 | ]
8 | };
9 |
--------------------------------------------------------------------------------
/.storybook/netlify.toml:
--------------------------------------------------------------------------------
1 |
2 | # COMMENT: This a rule for Single Page Applications
3 | [[redirects]]
4 | from = "/*"
5 | to = "/index.html"
6 | status = 200
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, sex characteristics, gender identity and expression,
9 | level of experience, education, socio-economic status, nationality, personal
10 | appearance, race, religion, or sexual identity and orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | * Using welcoming and inclusive language
18 | * Being respectful of differing viewpoints and experiences
19 | * Gracefully accepting constructive criticism
20 | * Focusing on what is best for the community
21 | * Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | * The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | * Trolling, insulting/derogatory comments, and personal or political attacks
28 | * Public or private harassment
29 | * Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | * Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at peter.zheng88228@gmail.com. All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
72 |
73 | [homepage]: https://www.contributor-covenant.org
74 |
75 | For answers to common questions about this code of conduct, see
76 | https://www.contributor-covenant.org/faq
77 |
--------------------------------------------------------------------------------
/CONTRIBUTION.md:
--------------------------------------------------------------------------------
1 |
9 | # Contributing to Vant React
10 |
11 | The following is a set of guidelines for contributing to Vant. Please spend several minutes in reading these guidelines before you create an issue or pull request.
12 |
13 | Anyway, these are just guidelines, not rules, use your best judgment and feel free to propose changes to this document in a pull request.
14 |
15 | ## Opening an Issue
16 |
17 | If you think you have found a bug, or have a new feature idea, please start by making sure it hasn't already been reported or fixed. You can search through existing issues and PRs to see if someone has reported one similar to yours.
18 |
19 | Next, create a new issue that briefly explains the problem, and provides a bit of background as to the circumstances that triggered it, and steps to reproduce it.
20 |
21 | ## Submitting a Pull Request
22 |
23 | It's welcomed to pull request, And there are some tips about that:
24 |
25 | - Before working on a large change, it is best to open an issue first to discuss it with the maintainers.
26 |
27 | - When in doubt, keep your pull requests small. To give a PR the best chance of getting accepted, don't bundle more than one feature or bug fix per pull request. It's always best to create two smaller PRs than one big one.
28 |
29 | - When adding new features or modifying existing, please attempt to include tests to confirm the new behavior.
30 |
31 | - Rebase before creating a PR to keep commit history clear.
32 |
33 | - Create a brunch name as the standard “contributer firstname/component name or feature name.
34 |
35 | - Add your branch name as PR title.
36 |
37 | - Add some descriptions and refer relative issues for your PR.
38 |
39 | ## Coding suggestion
40 |
41 | It's the suggestions for your coding
42 |
43 | - To define a prop as `string | ReactNode` if possible, developers usually need to support putting a DOM element in the place where the string is placed.
44 |
45 | ## Getting started
46 |
47 | ```
48 | git clone https://github.com/mxdi9i7/vant-react.git
49 |
50 | cd vant-react
51 |
52 | npm install
53 |
54 | npm run storybook
55 | # open http://localhost:9009
56 | ```
57 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) Vant React Open Source Software. and its affiliates.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # **Vant React**
2 |
3 | [](https://badge.fury.io/js/vant-react)
4 | [](LICENSE)
5 | 
6 | [](https://vant.bctc.io)
7 | [](https://vant.bctc.io)
8 |
9 | Lightweight Mobile UI Components built on Typescript and React in under 2kb!
10 |
11 | ## **Features**
12 |
13 | - Support Typescript
14 | - 60+ Reusable components
15 | - 100% Storybook coverage: [https://vant.bctc.io](https://vant.bctc.io)
16 | - Extensive documentation and demos
17 |
18 | ## Install
19 |
20 | ```text
21 | # Using npm
22 | npm i vant-react -S
23 |
24 | # Using yarn
25 | yarn add vant-react
26 | ```
27 |
28 | ## Quickstart
29 |
30 | ```text
31 | import React from 'react';
32 | import { Button } from 'vant-react';
33 | import 'vant-react/dist/index.css';
34 |
35 | const App = () => {
36 | return (
37 |
38 | );
39 | };
40 | ```
41 |
42 | ## Components completion status
43 |
44 | ### Basic Components
45 |
46 | - [x] Button
47 | - [x] Cell
48 | - [x] Icon
49 | - [x] Image
50 | - [ ] Layout
51 | - [x] Popup
52 | - [x] Built-in style
53 | - [x] Toast
54 |
55 | ### Form Components
56 |
57 | - [ ] Calendar
58 | - [ ] Cascader
59 | - [x] Checkbox
60 | - [ ] DatetimePicker
61 | - [x] Field
62 | - [ ] Form
63 | - [ ] NumberKeyboard
64 | - [ ] PasswordInput
65 | - [ ] Picker
66 | - [x] Radio
67 | - [x] Rate
68 | - [x] Search
69 | - [x] Slider
70 | - [x] Stepper
71 | - [x] Switch
72 | - [ ] Uploader
73 |
74 | ### Action Components
75 |
76 | - [ ] ActionSheet
77 | - [ ] Dialog
78 | - [ ] DropdownMenu
79 | - [x] Loading
80 | - [ ] Notify
81 | - [ ] Overlay
82 | - [ ] PullRefresh
83 | - [ ] ShareSheet
84 | - [ ] SwipeCell
85 |
86 | ### Display Components
87 |
88 | - [ ] Badge
89 | - [ ] Circle
90 | - [ ] Collapse
91 | - [ ] CountDown
92 | - [x] Divider
93 | - [ ] Empty
94 | - [ ] ImagePreview
95 | - [ ] Lazyload
96 | - [ ] List
97 | - [ ] NoticeBar
98 | - [ ] Popover
99 | - [ ] Progress
100 | - [ ] Skeleton
101 | - [ ] Steps
102 | - [ ] Sticky
103 | - [ ] Swipe
104 | - [x] Tag
105 |
106 | ### Navigation Components
107 |
108 | - [ ] Grid
109 | - [ ] IndexBar
110 | - [x] NavBar
111 | - [ ] Pagination
112 | - [ ] Sidebar
113 | - [ ] Tab
114 | - [ ] Tabbar
115 | - [ ] TreeSelect
116 |
117 | ### Business Components
118 |
119 | - [ ] AddressEdit
120 | - [ ] AddressList
121 | - [ ] Area
122 | - [ ] Card
123 | - [ ] ContactCard
124 | - [ ] ContactEdit
125 | - [ ] ContactList
126 | - [ ] Coupon
127 | - [ ] GoodsAction
128 | - [ ] SubmitBar
129 | - [ ] Sku
130 |
131 | ### Deprecated
132 |
133 | - [ ] SwitchCell
134 | - [ ] Panel
135 |
136 | ## Contribution
137 |
138 | If you like what we do, please consider buy us a bubble-tea(donate)!
139 |
140 | [](https://donorbox.org/vant-react-dev-team?default_interval=o&amount=5)
141 |
142 | Please make sure to read the[ Contributing Guide](./CONTRIBUTION.md) before making a pull request.
143 |
144 | Or, join our discussion group on Wechat by adding me!
145 |
146 | 
147 |
148 | ## Browser Support
149 |
150 | Modern browsers and Android 4.0+, iOS 8.0+.
151 |
152 | ## Open Source License
153 |
154 | This is an Open Source Software operating under the [MIT](https://github.com/mxdi9i7/vant-react/blob/master/LICENSE) License.
155 |
--------------------------------------------------------------------------------
/SUMMARY.md:
--------------------------------------------------------------------------------
1 | # Table of contents
2 |
3 | * [Introduction](README.md)
4 |
5 | ## Essentials
6 |
7 | * [Changelog](essentials/changelog.md)
8 |
9 | ## Basic Components
10 |
11 | * [Button](button-cell-icon-image-layout-popup-builtinstyle/button.md)
12 |
13 | ## Form Components
14 |
15 | * [Untitled](form-components/untitled.md)
16 |
17 | ## Action Components
18 |
19 | * [Untitled](action-components/untitled.md)
20 |
21 | ## Display Components
22 |
23 | * [Untitled](display-components/untitled.md)
24 |
25 | ## Navigation Components
26 |
27 | * [Navbar](navigation-components/navbar.md)
28 |
29 | ## Business Components
30 |
31 | * [Untitled](business-components/untitled.md)
32 |
33 | ## Deprecated
34 |
35 | * [Untitled](deprecated/untitled.md)
36 |
37 |
--------------------------------------------------------------------------------
/changelog.md:
--------------------------------------------------------------------------------
1 | # CHANGELOG
2 |
3 | ## Intro
4 |
5 | Vant follows Semantic Versioning 0.2.7.
6 |
7 | **Release Schedule**
8 |
9 | - Patch version:released weekly, including features and bug fixes.
10 | - Minor version:released every one to two months, including backwards compatible features.
11 | - Major version:including breaking changes and new features.
12 |
13 | ## **0**.2.7
14 |
15 | -08-17-2020
16 |
17 | **Bug Fixes**
18 |
19 | - Stepper
20 | - adjusted stepper margin
21 |
22 | ## **0**.2.6
23 |
24 | -08-15-2020
25 |
26 | **Bug Fixes**
27 |
28 | - Stepper
29 | - adjusted stepper layout
30 |
31 | ## **0**.2.5
32 |
33 | -08-13-2020
34 |
35 | **Bug Fixes**
36 |
37 | - Stepper
38 | - fixed export component
39 |
40 | ## **0**.2.4
41 |
42 | -08-12-2020
43 |
44 | **Feature**
45 |
46 | - Added Image components [\#44](https://github.com/mxdi9i7/vant-react/pull/44)
47 | - Added Checkbox components [\#67](https://github.com/mxdi9i7/vant-react/pull/67)
48 | - Added Stepper components [\#34](https://github.com/mxdi9i7/vant-react/pull/34)
49 | - Added Slider components [\#60](https://github.com/mxdi9i7/vant-react/pull/60)
50 |
51 | ## **0**.2.3
52 |
53 | -07-25-2020
54 |
55 | **Bug Fixes**
56 |
57 | - Popup:
58 | - revert and fix custom size, hided scrollbar, add padding and fit-content
59 |
60 | ## **0**.2.2
61 |
62 | -06-24-2020
63 |
64 | **Bug Fixes**
65 |
66 | - Popup:
67 | - fix custom size and hided scrollbar
68 | - add padding and fit-content
69 |
70 | ## **0**.2.1
71 |
72 | -06-18-2020
73 |
74 | **Bug Fixes**
75 |
76 | - Popup:
77 | - Adjusted position
78 |
79 | ## **0**.2.0
80 |
81 | -06-17-2020
82 |
83 | **Feature**
84 |
85 | - Added Field components [\#26](https://github.com/mxdi9i7/vant-react/pull/26)
86 | - Added Search components [\#31](https://github.com/mxdi9i7/vant-react/pull/31)
87 | - Added Rate components [\#32](https://github.com/mxdi9i7/vant-react/pull/32)
88 | - Added Popup components [\#33](https://github.com/mxdi9i7/vant-react/pull/33)
89 | - Added Cell components [\#33](https://github.com/mxdi9i7/vant-react/pull/42)
90 | - Added Built-in style
91 | - Basic docs: Popup & Field & Rate
92 | - Added changelogs
93 |
94 | **Bug Fixes**
95 |
96 | - Button:
97 | - Enabled icon buttons
98 | - Icon:
99 | - Added handle icon click
100 | - Adjusted icon librart sryles
101 |
102 | ## **0**.1.1
103 |
104 | -05-25-2020
105 |
106 | **Bug Fixes**
107 |
108 | - Button:
109 | - Enabled icon buttons, need to have icon library done first
110 | - Accepted linear gradient
111 | - Tag:
112 | - Accepted custom tag element
113 |
114 | ## **0**.1.0
115 |
116 | -05-24-2020
117 |
118 | **Feature**
119 |
120 | - Added Button components [\#4](https://github.com/mxdi9i7/vant-react/pull/4)
121 | - Added Icons components [\#6](https://github.com/mxdi9i7/vant-react/pull/6)
122 | - Added Navbar components [\#8](https://github.com/mxdi9i7/vant-react/pull/8)
123 | - Added Tag components [\#9](https://github.com/mxdi9i7/vant-react/pull/9)
124 | - Basic docs: Navbar & Tag & Icon
125 | - Added changelogs
126 |
127 | **Bug Fixes**
128 |
129 | - Button:
130 | - Added hairline props
131 | - Accepted color in rgb
132 | - Navbar:
133 | - Fixed navbar font weight
134 | - Added navbar prop -> border: boolean, default is true
135 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vant-react",
3 | "version": "0.3.1",
4 | "description": "Lightweight Mobile UI Components built in React & Typescript, inspired by Vant: https://youzan.github.io/vant",
5 | "author": "mxdi9i7",
6 | "license": "MIT",
7 | "repository": "mxdi9i7/vant-react",
8 | "main": "dist/index.js",
9 | "module": "dist/index.modern.js",
10 | "source": "src/index.tsx",
11 | "engines": {
12 | "node": ">=10"
13 | },
14 | "scripts": {
15 | "build": "microbundle-crl --no-compress --format modern,cjs --css-modules false",
16 | "start": "microbundle-crl watch --no-compress --format modern,cjs --css-modules false",
17 | "prepublish": "run-s build",
18 | "test": "run-s test:unit test:lint test:build",
19 | "test:build": "run-s build",
20 | "test:lint": "run-s lint",
21 | "test:unit": "cross-env CI=1 react-scripts test --env=jsdom --passWithNoTests",
22 | "test:watch": "react-scripts test --env=jsdom",
23 | "predeploy": "cd example && yarn install && yarn run build",
24 | "deploy": "gh-pages -d example/build",
25 | "lint": "eslint --ext .tsx ./src",
26 | "lint:watch": "esw --watch --fix --ext .tsx ./src",
27 | "storybook": "start-storybook -p 9009",
28 | "build-storybook": "build-storybook"
29 | },
30 | "peerDependencies": {
31 | "react": "^16.0.0"
32 | },
33 | "devDependencies": {
34 | "@storybook/addon-actions": "^5.3.18",
35 | "@storybook/addon-links": "^5.3.18",
36 | "@storybook/addons": "^5.3.18",
37 | "@storybook/preset-create-react-app": "^3.0.0",
38 | "@storybook/react": "^5.3.18",
39 | "@types/jest": "^25.1.4",
40 | "@types/react": "^16.9.27",
41 | "@typescript-eslint/eslint-plugin": "^2.26.0",
42 | "@typescript-eslint/parser": "^2.26.0",
43 | "babel-eslint": "^10.0.3",
44 | "cross-env": "^7.0.2",
45 | "eslint": "^6.8.0",
46 | "eslint-config-prettier": "^6.7.0",
47 | "eslint-config-standard": "^14.1.0",
48 | "eslint-config-standard-react": "^9.2.0",
49 | "eslint-plugin-import": "^2.18.2",
50 | "eslint-plugin-node": "^11.0.0",
51 | "eslint-plugin-prettier": "^3.1.1",
52 | "eslint-plugin-promise": "^4.2.1",
53 | "eslint-plugin-react": "^7.17.0",
54 | "eslint-plugin-standard": "^4.0.1",
55 | "eslint-watch": "^6.0.1",
56 | "gh-pages": "^2.2.0",
57 | "husky": "^4.2.3",
58 | "microbundle-crl": "^0.13.10",
59 | "npm-run-all": "^4.1.5",
60 | "prettier": "^2.0.4",
61 | "react": "^16.13.1",
62 | "react-dom": "^16.13.1",
63 | "react-scripts": "^3.4.3",
64 | "serve": "^11.3.0"
65 | },
66 | "files": [
67 | "dist"
68 | ],
69 | "eslintConfig": {
70 | "extends": "react-app"
71 | },
72 | "husky": {
73 | "hooks": {
74 | "pre-commit": "npm run lint",
75 | "pre-push": "npm run lint"
76 | }
77 | },
78 | "browserslist": {
79 | "production": [
80 | ">0.2%",
81 | "not dead",
82 | "not op_mini all"
83 | ],
84 | "development": [
85 | "last 1 chrome version",
86 | "last 1 firefox version",
87 | "last 1 safari version"
88 | ]
89 | },
90 | "dependencies": {
91 | "node-sass": "^4.14.1",
92 | "shortid": "^2.2.15"
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/src/assets/icons/loaders/Circular/index.scss:
--------------------------------------------------------------------------------
1 | @import '../../../../styles/colors.scss';
2 | @import '../../../../styles/variables.scss';
3 |
4 | .circular-loading {
5 | height: $loader-size;
6 | width: $loader-size;
7 | svg {
8 | animation-timing-function: linear;
9 | animation-name: svg-animation;
10 | animation-duration: $loader-animation-duration;
11 | animation-iteration-count: infinite;
12 | }
13 |
14 | circle {
15 | animation-timing-function: ease-in-out;
16 | animation-duration: $loader-circular-duration;
17 | animation-iteration-count: infinite;
18 | animation-name: circle-animation;
19 | animation-fill-mode: both;
20 | display: block;
21 | fill: transparent;
22 | stroke: $default;
23 | stroke-linecap: round;
24 | stroke-dasharray: 283;
25 | stroke-dashoffset: 280;
26 | stroke-width: 10px;
27 | transform-origin: 50% 50%;
28 | }
29 |
30 | @keyframes circle-animation {
31 | 0%,
32 | 25% {
33 | stroke-dashoffset: 280;
34 | transform: rotate(0);
35 | }
36 |
37 | 50%,
38 | 75% {
39 | stroke-dashoffset: 75;
40 | transform: rotate(45deg);
41 | }
42 |
43 | 100% {
44 | stroke-dashoffset: 280;
45 | transform: rotate(360deg);
46 | }
47 | }
48 |
49 | @keyframes svg-animation {
50 | 0% {
51 | transform: rotateZ(0deg);
52 | }
53 | 100% {
54 | transform: rotateZ(360deg);
55 | }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/assets/icons/loaders/Circular/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactElement } from 'react';
2 | import './index.scss';
3 |
4 | interface Props {
5 | className?: string;
6 | loadingSize?: string;
7 | color?: string;
8 | }
9 |
10 | export default function CircularLoading({
11 | className,
12 | loadingSize,
13 | color
14 | }: Props): ReactElement {
15 | return (
16 |
20 |
27 |
28 | );
29 | }
30 |
--------------------------------------------------------------------------------
/src/assets/icons/loaders/Spinner/index.scss:
--------------------------------------------------------------------------------
1 | @import '../../../../styles/variables.scss';
2 | @import '../../../../styles/colors.scss';
3 |
4 | .spinner-loading {
5 | height: $loader-size * 2;
6 | width: $loader-size * 2;
7 | svg {
8 | margin: auto;
9 | display: block;
10 | shape-rendering: auto;
11 |
12 | rect {
13 | width: 3px;
14 | height: 12px;
15 | fill: $default;
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/assets/icons/loaders/Spinner/index.tsx:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line no-unused-vars
2 | import React, { ReactElement } from 'react';
3 | import './index.scss';
4 |
5 | interface Props {
6 | className?: string;
7 | loadingSize?: string;
8 | }
9 |
10 | export default function SpinnerLoading({
11 | className,
12 | loadingSize
13 | }: Props): ReactElement {
14 | return (
15 |
19 |
169 |
170 | );
171 | }
172 |
--------------------------------------------------------------------------------
/src/assets/icons/vant-icons/config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | name: 'vant-icon',
3 | basic: [
4 | 'success',
5 | 'plus',
6 | 'cross',
7 | 'fail',
8 | 'arrow',
9 | 'arrow-left',
10 | 'arrow-up',
11 | 'arrow-down'
12 | ],
13 | outline: [
14 | // has corresponding filled icon
15 | 'location-o',
16 | 'like-o',
17 | 'star-o',
18 | 'phone-o',
19 | 'setting-o',
20 | 'fire-o',
21 | 'coupon-o',
22 | 'cart-o',
23 | 'shopping-cart-o',
24 | 'cart-circle-o',
25 | 'friends-o',
26 | 'comment-o',
27 | 'gem-o',
28 | 'gift-o',
29 | 'point-gift-o',
30 | 'send-gift-o',
31 | 'service-o',
32 | 'bag-o',
33 | 'todo-list-o',
34 | 'balance-list-o',
35 | 'close',
36 | 'clock-o',
37 | 'question-o',
38 | 'passed',
39 | 'add-o',
40 | 'gold-coin-o',
41 | 'info-o',
42 | 'play-circle-o',
43 | 'pause-circle-o',
44 | 'stop-circle-o',
45 | 'warning-o',
46 | 'phone-circle-o',
47 | 'music-o',
48 | 'smile-o',
49 | 'thumb-circle-o',
50 | 'comment-circle-o',
51 | 'browsing-history-o',
52 | 'underway-o',
53 | 'more-o',
54 | 'video-o',
55 | 'shop-o',
56 | 'shop-collect-o',
57 | 'chat-o',
58 | 'smile-comment-o',
59 | 'vip-card-o',
60 | 'award-o',
61 | 'diamond-o',
62 | 'volume-o',
63 | 'cluster-o',
64 | 'wap-home-o',
65 | 'photo-o',
66 | 'gift-card-o',
67 | 'expand-o',
68 | 'medal-o',
69 | 'good-job-o',
70 | 'manager-o',
71 | 'label-o',
72 | 'bookmark-o',
73 | 'bill-o',
74 | 'hot-o',
75 | 'hot-sale-o',
76 | 'new-o',
77 | 'new-arrival-o',
78 | 'goods-collect-o',
79 | 'eye-o',
80 | // without corresponding filled icon
81 | 'balance-o',
82 | 'refund-o',
83 | 'birthday-cake-o',
84 | 'user-o',
85 | 'orders-o',
86 | 'tv-o',
87 | 'envelop-o',
88 | 'flag-o',
89 | 'flower-o',
90 | 'filter-o',
91 | 'bar-chart-o',
92 | 'chart-trending-o',
93 | 'brush-o',
94 | 'bullhorn-o',
95 | 'hotel-o',
96 | 'cashier-o',
97 | 'newspaper-o',
98 | 'warn-o',
99 | 'notes-o',
100 | 'calender-o',
101 | 'bulb-o',
102 | 'user-circle-o',
103 | 'desktop-o',
104 | 'apps-o',
105 | 'home-o',
106 | 'search',
107 | 'points',
108 | 'edit',
109 | 'delete',
110 | 'qr',
111 | 'qr-invalid',
112 | 'closed-eye',
113 | 'down',
114 | 'scan',
115 | 'free-postage',
116 | 'certificate',
117 | 'logistics',
118 | 'contact',
119 | 'cash-back-record',
120 | 'after-sale',
121 | 'exchange',
122 | 'upgrade',
123 | 'ellipsis',
124 | 'circle',
125 | 'description',
126 | 'records',
127 | 'sign',
128 | 'completed',
129 | 'failure',
130 | 'ecard-pay',
131 | 'peer-pay',
132 | 'balance-pay',
133 | 'credit-pay',
134 | 'debit-pay',
135 | 'cash-on-deliver',
136 | 'other-pay',
137 | 'tosend',
138 | 'pending-payment',
139 | 'paid',
140 | 'aim',
141 | 'discount',
142 | 'idcard',
143 | 'replay',
144 | 'shrink'
145 | ],
146 | filled: [
147 | // has corresponding outline icon
148 | 'location',
149 | 'like',
150 | 'star',
151 | 'phone',
152 | 'setting',
153 | 'fire',
154 | 'coupon',
155 | 'cart',
156 | 'shopping-cart',
157 | 'cart-circle',
158 | 'friends',
159 | 'comment',
160 | 'gem',
161 | 'gift',
162 | 'point-gift',
163 | 'send-gift',
164 | 'service',
165 | 'bag',
166 | 'todo-list',
167 | 'balance-list',
168 | 'clear',
169 | 'clock',
170 | 'question',
171 | 'checked',
172 | 'add',
173 | 'gold-coin',
174 | 'info',
175 | 'play-circle',
176 | 'pause-circle',
177 | 'stop-circle',
178 | 'warning',
179 | 'phone-circle',
180 | 'music',
181 | 'smile',
182 | 'thumb-circle',
183 | 'comment-circle',
184 | 'browsing-history',
185 | 'underway',
186 | 'more',
187 | 'video',
188 | 'shop',
189 | 'shop-collect',
190 | 'chat',
191 | 'smile-comment',
192 | 'vip-card',
193 | 'award',
194 | 'diamond',
195 | 'volume',
196 | 'cluster',
197 | 'wap-home',
198 | 'photo',
199 | 'gift-card',
200 | 'expand',
201 | 'medal',
202 | 'good-job',
203 | 'manager',
204 | 'label',
205 | 'bookmark',
206 | 'bill',
207 | 'hot',
208 | 'hot-sale',
209 | 'new',
210 | 'new-arrival',
211 | 'goods-collect',
212 | 'eye',
213 | // without corresponding outline icon
214 | 'share',
215 | 'alipay',
216 | 'wechat',
217 | 'photograph',
218 | 'youzan-shield',
219 | 'umbrella-circle',
220 | 'bell',
221 | 'printer',
222 | 'map-marked',
223 | 'card',
224 | 'add-square',
225 | 'live',
226 | 'lock',
227 | 'audio',
228 | 'graphic',
229 | 'column',
230 | 'invition',
231 | 'play',
232 | 'pause',
233 | 'stop',
234 | 'weapp-nav',
235 | 'ascending',
236 | 'descending',
237 | 'bars',
238 | 'wap-nav'
239 | ]
240 | };
241 |
--------------------------------------------------------------------------------
/src/assets/icons/vant-icons/vant-icon-db1de1.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mxdi9i7/vant-react/e7f62d4ddac1e36a79b70bc5e222bf5fd9b45649/src/assets/icons/vant-icons/vant-icon-db1de1.ttf
--------------------------------------------------------------------------------
/src/assets/icons/vant-icons/vant-icon-db1de1.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mxdi9i7/vant-react/e7f62d4ddac1e36a79b70bc5e222bf5fd9b45649/src/assets/icons/vant-icons/vant-icon-db1de1.woff
--------------------------------------------------------------------------------
/src/assets/icons/vant-icons/vant-icon-db1de1.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mxdi9i7/vant-react/e7f62d4ddac1e36a79b70bc5e222bf5fd9b45649/src/assets/icons/vant-icons/vant-icon-db1de1.woff2
--------------------------------------------------------------------------------
/src/components/Button/helper.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactElement } from 'react';
2 |
3 | import SpinnerLoading from '../../assets/icons/loaders/Spinner';
4 | import CircularLoading from '../../assets/icons/loaders/Circular';
5 |
6 | import { LoadingIconProps } from './types';
7 |
8 | export const renderLoadingIcon = ({
9 | className,
10 | loadingType = 'circular',
11 | loadingText,
12 | loadingSize
13 | }: LoadingIconProps): ReactElement => {
14 | return (
15 |
16 | {loadingType === 'spinner' ? (
17 |
18 | ) : (
19 |
20 | )}
21 | {loadingText && {loadingText}}
22 |
23 | );
24 | };
25 |
26 | export const getContrastTextColor = (colorHex: string): string => {
27 | let r, g, b;
28 | if (colorHex.length !== 6 && colorHex.slice(0, 3) !== 'rgb') {
29 | return 'black';
30 | } // return black if color is not supplied
31 | else if (colorHex.length === 6) {
32 | r = parseInt(colorHex[0] + colorHex[1], 16);
33 | g = parseInt(colorHex[2] + colorHex[3], 16);
34 | b = parseInt(colorHex[4] + colorHex[5], 16);
35 | } else if (colorHex.length !== 6 && colorHex.slice(0, 3) === 'rgb') {
36 | const startIndex = colorHex.indexOf('(');
37 | const endIndex = colorHex.indexOf(')');
38 | const rgb = colorHex
39 | .slice(startIndex + 1, endIndex - startIndex)
40 | .split(',');
41 | r = rgb[0];
42 | g = rgb[1];
43 | b = rgb[2];
44 | }
45 | const brightness = (r * 299 + g * 587 + b * 114) / 1000;
46 | return brightness > 125 ? 'black' : 'white';
47 | };
48 |
49 | export const colorType = (colorHex: string): string => {
50 | if (colorHex.length !== 6 && colorHex.slice(0, 3) === 'rgb') return colorHex;
51 | const result = '#' + colorHex;
52 | return result;
53 | };
54 |
--------------------------------------------------------------------------------
/src/components/Button/index.scss:
--------------------------------------------------------------------------------
1 | @import '../../styles/colors.scss';
2 | @import '../../styles/spacing.scss';
3 | @import '../../styles/typography.scss';
4 | @import '../../styles/opacity.scss';
5 | @import '../../styles/variables.scss';
6 |
7 | $baseClass: 'vant-button';
8 |
9 | button.#{$baseClass},
10 | a.#{$baseClass} {
11 | @include normal;
12 | display: inline-flex;
13 | justify-content: center;
14 | align-items: center;
15 | position: relative;
16 | color: $dark-text;
17 | background-color: $default;
18 | padding: 0 $padding-sm;
19 | border-width: 1px;
20 | border-style: solid;
21 | border-color: transparent;
22 | height: $button-height;
23 | border-radius: 2px;
24 | cursor: pointer;
25 | transition: opacity 0.2s;
26 | outline: none;
27 | text-decoration: none;
28 |
29 | &::before {
30 | position: absolute;
31 | top: 50%;
32 | left: 50%;
33 | width: 100%;
34 | height: 100%;
35 | background-color: $black;
36 | border: inherit;
37 | border-color: $black;
38 | border-radius: inherit; /* inherit parent's border radius */
39 | transform: translate(-50%, -50%);
40 | opacity: 0;
41 | content: ' ';
42 | }
43 | &:first-child {
44 | i {
45 | margin: 0px;
46 | }
47 | }
48 |
49 | &:active {
50 | &::before {
51 | opacity: 0.1;
52 | }
53 | }
54 |
55 | &__default {
56 | background-color: $default;
57 | border-color: $grey-text;
58 | color: $dark-text;
59 | }
60 |
61 | &__primary {
62 | background-color: $primary;
63 | border-color: $primary;
64 | color: $light-text;
65 | }
66 | &__info {
67 | background-color: $info;
68 | border-color: $info;
69 | color: $light-text;
70 | }
71 | &__danger {
72 | background-color: $danger;
73 | border-color: $danger;
74 | color: $light-text;
75 | }
76 | &__warning {
77 | background-color: $warning;
78 | border-color: $warning;
79 | color: $light-text;
80 | }
81 |
82 | &__plain {
83 | background-color: $default;
84 | &.#{$baseClass}__primary {
85 | color: $primary;
86 | }
87 | &.#{$baseClass}__info {
88 | color: $info;
89 | }
90 | &.#{$baseClass}__danger {
91 | color: $danger;
92 | }
93 | &.#{$baseClass}__warning {
94 | color: $warning;
95 | }
96 | }
97 |
98 | &__hairline {
99 | border-width: 0.5px;
100 | padding: 0.5px $padding-sm + 0.5px;
101 | }
102 |
103 | &__square {
104 | border-radius: 0;
105 | }
106 | &__round {
107 | border-radius: $button-height / 2;
108 | }
109 |
110 | &__block {
111 | display: flex;
112 | width: 100%;
113 | }
114 |
115 | &__loading {
116 | cursor: not-allowed;
117 |
118 | span {
119 | margin-left: 5px;
120 | }
121 | }
122 |
123 | &__loading,
124 | &__disabled {
125 | &::before {
126 | display: none;
127 | }
128 | }
129 |
130 | &__disabled {
131 | cursor: not-allowed;
132 | opacity: $button-disabled-opacity;
133 | }
134 |
135 | &__large {
136 | height: 50px;
137 | font-size: 16px;
138 | }
139 |
140 | &__small {
141 | height: 30px;
142 | font-size: 12px;
143 | font-weight: 300;
144 | padding: 0 $space-md;
145 | }
146 |
147 | &__mini {
148 | height: 22px;
149 | font-size: 10px;
150 | padding: 0 $space-sm;
151 | }
152 |
153 | img {
154 | height: $loader-size;
155 | width: $loader-size;
156 | object-fit: cover;
157 | object-position: 50% 50%;
158 | margin-right: 5px;
159 | }
160 |
161 | .vant-icon__container {
162 | margin-right: 5px;
163 | }
164 |
165 | &__onlyIcon {
166 | .vant-icon__container {
167 | margin-right: 0px;
168 | }
169 | }
170 | }
171 |
--------------------------------------------------------------------------------
/src/components/Button/index.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Button from '.';
3 | import '../../styles/stories.scss';
4 |
5 | export default {
6 | title: 'Button',
7 | component: Button
8 | };
9 |
10 | export const ButtonTypes = () => (
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | );
19 |
20 | export const PlainButtons = () => (
21 |
22 |
25 |
28 |
29 | );
30 |
31 | export const HairlineButtons = () => (
32 |
33 |
36 |
39 |
40 | );
41 |
42 | export const DisabledButtons = () => (
43 |
44 |
47 |
50 |
51 | );
52 |
53 | export const LoadingButtons = () => (
54 |
55 |
58 |
61 |
64 |
67 |
68 | );
69 |
70 | export const ButtonShapes = () => (
71 |
72 |
75 |
78 |
79 | );
80 |
81 | export const ButtonSize = () => (
82 |
83 |
86 |
89 |
92 |
93 | );
94 |
95 | export const ButtonColor = () => (
96 |
97 |
98 |
101 |
104 |
105 | );
106 |
107 | export const ButtonTags = () => (
108 |
109 |
110 |
111 |
112 | );
113 |
114 | export const ButtonNativeTypes = () => (
115 |
116 |
117 |
118 |
119 |
120 | );
121 |
122 | export const BlockButtons = () => (
123 |
124 |
125 |
126 |
127 | );
128 |
129 | export const IconButton = () => (
130 |
131 |
132 |
135 |
138 |
139 | );
140 |
141 | export const ButtonURL = () => (
142 |
143 |
146 |
149 |
152 |
153 | );
154 |
155 | export const ButtonAction = () => (
156 |
157 |
158 |
159 |
162 |
170 | );
171 |
--------------------------------------------------------------------------------
/src/components/Button/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { renderLoadingIcon, getContrastTextColor, colorType } from './helper';
4 | import classnames from '../../utils/classNames';
5 |
6 | import { Props } from './types';
7 | import Icon from '../Icons';
8 |
9 | import './index.scss';
10 |
11 | const baseClass = 'vant-button';
12 |
13 | export default function Button({
14 | text,
15 | children,
16 | type = 'default',
17 | plain,
18 | disabled,
19 | loading,
20 | loadingType,
21 | loadingText,
22 | loadingSize,
23 | round,
24 | square,
25 | color,
26 | fontColor,
27 | tag,
28 | nativeType,
29 | block,
30 | url,
31 | replace,
32 | onClick,
33 | onTouchStart,
34 | icon,
35 | hairline,
36 | size = 'normal'
37 | }: Props) {
38 | const CustomTag = tag || 'button';
39 | const props = {
40 | className: classnames(baseClass, [
41 | { type },
42 | { plain: plain || hairline },
43 | { disabled },
44 | { loading },
45 | { round },
46 | { square },
47 | { block },
48 | { hairline },
49 | { [size]: size },
50 | { onlyIcon: !text && !children }
51 | ]),
52 | style: {}
53 | };
54 |
55 | if (nativeType) Object.assign(props, { nativeType });
56 |
57 | if (loadingSize)
58 | Object.assign(props, { style: { ...props.style, height: loadingSize } });
59 |
60 | if (fontColor)
61 | Object.assign(props, { style: { ...props.style, color: fontColor } });
62 |
63 | if (color) {
64 | if (color.indexOf('linear-gradient') === -1) {
65 | Object.assign(props, {
66 | style: {
67 | ...props.style,
68 | color: fontColor || getContrastTextColor(color),
69 | backgroundColor: colorType(color),
70 | borderColor: colorType(color)
71 | }
72 | });
73 | } else {
74 | Object.assign(props, {
75 | style: {
76 | ...props.style,
77 | color: fontColor || getContrastTextColor(color),
78 | background: color
79 | }
80 | });
81 | }
82 | }
83 |
84 | if (disabled)
85 | Object.assign(props, {
86 | disabled
87 | });
88 |
89 | if (url && tag === 'a') {
90 | Object.assign(props, {
91 | href: url
92 | });
93 | if (replace) {
94 | Object.assign(props, {
95 | target: '_self'
96 | });
97 | } else {
98 | Object.assign(props, {
99 | target: '_blank'
100 | });
101 | }
102 | }
103 |
104 | if (onClick) {
105 | Object.assign(props, {
106 | onClick
107 | });
108 | }
109 |
110 | if (onClick && loading) {
111 | Object.assign(props, {
112 | onClick: () => {}
113 | });
114 | }
115 |
116 | if (onTouchStart) {
117 | Object.assign(props, {
118 | onTouchStart
119 | });
120 | }
121 |
122 | if (onTouchStart && loading) {
123 | Object.assign(props, {
124 | onTouchStart: () => {}
125 | });
126 | }
127 |
128 | const NAV_ICON_SIZE = '16px';
129 |
130 | return (
131 |
132 | {icon?.includes('.') || icon?.includes('http')
133 | ? icon &&
134 | : icon && }
135 | {loading
136 | ? renderLoadingIcon({
137 | className: loadingType
138 | ? `${baseClass}__${loadingType}`
139 | : `${baseClass}__circular`,
140 | loadingType,
141 | loadingText,
142 | loadingSize
143 | })
144 | : text || children}
145 |
146 | );
147 | }
148 |
--------------------------------------------------------------------------------
/src/components/Button/types.ts:
--------------------------------------------------------------------------------
1 | export interface Props {
2 | text?: string;
3 | color?: string;
4 | fontColor?: string;
5 | children?: string;
6 | loadingText?: string;
7 | loadingSize?: string;
8 | size?: 'large' | 'small' | 'mini' | 'normal';
9 | icon?: string;
10 | hairline?: boolean;
11 | url?: string;
12 | plain?: boolean;
13 | loading?: boolean;
14 | disabled?: boolean;
15 | round?: boolean;
16 | square?: boolean;
17 | replace?: boolean;
18 | block?: boolean;
19 | tag?: 'button' | 'a';
20 | nativeType?: 'button' | 'submit' | 'reset';
21 | type?: ButtonTypes;
22 | loadingType?: LoadingTypes;
23 | onClick?: Function;
24 | onTouchStart?: Function;
25 | }
26 |
27 | export interface LoadingIconProps {
28 | className: string;
29 | loadingType: LoadingTypes;
30 | loadingText?: string;
31 | loadingSize?: string;
32 | }
33 |
34 | export type LoadingTypes = 'spinner' | 'circular' | undefined;
35 |
36 | export type ButtonTypes = 'default' | 'primary' | 'warning' | 'info' | 'danger';
37 |
--------------------------------------------------------------------------------
/src/components/Cell/index.scss:
--------------------------------------------------------------------------------
1 | @import '../../styles/colors.scss';
2 |
3 | $baseClass: 'vant-cell';
4 |
5 | * {
6 | margin: 0;
7 | padding: 0;
8 | }
9 |
10 | .#{$baseClass}__container {
11 | display: flex;
12 | flex-direction: column;
13 | justify-content: center;
14 | background-color: $default;
15 | width: 100%;
16 | padding: 12px;
17 | margin: 5px 0;
18 | cursor: pointer;
19 | text-decoration: none;
20 |
21 | .#{$baseClass}__block {
22 | display: flex;
23 | justify-content: space-between;
24 | cursor: pointer;
25 | }
26 |
27 | .#{$baseClass}__title,
28 | .#{$baseClass}__content {
29 | display: flex;
30 | align-items: center;
31 | p {
32 | margin: 0 5px;
33 | }
34 | }
35 |
36 | p {
37 | color: $grey;
38 | margin-top: 6px;
39 | }
40 |
41 | i,
42 | span {
43 | color: $black;
44 | margin-right: 6px;
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/components/Cell/index.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import Cell from '.';
4 | import Tag from '../Tag';
5 | import '../../styles/stories.scss';
6 |
7 | export default {
8 | title: 'Cell',
9 | component: Cell
10 | };
11 |
12 | export const BasicUsage = () => (
13 |
14 | |
18 | |
23 |
24 | );
25 |
26 | export const cellIcon = () => (
27 |
28 | |
34 |
35 | );
36 |
37 | export const cellTag = () => (
38 |
39 | | }
42 | content={{ text: 'Content', fontSize: '12px' }}
43 | />
44 |
45 | );
46 |
47 | export const roundCell = () => (
48 |
49 | |
50 |
51 | );
52 |
53 | export const valueOnly = () => (
54 |
55 | |
56 |
57 | );
58 |
59 | export const URL = () => (
60 |
61 | |
66 |
67 | );
68 |
69 | export const checkbox = () => (
70 |
71 | |
77 |
78 | );
79 |
80 | export const OnClick = () => (
81 |
82 | {
85 | alert(e);
86 | }}
87 | />
88 | |
89 | );
90 |
--------------------------------------------------------------------------------
/src/components/Cell/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 |
3 | import classnames from '../../utils/classNames';
4 |
5 | import './index.scss';
6 | import Icon from '../Icons';
7 | import Checkbox from '../Checkbox';
8 | import { IProps } from './types';
9 | import Radio from '../Radio';
10 |
11 | const baseClass = 'vant-cell';
12 |
13 | const Cell = ({
14 | url,
15 | onClick,
16 | title,
17 | titleIcon,
18 | content,
19 | contentIcon = url || onClick ? { name: 'arrow', size: '12px' } : null,
20 | description,
21 | checkbox,
22 | radio,
23 | tag,
24 | replace,
25 | round
26 | }: IProps) => {
27 | const [isActive, setActive] = useState(false);
28 |
29 | const CustomTag = url ? 'a' : 'div';
30 | const containerProps = {
31 | className: classnames(`${baseClass}__container`, []),
32 | style: {}
33 | };
34 | const titleProps = {
35 | className: classnames(`${baseClass}__title`, [])
36 | };
37 | const contentProps = {
38 | className: classnames(`${baseClass}__content`, [])
39 | };
40 |
41 | if (round)
42 | Object.assign(containerProps, {
43 | style: { ...containerProps.style, borderRadius: '16px' }
44 | });
45 |
46 | if (url) {
47 | Object.assign(containerProps, {
48 | href: url
49 | });
50 | if (replace) {
51 | Object.assign(containerProps, {
52 | target: '_self'
53 | });
54 | } else {
55 | Object.assign(containerProps, {
56 | target: '_blank'
57 | });
58 | }
59 | }
60 |
61 | if (onClick) {
62 | Object.assign(containerProps, {
63 | onClick
64 | });
65 | }
66 |
67 | if (checkbox) {
68 | Object.assign(containerProps, {
69 | onClick: () => {
70 | setActive(!isActive);
71 | }
72 | });
73 | }
74 |
75 | const renderCustomContent = () => {
76 | if (checkbox) {
77 | return (
78 |
83 | );
84 | } else if (radio) {
85 | return ;
86 | } else {
87 | return (
88 |
89 | {content && (
90 |
{content.text}
91 | )}
92 | {contentIcon && (
93 |
94 | )}
95 |
96 | );
97 | }
98 | };
99 |
100 | return (
101 |
102 |
103 |
104 | {titleIcon && }
105 | {title && (
106 | {title.text}
107 | )}
108 | {tag && tag}
109 |
110 | {renderCustomContent()}
111 |
112 | {description && (
113 | {description.text}
114 | )}
115 |
116 | );
117 | };
118 |
119 | export default Cell;
120 |
--------------------------------------------------------------------------------
/src/components/Cell/types.ts:
--------------------------------------------------------------------------------
1 | import { ReactElement } from 'react';
2 | import { IProps as RadioProps } from '../Radio/types';
3 | import { IProps as CheckboxProps } from '../Checkbox/index';
4 |
5 | export interface IProps {
6 | title?: { text: string; fontSize: string };
7 | titleIcon?: { name: string; size: string };
8 | content?: { text: string; fontSize: string };
9 | contentIcon?: { name: string; size: string } | null;
10 | description?: { text: string; fontSize: string };
11 | checkbox?: CheckboxProps;
12 | radio?: RadioProps;
13 | tag?: ReactElement;
14 | url?: string;
15 | replace?: boolean;
16 | round?: boolean;
17 | onClick?: Function;
18 | }
19 |
--------------------------------------------------------------------------------
/src/components/Checkbox/index.scss:
--------------------------------------------------------------------------------
1 | @import '../../styles/colors.scss';
2 | @import '../../styles/spacing.scss';
3 | @import '../../styles/typography.scss';
4 |
5 | $baseClass: 'vant-checkbox';
6 |
7 | .#{$baseClass} {
8 | display: flex;
9 | align-items: center;
10 | @include form-label;
11 |
12 | label {
13 | margin-left: $space-md;
14 | user-select: none;
15 | cursor: pointer;
16 | }
17 |
18 | &:hover {
19 | cursor: pointer;
20 | }
21 |
22 | &__icon-container {
23 | display: inline-flex;
24 | align-items: center;
25 | }
26 |
27 | &__disabled {
28 | color: $placeholder;
29 | user-select: none;
30 |
31 | &:hover {
32 | cursor: not-allowed;
33 | }
34 |
35 | label {
36 | cursor: not-allowed;
37 | }
38 |
39 | i {
40 | color: $placeholder !important;
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/components/Checkbox/index.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Checkbox from '.';
3 |
4 | import '../../styles/stories.scss';
5 |
6 | export default {
7 | title: 'Checkbox',
8 | component: Checkbox
9 | };
10 |
11 | export const BasicUsage = () => (
12 |
13 |
14 |
15 | );
16 |
17 | export const Disabled = () => (
18 |
19 |
20 |
21 | );
22 |
23 | export const CustomColor = () => (
24 |
25 |
26 |
27 | );
28 |
29 | export const LabelDisable = () => (
30 |
31 |
32 |
33 | );
34 |
35 | export const OnChange = () => (
36 |
37 | alert(`Checkbox is checked: ${checked}`)}
40 | />
41 |
42 | );
43 |
44 | export const OnClick = () => (
45 |
46 | alert('clicked')} />
47 |
48 | );
49 |
--------------------------------------------------------------------------------
/src/components/Checkbox/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 |
3 | import classnames from '../../utils/classNames';
4 |
5 | import './index.scss';
6 | import Icon from '../Icons';
7 |
8 | export interface IProps {
9 | checked?: boolean;
10 | name?: string;
11 | activeIcon?: string;
12 | inactiveIcon?: string;
13 | checkedColor?: string;
14 | labelText?: string;
15 | disabled?: boolean;
16 | labelDisabled?: boolean;
17 | onChange?: Function;
18 | onClicked?: Function;
19 | }
20 |
21 | const baseClass = 'vant-checkbox';
22 |
23 | // TODO: Round/Square checkbox
24 | // TODO: Checkbox groups
25 |
26 | const Checkbox = ({
27 | checked = false,
28 | onChange,
29 | onClicked,
30 | name,
31 | activeIcon = 'checked',
32 | checkedColor = '#1989fa',
33 | labelText,
34 | inactiveIcon = 'passed',
35 | disabled,
36 | labelDisabled
37 | }: IProps) => {
38 | const [isChecked, handleCheck] = useState(checked);
39 |
40 | const handleClick = (e) => {
41 | return onClicked && onClicked(e);
42 | };
43 |
44 | useEffect(() => {
45 | return onChange && onChange(isChecked);
46 | }, [isChecked]);
47 |
48 | const handleContainerClick = (e) => {
49 | e.preventDefault();
50 | if (!disabled && !labelDisabled) {
51 | handleCheck(!isChecked);
52 | handleClick(e);
53 | }
54 | };
55 |
56 | const handleIconClick = (e) => {
57 | e.preventDefault();
58 | if (!disabled) {
59 | handleCheck(!isChecked);
60 | handleClick(e);
61 | }
62 | };
63 |
64 | const iconName = isChecked ? activeIcon : inactiveIcon;
65 | const iconColor = disabled ? '#c8c9cc' : checkedColor;
66 |
67 | return (
68 |
76 |
77 |
78 |
79 |
80 |
81 | );
82 | };
83 |
84 | export default Checkbox;
85 |
--------------------------------------------------------------------------------
/src/components/Divider/index.scss:
--------------------------------------------------------------------------------
1 | @import '../../styles/variables.scss';
2 | $baseClass: 'vant-divider';
3 |
4 | .#{$baseClass} {
5 | display: flex;
6 | align-items: center;
7 | margin: $divider-margin;
8 | color: $divider-text-color;
9 | font-size: $divider-font-size;
10 | line-height: $divider-line-height;
11 | border-color: $divider-border-color;
12 | border-style: solid;
13 | border-width: 0;
14 |
15 | &::before,
16 | &::after {
17 | display: block;
18 | flex: 1;
19 | box-sizing: border-box;
20 | height: 1px;
21 | border-color: inherit;
22 | border-style: inherit;
23 | border-width: $border-width-base 0 0;
24 | }
25 |
26 | &::before {
27 | content: '';
28 | }
29 |
30 | &__hairline {
31 | &::before,
32 | &::after {
33 | transform: scaleY(0.5);
34 | }
35 | }
36 |
37 | &__dashed {
38 | border-style: dashed;
39 | }
40 |
41 | &__content-center,
42 | &__content-left,
43 | &__content-right {
44 | &::before {
45 | margin-right: $divider-content-padding;
46 | }
47 |
48 | &::after {
49 | margin-left: $divider-content-padding;
50 | content: '';
51 | }
52 | }
53 |
54 | &__content-left {
55 | &::before {
56 | max-width: $divider-content-left-width;
57 | }
58 | }
59 |
60 | &__content-right {
61 | &::after {
62 | max-width: $divider-content-right-width;
63 | }
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/components/Divider/index.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Divider from './index';
3 |
4 | export default {
5 | title: 'Divider',
6 | component: Divider
7 | };
8 |
9 | export const BasicWithContent = () => {
10 | return 312;
11 | };
12 | export const BasicWithOutContent = () => {
13 | return ;
14 | };
15 |
16 | export const leftContent = () => {
17 | return 312;
18 | };
19 |
20 | export const rightContent = () => {
21 | return 312;
22 | };
23 | export const dashed = () => {
24 | return 312;
25 | };
26 |
27 | export const hairline = () => {
28 | return 312;
29 | };
30 |
--------------------------------------------------------------------------------
/src/components/Divider/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC } from 'react';
2 | import classnames from '../../utils/classNames';
3 |
4 | import './index.scss';
5 |
6 | export type contentPosition = 'center' | 'left' | 'right';
7 | export interface DividerProps {
8 | dashed?: boolean;
9 | hairline?: boolean;
10 | contentPosition?: contentPosition;
11 | className?: string;
12 | }
13 | const baseClass = 'vant-divider';
14 | const Divider: FC = ({
15 | dashed = false,
16 | hairline = true,
17 | contentPosition = 'center',
18 | children,
19 | className,
20 | ...restProps
21 | }) => {
22 | if (children) {
23 | className = classnames(baseClass, [
24 | { dashed },
25 | { hairline },
26 | { type: 'content-' + contentPosition }
27 | ]);
28 | } else {
29 | className = classnames(baseClass, [{ dashed }, { hairline }]);
30 | }
31 | return (
32 |
33 | {children || ''}
34 |
35 | );
36 | };
37 | export default Divider;
38 |
--------------------------------------------------------------------------------
/src/components/Field/index.scss:
--------------------------------------------------------------------------------
1 | @import '../../styles/colors.scss';
2 | @import '../../styles/spacing.scss';
3 | @import '../../styles/variables.scss';
4 | @import '../../styles/typography.scss';
5 |
6 | $baseClass: 'vant-field';
7 |
8 | .#{$baseClass} {
9 | @include field-label;
10 | width: 100%;
11 | background-color: $default;
12 | padding: 10px $space-lg;
13 | display: flex;
14 | overflow: hidden;
15 | position: relative;
16 |
17 | .#{$baseClass}__label {
18 | display: flex;
19 | align-items: center;
20 | width: 90px;
21 |
22 | .vant-icon__container {
23 | margin-right: 5px;
24 | }
25 | }
26 |
27 | .#{$baseClass}__input {
28 | width: 100%;
29 | display: flex;
30 | flex-direction: column;
31 | overflow: visible;
32 | color: $grey;
33 | word-wrap: break-word;
34 | vertical-align: middle;
35 | position: relative;
36 |
37 | .#{$baseClass}__field {
38 | display: flex;
39 | width: inherit;
40 | }
41 |
42 | .vant-icon__container {
43 | padding: 0 5px;
44 | }
45 |
46 | button {
47 | position: absolute;
48 | right: 0;
49 | top: 50%;
50 | transform: translateY(-50%);
51 | }
52 | }
53 |
54 | .#{$baseClass}__error {
55 | color: $danger;
56 | font-size: 12px;
57 | text-align: left;
58 | }
59 |
60 | .#{$baseClass}__word-limit {
61 | margin-top: $space-sm;
62 | font-size: 12px;
63 | color: $word-limit;
64 | line-height: 16px;
65 | text-align: right;
66 | }
67 |
68 | input {
69 | @include field-label;
70 | width: inherit;
71 | outline: none;
72 | display: block;
73 | text-align: left;
74 | line-height: inherit;
75 | border: 0;
76 | resize: none;
77 | padding: 0;
78 |
79 | &::placeholder {
80 | color: $placeholder;
81 | }
82 | }
83 |
84 | &__disabled {
85 | input {
86 | color: $grey;
87 | }
88 | }
89 |
90 | &__error,
91 | &__showWordLimit {
92 | .full {
93 | color: $danger;
94 | }
95 |
96 | .#{$baseClass}__label {
97 | align-items: flex-start;
98 | }
99 | }
100 |
101 | &__input-center {
102 | input {
103 | text-align: center;
104 | }
105 | }
106 | &__input-right {
107 | input {
108 | text-align: right;
109 | }
110 | }
111 |
112 | &__label-center {
113 | .#{$baseClass}__label {
114 | justify-content: center;
115 | }
116 | }
117 | &__label-right {
118 | .#{$baseClass}__label {
119 | justify-content: flex-end;
120 | }
121 | }
122 |
123 | &__error-right {
124 | .#{$baseClass}__error {
125 | text-align: right;
126 | }
127 | }
128 | &__error-center {
129 | .#{$baseClass}__error {
130 | text-align: center;
131 | }
132 | }
133 |
134 | &__required {
135 | .#{$baseClass}__label {
136 | label {
137 | &::before {
138 | position: absolute;
139 | left: $space-md;
140 | color: $danger;
141 | font-size: 14px;
142 | content: '*';
143 | }
144 | }
145 | }
146 | }
147 |
148 | &__border {
149 | &:not(:last-child)::after {
150 | position: absolute;
151 | box-sizing: border-box;
152 | content: ' ';
153 | pointer-events: none;
154 | right: 0;
155 | bottom: 0;
156 | left: $space-lg;
157 | border-bottom: 1px solid $grey-text;
158 | transform: scaleY(0.5);
159 | }
160 | }
161 | }
162 |
--------------------------------------------------------------------------------
/src/components/Field/index.stories.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import Field from '.';
3 | import '../../styles/stories.scss';
4 | import Button from '../Button';
5 |
6 | export default {
7 | title: 'Field',
8 | component: Field
9 | };
10 |
11 | export const BasicUsage = () => (
12 |
13 |
14 |
15 | );
16 |
17 | export const RequiredField = () => (
18 |
19 |
20 |
21 | );
22 |
23 | export const CustomTypes = () => (
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | );
32 |
33 | export const Disabled = () => (
34 |
35 |
36 |
37 |
38 | );
39 |
40 | export const Colon = () => (
41 |
42 |
43 |
44 | );
45 |
46 | export const ShowIcon = () => {
47 | return (
48 |
49 |
50 |
51 |
52 |
53 | );
54 | };
55 |
56 | export const FieldEvents = () => {
57 | const [value, setValue] = useState('');
58 | const [isFocus, setFocus] = useState(false);
59 | return (
60 |
61 |
Value: {value}
62 |
setValue(e.target.value)}
68 | onClear={() => setValue('')}
69 | clearable
70 | />
71 | alert('Click event')}
77 | />
78 | setFocus(true)}
83 | onBlur={() => setFocus(false)}
84 | />
85 | alert('Input clicked')}
90 | />
91 | alert('Left Icon Clicked')}
97 | onClickRightIcon={() => alert('Right Icon Clicked')}
98 | />
99 |
100 | );
101 | };
102 |
103 | export const FieldRef = () => {
104 | const [containerRef, setContainerRef] = useState(null);
105 | const [fieldRef, setFieldRef] = useState(null);
106 | const [clickOutside, setClickOutside] = useState(false);
107 |
108 | window.addEventListener('click', (e) => {
109 | if (
110 | containerRef !== undefined &&
111 | // @ts-ignore: Object is possibly 'null'.
112 | containerRef.current &&
113 | // @ts-ignore: Object is possibly 'null'.
114 | !containerRef.current.contains(e.target)
115 | ) {
116 | setClickOutside(true);
117 | } else {
118 | setClickOutside(false);
119 | }
120 | });
121 |
122 | return (
123 |
124 |
125 | Container Ref element name:
126 | {
127 | // @ts-ignore: Object is possibly 'null'.
128 | containerRef && containerRef.current.localName
129 | }
130 |
131 |
132 | Field Ref element name:{' '}
133 | {
134 | // @ts-ignore: Object is possibly 'null'.
135 | fieldRef && fieldRef.current.localName
136 | }
137 |
138 |
setContainerRef(ref)}
143 | getFieldRef={(ref) => setFieldRef(ref)}
144 | />
145 |
146 | );
147 | };
148 |
149 | export const AutoFocus = () => {
150 | return (
151 |
152 |
153 |
154 | );
155 | };
156 |
157 | export const ErrorInfo = () => {
158 | return (
159 |
160 |
161 |
162 | );
163 | };
164 |
165 | export const MaxLengthWordLimit = () => {
166 | const [value, setValue] = useState('');
167 | return (
168 |
169 | setValue(e.target.value)}
172 | label='Max length'
173 | maxLength={5}
174 | showWordLimit
175 | />
176 |
177 | );
178 | };
179 |
180 | export const FieldWithButton = () => {
181 | return (
182 |
183 | alert('Message sent!')}
190 | text='Send SMS'
191 | type='primary'
192 | />
193 | }
194 | />
195 |
196 | );
197 | };
198 |
199 | const pattern = new RegExp(/^[a-zA-Z]*$/);
200 |
201 | export const Formatter = () => {
202 | const [value, setValue] = useState('');
203 | return (
204 |
205 | setValue(e.target.value)}
210 | formatter={(value) => pattern.test(value)}
211 | />
212 |
213 | );
214 | };
215 |
216 | export const LabelUtilities = () => (
217 |
218 |
219 |
220 | );
221 |
222 | export const LabelInputAlignment = () => (
223 |
224 |
231 |
238 |
248 |
258 |
259 | );
260 |
261 | export const AutoResize = () => (
262 |
263 |
264 |
265 | );
266 |
--------------------------------------------------------------------------------
/src/components/Field/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useRef, useEffect, useState } from 'react';
2 |
3 | import classnames from '../../utils/classNames';
4 |
5 | import './index.scss';
6 | import Icon from '../Icons';
7 | import { IProps } from './types';
8 |
9 | const ICON_SIZE = '16px';
10 |
11 | const baseClass = 'vant-field';
12 |
13 | // TODO: Resize inputs
14 |
15 | const Field = ({
16 | value,
17 | type = 'text',
18 | label,
19 | name,
20 | placeholder,
21 | readonly,
22 | disabled,
23 | colon,
24 | labelIcon,
25 | leftIcon,
26 | rightIcon,
27 | clearable,
28 | clickable,
29 | onChange,
30 | onClear,
31 | onClick,
32 | onFocus,
33 | onBlur,
34 | onClickInput,
35 | onClickLeftIcon,
36 | onClickRightIcon,
37 | getContainerRef,
38 | getFieldRef,
39 | autofocus,
40 | error,
41 | errorMessage,
42 | maxLength,
43 | showWordLimit,
44 | button,
45 | formatter = () => true,
46 | labelClass,
47 | labelWidth,
48 | labelAlign = 'left',
49 | inputAlign = 'left',
50 | errorAlign = 'left',
51 | required,
52 | border = true
53 | }: IProps) => {
54 | const [containerFocus, setContainerFocus] = useState(false);
55 |
56 | const handleChange = (e) => {
57 | const inputValue = e.target.value;
58 | if (formatter(inputValue)) {
59 | if (onChange) {
60 | if (!maxLength) {
61 | return onChange(e);
62 | } else {
63 | if (
64 | (value && value.length < maxLength) ||
65 | inputValue.length < maxLength
66 | ) {
67 | return onChange(e);
68 | }
69 | }
70 | }
71 | }
72 | };
73 |
74 | const handleClick = (e) => {
75 | if (clickable && onClick) {
76 | return onClick(e);
77 | }
78 | };
79 |
80 | const handleClickInput = (e) => {
81 | if (clickable && onClickInput) {
82 | return onClickInput(e);
83 | }
84 | };
85 |
86 | const handleFocus = (e) => {
87 | if (onFocus) return onFocus(e);
88 | };
89 |
90 | const handleBlur = (e) => {
91 | if (onBlur) return onBlur(e);
92 | };
93 |
94 | const handleClickLeftIcon = (e) => {
95 | if (onClickLeftIcon && clickable) return onClickLeftIcon(e);
96 | };
97 |
98 | const handleClickRightIcon = (e) => {
99 | if (onClickRightIcon && clickable) return onClickRightIcon(e);
100 | };
101 |
102 | const fieldContainerRef = useRef(null);
103 | const fieldRef = useRef(null);
104 |
105 | useEffect(() => {
106 | if (getContainerRef) getContainerRef(fieldContainerRef);
107 | if (getFieldRef) getFieldRef(fieldRef);
108 | }, [getContainerRef, getFieldRef]);
109 |
110 | useEffect(() => {
111 | window.addEventListener('click', (e) => {
112 | // @ts-ignore: Object is possibly 'null'.
113 | if (fieldContainerRef?.current?.contains(e.target)) {
114 | setContainerFocus(true);
115 | } else {
116 | setContainerFocus(false);
117 | }
118 | });
119 | return () => window.removeEventListener('click', () => {});
120 | }, []);
121 |
122 | const containerProps = {
123 | className: classnames(baseClass, [
124 | { disabled },
125 | { readonly },
126 | { error },
127 | { showWordLimit },
128 | { [`input-${inputAlign}`]: inputAlign },
129 | { [`label-${labelAlign}`]: labelAlign },
130 | { [`error-${errorAlign}`]: errorAlign },
131 | { border },
132 | { required }
133 | ]),
134 | onClick: handleClick,
135 | ref: fieldContainerRef
136 | };
137 |
138 | const inputProps = {
139 | value,
140 | type,
141 | name,
142 | placeholder: placeholder || label,
143 | disabled,
144 | readOnly: readonly,
145 | ref: fieldRef,
146 | autoFocus: autofocus,
147 | onChange: handleChange,
148 | onBlur: handleBlur,
149 | onFocus: handleFocus,
150 | onClick: handleClickInput
151 | };
152 |
153 | const labelProps = {
154 | htmlFor: name,
155 | className: labelClass
156 | };
157 |
158 | const labelContainerProps = {
159 | style: {},
160 | className: `${baseClass}__label ${labelClass || ''}`
161 | };
162 |
163 | if (type === 'digit')
164 | Object.assign(inputProps, { inputMode: 'numeric', type: 'tel' });
165 |
166 | if (labelWidth)
167 | Object.assign(labelContainerProps, { style: { width: labelWidth } });
168 |
169 | return (
170 |
171 | {label && (
172 |
173 | {labelIcon && }
174 |
178 |
179 | )}
180 |
181 |
182 | {leftIcon && (
183 |
189 | )}
190 |
191 | {clearable && value && containerFocus && (
192 |
193 | )}
194 | {rightIcon && !clearable && (
195 |
200 | )}
201 | {button && button}
202 |
203 | {error && errorMessage && (
204 |
{errorMessage}
205 | )}
206 | {showWordLimit && (
207 |
212 | {value ? value.length : 0}/{maxLength}
213 |
214 | )}
215 |
216 |
217 | );
218 | };
219 |
220 | export default Field;
221 |
--------------------------------------------------------------------------------
/src/components/Field/types.ts:
--------------------------------------------------------------------------------
1 | import { ReactElement } from 'react';
2 | export type TAlignment = 'center' | 'right' | 'left';
3 |
4 | export interface IProps {
5 | value?: string;
6 | type?: string;
7 | name?: string;
8 | label?: string;
9 | placeholder?: string;
10 | errorMessage?: string;
11 | labelClass?: string;
12 | labelWidth?: string;
13 | labelAlign?: TAlignment;
14 | inputAlign?: TAlignment;
15 | errorAlign?: TAlignment;
16 | maxLength?: number;
17 | showWordLimit?: boolean;
18 | disabled?: boolean;
19 | readonly?: boolean;
20 | clearable?: boolean;
21 | colon?: boolean;
22 | clickable?: boolean;
23 | autofocus?: boolean;
24 | required?: boolean;
25 | border?: boolean;
26 | error?: boolean;
27 | labelIcon?: string;
28 | leftIcon?: string;
29 | rightIcon?: string;
30 | onChange?: Function;
31 | onClear?: Function;
32 | onClick?: Function;
33 | onFocus?: Function;
34 | onBlur?: Function;
35 | onClickInput?: Function;
36 | onClickLeftIcon?: Function;
37 | onClickRightIcon?: Function;
38 | getContainerRef?: Function;
39 | getFieldRef?: Function;
40 | formatter?: Function;
41 | button?: ReactElement;
42 | }
43 |
--------------------------------------------------------------------------------
/src/components/Icons/index.scss:
--------------------------------------------------------------------------------
1 | @import '../../assets/icons/vant-icons/index.scss';
2 | @import '../../styles/colors.scss';
3 | @import '../../styles/variables.scss';
4 | @import '../../styles/global.scss';
5 |
6 | $baseContainerClass: 'vant-icon__container';
7 | $baseClass: 'vant-icon';
8 |
9 | .#{$baseContainerClass} {
10 | display: flex;
11 | justify-content: center;
12 | align-items: center;
13 | position: relative;
14 |
15 | &__dot {
16 | .#{$baseClass}--dot {
17 | position: absolute;
18 | top: 0;
19 | right: 5px;
20 | height: $icon-dot-size;
21 | width: $icon-dot-size;
22 | border-radius: 50%;
23 | background-color: $danger;
24 | font-size: 12px;
25 | z-index: 1;
26 | }
27 |
28 | .#{$baseClass}--badge {
29 | position: absolute;
30 | top: 0;
31 | right: 1;
32 | min-width: 16px;
33 | padding: 0 3px;
34 | color: $default;
35 | font-weight: 500;
36 | font-size: 12px;
37 | line-height: 14px;
38 | text-align: center;
39 | background-color: $danger;
40 | border-radius: 16px;
41 | -webkit-transform: translate(50%, -50%);
42 | transform: translate(50%, -50%);
43 | -webkit-transform-origin: 100%;
44 | transform-origin: 100%;
45 | z-index: 1;
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/components/Icons/index.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Icon from './';
3 | import IconsConfig from '../../assets/icons/vant-icons/config';
4 | import '../../styles/stories.scss';
5 |
6 | export default {
7 | title: 'Icons',
8 | component: Icon
9 | };
10 |
11 | export const AllIcons = () => (
12 |
13 |
{IconsConfig.basic.length} Basic Icons
14 |
15 | {IconsConfig.basic.map((v, i) => (
16 |
17 |
18 | {v}
19 |
20 | ))}
21 |
22 |
{IconsConfig.outline.length} Outline Icons
23 |
24 | {IconsConfig.outline.map((v, i) => (
25 |
26 |
27 | {v}
28 |
29 | ))}
30 |
31 |
{IconsConfig.filled.length} Filled Icons
32 |
33 | {IconsConfig.filled.map((v, i) => (
34 |
35 |
36 | {v}
37 |
38 | ))}
39 |
40 |
41 | );
42 |
43 | export const IconColor = () => (
44 |
45 |
46 |
47 |
48 |
49 |
50 | );
51 |
52 | export const IconSize = () => (
53 |
54 |
55 |
56 |
57 |
58 |
59 | );
60 |
61 | export const IconDotsAndBadges = () => (
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 | );
70 | export const IconTags = () => (
71 |
72 |
73 |
74 |
75 | );
76 |
77 | export const IconAction = () => (
78 |
79 | window.alert(e.target)} />
80 |
81 | );
82 |
--------------------------------------------------------------------------------
/src/components/Icons/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import './index.scss';
4 | import classnames from '../../utils/classNames';
5 |
6 | interface IProps {
7 | name: string;
8 | dot?: boolean;
9 | badge?: string;
10 | color?: string;
11 | size?: string;
12 | classPrefix?: string;
13 | tag?: 'i' | 'span';
14 | onClick?: Function;
15 | }
16 |
17 | const baseClass = 'vant-icon';
18 |
19 | export default function Icon({
20 | name,
21 | dot,
22 | badge,
23 | color,
24 | size,
25 | classPrefix = baseClass,
26 | tag,
27 | onClick
28 | }: IProps) {
29 | const CustomTag = tag || 'i' || 'span';
30 | const containerProps = {
31 | className: classnames(`${classPrefix}__container`, [
32 | {
33 | dot: dot || badge
34 | }
35 | ])
36 | };
37 | const iconProps = {
38 | className: `${classPrefix} ${classPrefix}-${name}`,
39 | style: {
40 | fontSize: '28px'
41 | }
42 | };
43 |
44 | if (color)
45 | Object.assign(iconProps, {
46 | style: {
47 | ...iconProps.style,
48 | color
49 | }
50 | });
51 |
52 | if (size) {
53 | Object.assign(iconProps, {
54 | style: {
55 | ...iconProps.style,
56 | fontSize: size
57 | }
58 | });
59 | }
60 | if (onClick) {
61 | Object.assign(iconProps, {
62 | onClick
63 | });
64 | }
65 |
66 | return (
67 |
68 | {dot && !badge && }
69 | {badge && {badge}}
70 |
71 |
72 | );
73 | }
74 |
--------------------------------------------------------------------------------
/src/components/Image/index.scss:
--------------------------------------------------------------------------------
1 | @import '../../styles/colors.scss';
2 | @import '../../styles/spacing.scss';
3 | @import '../../styles/variables.scss';
4 | @import '../../styles/typography.scss';
5 |
6 | $baseClass: 'vant-image';
7 |
8 | .#{$baseClass} {
9 | background-color: #f7f8fa;
10 | &__contain {
11 | img {
12 | object-fit: contain;
13 | }
14 | }
15 |
16 | &__cover {
17 | img {
18 | object-fit: cover;
19 | }
20 | }
21 |
22 | &__fill {
23 | img {
24 | object-fit: fill;
25 | }
26 | }
27 |
28 | &__none {
29 | img {
30 | object-fit: none;
31 | }
32 | }
33 |
34 | &__scale-down {
35 | img {
36 | object-fit: scale-down;
37 | }
38 | }
39 |
40 | &__round {
41 | img {
42 | border-radius: 50%;
43 | }
44 | }
45 |
46 | &__empty {
47 | height: 100px;
48 | width: 100px;
49 | display: flex;
50 | justify-content: center;
51 | align-items: center;
52 | color: $grey;
53 |
54 | circle {
55 | stroke: $grey;
56 | }
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/components/Image/index.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Image from '.';
3 | import '../../styles/stories.scss';
4 |
5 | export default {
6 | title: 'Image',
7 | component: Image
8 | };
9 |
10 | const dummyImage = 'https://img.yzcdn.cn/vant/cat.jpeg';
11 | const nonExistentImage = 'https://img.yzcdn.cn/vant/cat123.jpeg';
12 |
13 | export const BasicUsage = () => (
14 |
15 |
16 |
17 | );
18 |
19 | export const FillMode = () => (
20 |
21 |
22 |
23 | contain
24 |
25 |
26 |
27 | cover
28 |
29 |
30 |
31 | fill
32 |
33 |
34 |
35 | none
36 |
37 |
38 |
39 | scale-down
40 |
41 |
42 | );
43 |
44 | export const RoundImage = () => (
45 |
46 |
47 |
48 | contain
49 |
50 |
51 |
52 | cover
53 |
54 |
55 |
56 | fill
57 |
58 |
59 |
60 | none
61 |
62 |
63 |
64 | scale-down
65 |
66 |
67 | );
68 |
69 | export const Loading = () => (
70 |
71 |
72 |
73 |
74 | );
75 |
76 | export const Error = () => (
77 |
78 |
79 |
80 | );
81 |
--------------------------------------------------------------------------------
/src/components/Image/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import Icon from '../Icons';
3 | import classnames from '../../utils/classNames';
4 |
5 | import './index.scss';
6 | import CircularLoading from '../../assets/icons/loaders/Circular';
7 |
8 | export interface IProps {
9 | src?: string;
10 | fit?: 'contain' | 'cover' | 'fill' | 'none' | 'scale-down';
11 | alt?: string;
12 | width?: number | string;
13 | height?: number | string;
14 | radius?: number | string;
15 | round?: boolean;
16 | showError?: boolean;
17 | showLoading?: boolean;
18 | errorIcon?: string;
19 | loadingIcon?: string;
20 | loadingSpinner?: boolean;
21 | }
22 |
23 | // TODO: LazyLoad, need lazyLoad component
24 |
25 | const baseClass = 'vant-image';
26 |
27 | const Image = ({
28 | src,
29 | fit = 'fill',
30 | alt,
31 | width,
32 | height,
33 | radius = 0,
34 | round = false,
35 | showError = true,
36 | showLoading = true,
37 | errorIcon = 'warning-o',
38 | loadingIcon = 'photo-o',
39 | loadingSpinner = false
40 | }: IProps) => {
41 | const [isError, setError] = useState(false);
42 | const [isLoading, setLoading] = useState(true);
43 |
44 | const className = classnames(baseClass, [
45 | {
46 | contain: fit === 'contain'
47 | },
48 | {
49 | cover: fit === 'cover'
50 | },
51 | {
52 | fill: fit === 'fill'
53 | },
54 | {
55 | 'scale-down': fit === 'scale-down'
56 | },
57 | {
58 | none: fit === 'none'
59 | },
60 | {
61 | round
62 | },
63 | {
64 | empty: isError || isLoading
65 | }
66 | ]);
67 |
68 | const renderIcon = () => {
69 | if (isLoading && showLoading) {
70 | if (loadingSpinner) return ;
71 | if (!src) return ;
72 | if (isError && showError) {
73 | return ;
74 | }
75 | if (loadingIcon) return ;
76 | }
77 | if (isError && showError) return ;
78 | return null;
79 | };
80 |
81 | return (
82 |
83 | {renderIcon()}
84 |

setError(true)}
94 | onLoad={() => setLoading(false)}
95 | />
96 |
97 | );
98 | };
99 |
100 | export default Image;
101 |
--------------------------------------------------------------------------------
/src/components/Loading/index.scss:
--------------------------------------------------------------------------------
1 | @import '../../styles/colors.scss';
2 | @import '../../styles/spacing.scss';
3 | @import '../../styles/animation.scss';
4 |
5 | $baseClass: vant-loading;
6 |
7 | @mixin generate-spinner($n, $i: 1) {
8 | @if $i <= $n {
9 | i:nth-of-type(#{$i}) {
10 | transform: rotate($i * 30deg);
11 | opacity: 1 - (0.75 / 12) * ($i - 1);
12 | }
13 | @include generate-spinner($n, ($i + 1));
14 | }
15 | }
16 |
17 | .#{$baseClass} {
18 | position: relative;
19 | color: #c8c9cc;
20 | font-size: 0;
21 | vertical-align: middle;
22 |
23 | &--spinner {
24 | position: relative;
25 | display: inline-block;
26 | width: 30px;
27 | // compatible for 1.x, users may set width or height in root element
28 | max-width: 100%;
29 | height: 30px;
30 | max-height: 100%;
31 | vertical-align: middle;
32 | animation: vant-rotate 0.8s linear infinite;
33 |
34 | @include generate-spinner(12);
35 |
36 | &__spinner {
37 | animation-timing-function: steps(12);
38 |
39 | i {
40 | position: absolute;
41 | top: 0;
42 | left: 0;
43 | width: 100%;
44 | height: 100%;
45 |
46 | &::before {
47 | display: block;
48 | width: 2px;
49 | height: 25%;
50 | margin: 0 auto;
51 | background-color: currentColor;
52 | border-radius: 40%;
53 | content: ' ';
54 | }
55 | }
56 | }
57 |
58 | &__circular {
59 | animation-duration: 2s;
60 | }
61 | }
62 |
63 | &--circular {
64 | display: block;
65 | width: 100%;
66 | height: 100%;
67 |
68 | circle {
69 | animation: vant-circular 1.5s ease-in-out infinite;
70 | stroke: currentColor;
71 | stroke-width: 3;
72 | stroke-linecap: round;
73 | }
74 | }
75 |
76 | &--text {
77 | display: inline-block;
78 | margin-left: $padding-xs;
79 | color: #c8c9cc;
80 | font-size: 14px;
81 | vertical-align: middle;
82 | }
83 |
84 | &__vertical {
85 | display: flex;
86 | flex-direction: column;
87 | align-items: center;
88 |
89 | .#{$baseClass}--text {
90 | margin: $padding-xs 0 0;
91 | }
92 | }
93 | }
94 |
95 | @keyframes vant-circular {
96 | 0% {
97 | stroke-dasharray: 1, 200;
98 | stroke-dashoffset: 0;
99 | }
100 |
101 | 50% {
102 | stroke-dasharray: 90, 150;
103 | stroke-dashoffset: -40;
104 | }
105 |
106 | 100% {
107 | stroke-dasharray: 90, 150;
108 | stroke-dashoffset: -120;
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/src/components/Loading/index.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Loading from './index';
3 |
4 | import '../../styles/stories.scss';
5 |
6 | export default {
7 | title: 'Loading',
8 | component: Loading
9 | };
10 |
11 | export const BasicUsage = () => {
12 | return (
13 |
14 |
15 |
16 |
17 | );
18 | };
19 |
20 | export const LoadingText = () => {
21 | return (
22 |
23 |
24 |
25 |
26 | );
27 | };
28 |
29 | export const LoadingColor = () => {
30 | return (
31 |
32 |
33 |
34 |
40 |
41 | );
42 | };
43 |
44 | export const LoadingSize = () => {
45 | return (
46 |
47 |
48 |
49 |
50 |
51 | );
52 | };
53 |
54 | export const LoadingVertical = () => {
55 | return (
56 |
57 |
58 |
59 |
60 |
61 | );
62 | };
63 |
--------------------------------------------------------------------------------
/src/components/Loading/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC } from 'react';
2 | import { IProps } from './types';
3 | import { addUnit, classnames, getSizeStyle } from '../../utils';
4 |
5 | import './index.scss';
6 |
7 | const baseClass = 'vant-loading';
8 |
9 | const SpinIcon = () => {
10 | const arr: JSX.Element[] = [];
11 | for (let i = 0; i < 12; i++) {
12 | arr.push();
13 | }
14 | return arr;
15 | };
16 |
17 | const CircularIcon = (
18 |
24 | );
25 |
26 | const Loading: FC = ({
27 | size = '30px',
28 | type = 'circular',
29 | color = '#c9c9c9',
30 | text,
31 | textSize = '14px',
32 | textColor = '#c9c9c9',
33 | vertical
34 | }) => {
35 | console.log('loading');
36 | const contentProps = {
37 | className: classnames(`${baseClass}`, [{ vertical }]),
38 | style: {}
39 | };
40 |
41 | const iconProps = {
42 | className: classnames(`${baseClass}--spinner`, [{ [type]: type }]),
43 | style: {
44 | color,
45 | ...getSizeStyle(size)
46 | }
47 | };
48 |
49 | const textProps = {
50 | className: classnames(`${baseClass}--text`, []),
51 | style: {
52 | fontSize: addUnit(textSize),
53 | color: textColor ?? color
54 | }
55 | };
56 |
57 | return (
58 |
59 |
60 | {type === 'spinner' ? SpinIcon() : CircularIcon}
61 |
62 | {text && {text}}
63 |
64 | );
65 | };
66 |
67 | export default Loading;
68 |
--------------------------------------------------------------------------------
/src/components/Loading/types.ts:
--------------------------------------------------------------------------------
1 | export type LoadingType = 'circular' | 'spinner';
2 |
3 | export interface IProps {
4 | color?: string;
5 | type?: LoadingType;
6 | size?: number | string;
7 | text?: string;
8 | textSize?: number | string;
9 | textColor?: string;
10 | vertical?: boolean;
11 | }
12 |
--------------------------------------------------------------------------------
/src/components/Navbar/index.scss:
--------------------------------------------------------------------------------
1 | @import '../../styles/colors.scss';
2 | @import '../../styles/spacing.scss';
3 | @import '../../styles/variables.scss';
4 | @import '../../styles/typography.scss';
5 |
6 | $baseClass: 'vant-navbar';
7 |
8 | nav.#{$baseClass} {
9 | position: relative;
10 | display: flex;
11 | justify-content: space-between;
12 | align-items: center;
13 | background-color: $default;
14 | height: $navbar-height;
15 | width: 100%;
16 | border-bottom: 1px solid transparent;
17 |
18 | .#{$baseClass}__title {
19 | @include nav-title;
20 | position: absolute;
21 | left: 50%;
22 | top: 50%;
23 | transform: translate(-50%, -50%);
24 | }
25 |
26 | .#{$baseClass}__left,
27 | .#{$baseClass}__right {
28 | @include nav-link;
29 | display: flex;
30 | align-items: center;
31 | padding: 0 $space-lg;
32 | cursor: pointer;
33 |
34 | .vant-icon__container {
35 | height: auto;
36 | width: auto;
37 | .vant-icon {
38 | color: $info;
39 | }
40 | }
41 | }
42 |
43 | .#{$baseClass}__left {
44 | .vant-icon {
45 | margin-right: $space-sm;
46 | }
47 | }
48 | .#{$baseClass}__right {
49 | .vant-icon {
50 | margin-left: $space-sm;
51 | }
52 | }
53 |
54 | &__fixed {
55 | position: fixed;
56 | top: 0;
57 | }
58 |
59 | &__border {
60 | border-color: $grey-text;
61 | }
62 |
63 | .#{$baseClass}__text--left,
64 | .#{$baseClass}__text--right {
65 | font-weight: 300;
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/components/Navbar/index.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Navbar from '.';
3 | import '../../styles/stories.scss';
4 |
5 | export default {
6 | title: 'Navbar',
7 | component: Navbar
8 | };
9 |
10 | export const NavbarTitle = () => (
11 |
12 |
13 |
14 | );
15 |
16 | export const NavbarLeftAndRightText = () => (
17 |
18 |
19 |
26 |
27 |
28 | );
29 |
30 | export const NavbarFixed = () => (
31 |
32 |
39 |
40 | );
41 | export const NavbarBorder = () => (
42 |
43 |
50 |
51 | );
52 |
53 | export const NavbarWithLongTitle = () => (
54 |
55 |
61 |
62 | );
63 |
64 | export const NavbarClickHandler = () => (
65 |
66 | alert(e.target.innerHTML + ' Left Click')}
72 | onClickRight={(e) => alert(e.target.innerHTML + ' Right Click')}
73 | />
74 |
75 | );
76 |
--------------------------------------------------------------------------------
/src/components/Navbar/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactElement } from 'react';
2 |
3 | import Icon from '../Icons';
4 |
5 | import classnames from '../../utils/classNames';
6 |
7 | import './index.scss';
8 |
9 | interface Props {
10 | title?: string;
11 | leftText?: string;
12 | rightText?: string;
13 | border?: boolean;
14 | fixed?: boolean;
15 | leftIcon?: string;
16 | rightIcon?: string;
17 | onClickLeft?: Function;
18 | onClickRight?: Function;
19 | zIndex?: number;
20 | }
21 |
22 | const baseClass = 'vant-navbar';
23 |
24 | // TODO: Enable placeholder: Whether to generate a placeholder element when fixed
25 |
26 | export default function Navbar({
27 | title,
28 | leftText,
29 | rightText,
30 | leftIcon,
31 | rightIcon,
32 | border,
33 | fixed,
34 | zIndex = 1,
35 | onClickLeft = () => {},
36 | onClickRight = () => {}
37 | }: Props): ReactElement {
38 | const navProps = {
39 | style: {
40 | zIndex
41 | },
42 | className: classnames(baseClass, [{ border }, { fixed }])
43 | };
44 |
45 | const NAV_ICON_SIZE = '16px';
46 |
47 | return (
48 |
60 | );
61 | }
62 |
--------------------------------------------------------------------------------
/src/components/Popup/index.scss:
--------------------------------------------------------------------------------
1 | @import '../../styles/colors.scss';
2 | @import '../../styles/variables.scss';
3 |
4 | $baseClass: 'vant-popup';
5 | $baseContainerClass: 'vant-popup__container';
6 | $baseContentClass: 'vant-popup__content';
7 |
8 | .#{$baseContainerClass} {
9 | visibility: hidden;
10 | position: fixed;
11 | top: 0;
12 | left: 0;
13 | height: 100vh;
14 | width: 100vw;
15 | z-index: 10;
16 | transition: 0.6s;
17 |
18 | &__isActive {
19 | visibility: visible;
20 | background-color: rgba(
21 | $color: $popup-background-color,
22 | $alpha: $popup-alpha
23 | );
24 | }
25 |
26 | &__center {
27 | display: flex;
28 | justify-content: center;
29 | align-items: center;
30 | }
31 | }
32 |
33 | .#{$baseClass} {
34 | overflow: scroll;
35 | background-color: $default;
36 | display: flex;
37 | justify-content: center;
38 | align-items: center;
39 | transition: 0.5s ease-in-out;
40 | position: fixed;
41 |
42 | &__closeable {
43 | .closeIcon {
44 | position: absolute;
45 | z-index: 2;
46 |
47 | i {
48 | cursor: pointer;
49 | }
50 | }
51 | }
52 |
53 | &__isActive {
54 | transform: translateX(0px) !important;
55 | transform: translateY(0px) !important;
56 | transition: 0.5s ease-in-out;
57 | }
58 |
59 | &__center {
60 | display: none;
61 | }
62 |
63 | &__left {
64 | height: 100vh;
65 | left: 0;
66 | transform: translateX(-100%);
67 | }
68 |
69 | &__right {
70 | height: 100vh;
71 | right: 0;
72 | transform: translateX(100%);
73 | }
74 |
75 | &__top {
76 | width: 100vw;
77 | top: 0;
78 | transform: translateY(-100%);
79 | }
80 |
81 | &__bottom {
82 | width: 100vw;
83 | bottom: 0;
84 | transform: translateY(100%);
85 | }
86 | }
87 |
88 | .#{$baseClass}::-webkit-scrollbar {
89 | width: 0 !important; //chrome and Safari
90 | }
91 |
92 | .#{$baseClass} {
93 | -ms-overflow-style: none; //IE 10+
94 | overflow: -moz-scrollbars-none; //Firefox
95 | }
96 |
97 | .vant-popup__center.vant-popup__isActive {
98 | display: block;
99 | }
100 |
101 | .#{$baseContentClass} {
102 | width: -moz-fit-content;
103 | width: -webkit-fit-content;
104 | width: fit-content;
105 | height: -moz-fit-content;
106 | height: -webkit-fit-content;
107 | height: fit-content;
108 | display: none;
109 |
110 | &__isActive {
111 | display: block;
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/src/components/Popup/index.stories.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import Popup from './';
3 | import Button from '../Button';
4 | import { AllIcons } from '../Icons/index.stories';
5 | import { Types } from '../Tag/index.stories';
6 |
7 | import '../../styles/stories.scss';
8 |
9 | export default {
10 | title: 'Popup',
11 | component: Popup
12 | };
13 |
14 | export const DefaultPopup = () => {
15 | const [centerPopup, setCenterPopup] = useState(false);
16 |
17 | return (
18 |
19 |
32 | );
33 | };
34 |
35 | export const PopupTypes = () => {
36 | const [leftPopup, setLeftPopup] = useState(false);
37 | const [rightPopup, setRightPopup] = useState(false);
38 | const [topPopup, setTopPopup] = useState(false);
39 | const [bottomPopup, setBottomPopup] = useState(false);
40 |
41 | return (
42 |
43 |
96 | );
97 | };
98 |
99 | export const PopupSize = () => {
100 | const [centerPopupA, setCenterPopupA] = useState(false);
101 | const [centerPopupB, setCenterPopupB] = useState(false);
102 |
103 | return (
104 |
105 |
{
107 | setCenterPopupA(true);
108 | }}
109 | text='Button A'
110 | type='primary'
111 | />
112 |
117 | {
119 | setCenterPopupB(true);
120 | }}
121 | text='Button B'
122 | type='info'
123 | />
124 |
129 |
130 | );
131 | };
132 |
133 | export const PopupContent = () => {
134 | const [centerPopupA, setCenterPopupA] = useState(false);
135 | const [centerPopupB, setCenterPopupB] = useState(false);
136 |
137 | return (
138 |
139 |
{
141 | setCenterPopupA(true);
142 | }}
143 | text='Button A'
144 | type='primary'
145 | />
146 |
156 | {
158 | setCenterPopupB(true);
159 | }}
160 | text='Button B'
161 | type='info'
162 | />
163 | }
168 | padding='0 30px'
169 | />
170 |
171 | );
172 | };
173 |
174 | export const CloseIcon = () => {
175 | const [centerPopupA, setCenterPopupA] = useState(false);
176 | const [centerPopupB, setCenterPopupB] = useState(false);
177 | const [centerPopupC, setCenterPopupC] = useState(false);
178 |
179 | return (
180 |
181 |
{
183 | setCenterPopupA(true);
184 | }}
185 | text='Button A'
186 | type='primary'
187 | />
188 |
194 | {
196 | setCenterPopupB(true);
197 | }}
198 | text='Button B'
199 | type='info'
200 | />
201 |
208 | {
210 | setCenterPopupC(true);
211 | }}
212 | text='Button C'
213 | type='warning'
214 | />
215 |
223 |
224 | );
225 | };
226 |
227 | export const RoundPopup = () => {
228 | const [leftPopup, setLeftPopup] = useState(false);
229 | const [topPopup, setTopPopup] = useState(false);
230 |
231 | return (
232 |
233 |
{
235 | setLeftPopup(true);
236 | }}
237 | text='From Left'
238 | type='primary'
239 | />
240 | }
246 | size={{ width: '50vw', height: '100vh' }}
247 | />
248 |
249 | {
251 | setTopPopup(true);
252 | }}
253 | text='From Top'
254 | type='warning'
255 | />
256 | }
262 | size={{ width: '100vw', height: '50vh' }}
263 | />
264 |
265 | );
266 | };
267 |
268 | export const PopupColor = () => {
269 | const [centerPopupA, setCenterPopupA] = useState(false);
270 | const [centerPopupB, setCenterPopupB] = useState(false);
271 |
272 | return (
273 |
274 |
{
276 | setCenterPopupA(true);
277 | }}
278 | text='Button A'
279 | type='primary'
280 | />
281 |
287 | {
289 | setCenterPopupB(true);
290 | }}
291 | text='Button B'
292 | type='info'
293 | />
294 |
300 |
301 | );
302 | };
303 |
304 | export const PopupAction = () => {
305 | const [centerPopupA, setCenterPopupA] = useState(false);
306 |
307 | return (
308 |
309 |
{
311 | setCenterPopupA(true);
312 | }}
313 | text='Button'
314 | type='primary'
315 | />
316 | {
326 | alert(e);
327 | }}
328 | />
329 |
330 | );
331 | };
332 |
--------------------------------------------------------------------------------
/src/components/Popup/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useRef, useEffect } from 'react';
2 |
3 | import classnames from '../../utils/classNames';
4 | import Icon from '../Icons';
5 | import { IProps } from './types';
6 |
7 | import './index.scss';
8 |
9 | const baseClass = 'vant-popup';
10 |
11 | const Popup = ({
12 | closeable,
13 | text,
14 | content,
15 | borderRadius,
16 | type = 'center',
17 | color,
18 | size,
19 | padding,
20 | isActive,
21 | onSetActive,
22 | onClick,
23 | closeIcon = { name: 'cross', size: '20px' },
24 | closeIconPosition = { top: '10px', right: '10px' }
25 | }: IProps) => {
26 | const popupRef = useRef(null) || { current: {} };
27 |
28 | useEffect(() => {
29 | document.addEventListener('click', handleClickOutside, true);
30 | return () => {
31 | document.removeEventListener('click', handleClickOutside, true);
32 | };
33 | });
34 |
35 | const containerProps = {
36 | className: classnames(`${baseClass}__container`, [{ isActive }, { type }]),
37 | style: {}
38 | };
39 |
40 | const popupProps = {
41 | className: classnames(baseClass, [{ closeable }, { isActive }, { type }]),
42 | style: {}
43 | };
44 |
45 | const contentProps = {
46 | className: classnames(`${baseClass}__content`, [{ isActive }]),
47 | style: {}
48 | };
49 |
50 | const handleClickOutside = (e) => {
51 | if (popupRef.current && !(popupRef as any).current.contains(e.target)) {
52 | onSetActive(false);
53 | }
54 | };
55 |
56 | if (size)
57 | Object.assign(popupProps, {
58 | style: {
59 | ...popupProps.style,
60 | width: size.width && size.width,
61 | height: size.height && size.height
62 | }
63 | });
64 |
65 | if (size)
66 | Object.assign(contentProps, {
67 | style: {
68 | ...contentProps.style,
69 | width: 'inherit',
70 | height: 'inherit'
71 | }
72 | });
73 |
74 | if (padding)
75 | Object.assign(contentProps, {
76 | style: {
77 | ...contentProps.style,
78 | padding
79 | }
80 | });
81 |
82 | if (onClick) {
83 | Object.assign(contentProps, {
84 | onClick
85 | });
86 | }
87 |
88 | if (color)
89 | Object.assign(popupProps, {
90 | style: {
91 | ...popupProps.style,
92 | backgroundColor: color,
93 | borderColor: color
94 | }
95 | });
96 |
97 | const isInclude: Function = (data: string) => {
98 | return popupProps.className.includes(data);
99 | };
100 |
101 | if (borderRadius)
102 | Object.assign(popupProps, {
103 | style: {
104 | ...popupProps.style,
105 | borderTopLeftRadius:
106 | (isInclude('right') || isInclude('center') || isInclude('bottom')) &&
107 | borderRadius,
108 | borderTopRightRadius:
109 | (isInclude('left') || isInclude('center') || isInclude('bottom')) &&
110 | borderRadius,
111 | borderBottomLeftRadius:
112 | (isInclude('right') || isInclude('center') || isInclude('top')) &&
113 | borderRadius,
114 | borderBottomRightRadius:
115 | (isInclude('left') || isInclude('center') || isInclude('top')) &&
116 | borderRadius
117 | }
118 | });
119 |
120 | return (
121 |
122 |
123 | {closeable && (
124 |
{
127 | onSetActive(false);
128 | }}
129 | style={closeIconPosition}
130 | >
131 |
132 |
133 | )}
134 |
135 | {text && (
136 |
144 | {text.text}
145 |
146 | )}
147 | {content && content}
148 |
149 |
150 |
151 | );
152 | };
153 |
154 | export default Popup;
155 |
--------------------------------------------------------------------------------
/src/components/Popup/types.ts:
--------------------------------------------------------------------------------
1 | import { ReactElement } from 'react';
2 | import { TAlignment } from '../Field/types';
3 |
4 | export interface IProps {
5 | isActive: boolean;
6 | borderRadius?: string;
7 | size?: { width: string; height: string };
8 | text?: {
9 | text: string;
10 | color: string;
11 | fontSize: string;
12 | textAlign: TAlignment;
13 | };
14 | content?: ReactElement;
15 | type?: PopupTypes;
16 | color?: string;
17 | children?: string;
18 | padding?: string;
19 | closeable?: boolean;
20 | closeIcon?: { name: string; size: string };
21 | closeIconPosition?: object;
22 | onSetActive: Function;
23 | onClick?: Function;
24 | }
25 |
26 | export type PopupTypes = 'center' | 'top' | 'bottom' | 'left' | 'right';
27 |
--------------------------------------------------------------------------------
/src/components/Radio/index.scss:
--------------------------------------------------------------------------------
1 | @import '../../styles/colors.scss';
2 |
3 | $baseClass: 'vant-radio';
4 |
5 | .#{$baseClass} {
6 | display: flex;
7 | align-items: center;
8 | justify-content: space-between;
9 | width: 100%;
10 |
11 | label {
12 | margin-left: 5px;
13 | }
14 |
15 | input {
16 | visibility: hidden;
17 | }
18 |
19 | *:hover {
20 | cursor: pointer;
21 | }
22 |
23 | &.#{$baseClass}__disabled {
24 | &:hover,
25 | *:hover {
26 | cursor: not-allowed;
27 | }
28 | label {
29 | color: $placeholder;
30 | }
31 | }
32 |
33 | &.#{$baseClass}__rtl {
34 | flex-direction: row-reverse;
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/components/Radio/index.stories.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import Radio from '.';
3 |
4 | import '../../styles/stories.scss';
5 | import Cell from '../Cell';
6 |
7 | export default {
8 | title: 'Radio',
9 | component: Radio
10 | };
11 |
12 | const FormContainer = ({ children }) => {
13 | return {children}
;
14 | };
15 |
16 | export const BasicUsage = () => {
17 | const [checked, setChecked] = useState(false);
18 | return (
19 |
20 |
21 | setChecked(!checked)} />
22 |
23 |
24 | );
25 | };
26 |
27 | export const RadioDisabled = () => {
28 | const [checked, setChecked] = useState(false);
29 |
30 | return (
31 |
32 |
33 | setChecked(!checked)}
37 | />
38 |
39 |
40 | );
41 | };
42 |
43 | export const LabelDisabled = () => {
44 | const [checked, setChecked] = useState(false);
45 |
46 | return (
47 |
48 |
49 | setChecked(!checked)}
53 | />
54 |
55 |
56 | );
57 | };
58 |
59 | export const RadioColor = () => {
60 | const [checked, setChecked] = useState(false);
61 |
62 | return (
63 |
64 |
65 | setChecked(!checked)}
69 | />
70 |
71 |
72 | );
73 | };
74 |
75 | export const OnClick = () => {
76 | const [checked, setChecked] = useState(false);
77 |
78 | return (
79 |
80 |
81 | {
84 | setChecked(!checked);
85 | alert(checked);
86 | }}
87 | />
88 |
89 |
90 | );
91 | };
92 |
93 | export const RadioCell = () => {
94 | const [checked, setChecked] = useState(false);
95 |
96 | return (
97 | <>
98 |
99 | setChecked(!checked)
104 | }}
105 | />
106 | |
107 | >
108 | );
109 | };
110 |
111 | export const RadioCellRTL = () => {
112 | const [checked, setChecked] = useState(false);
113 |
114 | return (
115 | <>
116 |
117 | setChecked(!checked)
123 | }}
124 | />
125 | |
126 | >
127 | );
128 | };
129 |
--------------------------------------------------------------------------------
/src/components/Radio/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { MouseEvent } from 'react';
2 |
3 | import classnames from '../../utils/classNames';
4 | import Icon from '../Icons';
5 |
6 | import './index.scss';
7 | import { IProps } from './types';
8 |
9 | const baseClass = 'vant-radio';
10 |
11 | const Radio = ({
12 | name,
13 | disabled,
14 | checked,
15 | labelDisabled,
16 | checkedColor,
17 | onClick,
18 | rtl,
19 | label = 'radio button'
20 | }: IProps) => {
21 | const handleClick = (event: MouseEvent): void => {
22 | if (!labelDisabled) {
23 | onClick && onClick(event);
24 | }
25 | };
26 |
27 | const handleRadioClick = (event: MouseEvent): void => {
28 | if (labelDisabled) {
29 | onClick && onClick(event);
30 | }
31 | };
32 |
33 | const iconName = checked ? 'checked' : 'circle';
34 | const iconColor = disabled ? '#c8c9cc' : checked ? checkedColor : '#000';
35 |
36 | // TODO: Add form related inputs here when working on form element
37 | return (
38 |
46 |
50 |
51 |
52 |
53 |
54 | );
55 | };
56 |
57 | export default Radio;
58 |
--------------------------------------------------------------------------------
/src/components/Radio/types.ts:
--------------------------------------------------------------------------------
1 | export interface IProps {
2 | name?: string;
3 | disabled?: boolean;
4 | checked: boolean;
5 | labelDisabled?: boolean;
6 | rtl?: boolean;
7 | iconSize?: string;
8 | checkedColor?: string;
9 | onClick: Function;
10 | label?: string;
11 | }
12 |
--------------------------------------------------------------------------------
/src/components/Rate/index.scss:
--------------------------------------------------------------------------------
1 | @import '../../styles/colors.scss';
2 | @import '../../styles/spacing.scss';
3 | @import '../../styles/typography.scss';
4 | @import '../../styles/opacity.scss';
5 | @import '../../styles/variables.scss';
6 |
7 | $baseClass: 'vant-rate';
8 |
9 | .#{$baseClass} {
10 | display: flex;
11 | cursor: pointer;
12 |
13 | .#{$baseClass}__icon {
14 | &:last-of-type {
15 | margin-right: 0 !important;
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/components/Rate/index.stories.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import Rate from '../Rate';
3 | import '../../styles/stories.scss';
4 |
5 | export default {
6 | title: 'Rate',
7 | component: Rate
8 | };
9 |
10 | export const BasicUsage = () => (
11 |
12 |
13 |
14 | );
15 |
16 | export const CustomIcon = () => (
17 |
18 |
19 |
20 | );
21 |
22 | export const CustomColor = () => (
23 |
24 |
25 |
26 | );
27 |
28 | export const CustomCount = () => (
29 |
30 |
37 |
38 | );
39 |
40 | export const Disabled = () => (
41 |
42 |
49 |
50 | );
51 |
52 | export const ReadOnly = () => (
53 |
54 |
61 |
62 | );
63 |
64 | export const CustomGutter = () => (
65 |
66 |
73 |
74 | );
75 |
76 | export const ListenOnChange = () => {
77 | const [currentRate, setRate] = useState(4);
78 |
79 | return (
80 |
81 |
{currentRate}
82 | setRate(rate)}
84 | currentRate={currentRate}
85 | icon='like'
86 | voidIcon='like-o'
87 | color='#1989fa'
88 | />
89 |
90 | );
91 | };
92 |
--------------------------------------------------------------------------------
/src/components/Rate/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import shortid from 'shortid';
3 |
4 | import classnames from '../../utils/classNames';
5 |
6 | import './index.scss';
7 | import Icon from '../Icons';
8 | import { IProps } from './types';
9 | import RateIcon from './subcomponents/rate-icon';
10 |
11 | const baseClass = 'vant-rate';
12 |
13 | const renderIcon = (
14 | color,
15 | size,
16 | icon,
17 | numberOfIcons,
18 | handleClick,
19 | isActive,
20 | activeCount,
21 | gutter
22 | ) => {
23 | const icons = new Array(numberOfIcons);
24 | for (let i = 0; i < numberOfIcons; i++) {
25 | icons.push(
26 |
30 | handleClick(isActive ? index : index + activeCount)
31 | }
32 | key={shortid.generate()}
33 | icon={}
34 | className={`${baseClass}__icon`}
35 | />
36 | );
37 | }
38 | return icons;
39 | };
40 |
41 | const Rate = ({
42 | currentRate = 5,
43 | count = 5,
44 | size = '20px',
45 | icon = 'star',
46 | voidIcon = 'star-o',
47 | gutter = '4px',
48 | color = '#ffd21e',
49 | voidColor = '#c8c9cc',
50 | disabledColor = '#c8c9cc',
51 | allowHalf,
52 | disabled,
53 | readonly,
54 | onChange
55 | }: IProps) => {
56 | const [activeCount, setActiveCount] = useState(currentRate || count);
57 |
58 | const rateProps = {
59 | className: classnames(baseClass, [
60 | {
61 | allowHalf,
62 | disabled,
63 | readonly
64 | }
65 | ])
66 | };
67 |
68 | // TODO: Add half star feature
69 | // TODO: Add touchable feature
70 |
71 | const handleClick = (index) => {
72 | if (!disabled && !readonly) {
73 | const nextRate = index + 1;
74 | setActiveCount(nextRate);
75 | if (!!onChange) onChange(nextRate);
76 | }
77 | };
78 |
79 | return (
80 |
81 | {renderIcon(
82 | disabled ? disabledColor : color,
83 | size,
84 | icon,
85 | activeCount,
86 | handleClick,
87 | true,
88 | activeCount,
89 | gutter
90 | )}
91 | {renderIcon(
92 | disabled ? disabledColor : voidColor,
93 | size,
94 | voidIcon,
95 | count - activeCount,
96 | handleClick,
97 | false,
98 | activeCount,
99 | gutter
100 | )}
101 |
102 | );
103 | };
104 |
105 | export default Rate;
106 |
--------------------------------------------------------------------------------
/src/components/Rate/subcomponents/rate-icon.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactElement } from 'react';
2 |
3 | interface Props {
4 | icon: ReactElement;
5 | className: string;
6 | handleClick: Function;
7 | index: number;
8 | gutter: string;
9 | }
10 |
11 | const RateIcon = ({ handleClick, index, gutter, icon, className }: Props) => {
12 | return (
13 | handleClick(index)}
16 | className={className}
17 | >
18 | {icon}
19 |
20 | );
21 | };
22 |
23 | export default RateIcon;
24 |
--------------------------------------------------------------------------------
/src/components/Rate/types.ts:
--------------------------------------------------------------------------------
1 | export interface IProps {
2 | currentRate?: number;
3 | count?: number;
4 | size?: string;
5 | icon?: string;
6 | gutter?: string;
7 | voidIcon?: string;
8 | allowHalf?: boolean;
9 | disabled?: boolean;
10 | readonly?: boolean;
11 | color?: string;
12 | voidColor?: string;
13 | disabledColor?: string;
14 | touchable?: boolean;
15 | onChange?: Function;
16 | }
17 |
--------------------------------------------------------------------------------
/src/components/Search/index.scss:
--------------------------------------------------------------------------------
1 | @import '../../styles/colors.scss';
2 | @import '../../styles/spacing.scss';
3 | @import '../../styles/variables.scss';
4 | @import '../../styles/typography.scss';
5 |
6 | $baseClass: 'vant-search';
7 |
8 | .#{$baseClass} {
9 | background-color: $default;
10 | width: 100%;
11 | display: flex;
12 | padding: 10px 12px;
13 | position: relative;
14 | align-items: center;
15 |
16 | &:first-of-type(i) {
17 | position: absolute;
18 | left: 8px;
19 | }
20 |
21 | &__round {
22 | .vant-field {
23 | border-radius: 999px;
24 | }
25 | }
26 |
27 | &__showAction {
28 | padding-right: 0;
29 | }
30 |
31 | &__leftIcon {
32 | .vant-field__error {
33 | padding-left: 24px;
34 | }
35 | }
36 |
37 | .vant-field {
38 | background-color: $grey-background;
39 | padding: 0 0 0 8px;
40 |
41 | .vant-field__input {
42 | padding: 5px 8px 5px 0;
43 |
44 | .vant-icon__container {
45 | padding: 0;
46 | }
47 |
48 | input {
49 | width: 100%;
50 | background-color: $grey-background;
51 | border: none;
52 | border-radius: 2px;
53 | padding-left: $space-md;
54 | }
55 | }
56 | }
57 |
58 | &__action {
59 | padding: 0 $space-md;
60 | cursor: pointer;
61 |
62 | button {
63 | min-height: 34px;
64 | }
65 |
66 | .#{$baseClass}__cancel {
67 | @include search-action;
68 | cursor: pointer;
69 | background-color: $default;
70 | border: 0;
71 | padding: 0;
72 | }
73 | }
74 |
75 | &__disabled {
76 | input {
77 | cursor: not-allowed;
78 | }
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/src/components/Search/index.stories.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import Search from '.';
3 | import '../../styles/stories.scss';
4 | import Button from '../Button';
5 |
6 | export default {
7 | title: 'Search',
8 | component: Search
9 | };
10 |
11 | export const BasicUsage = () => (
12 |
13 |
14 |
15 |
16 | );
17 |
18 | export const CustomLabel = () => (
19 |
20 |
25 |
26 |
27 |
28 | );
29 |
30 | export const BackgroundColor = () => (
31 |
32 |
33 |
34 | );
35 |
36 | export const MaxLength = () => (
37 |
38 |
39 |
40 | );
41 |
42 | export const PlaceholderAutoFocus = () => (
43 |
44 |
45 |
46 |
47 | );
48 |
49 | export const SearchActions = () => {
50 | const handleClick = (e) => {
51 | e.preventDefault();
52 | alert('Action clicked');
53 | };
54 | const [value, setValue] = useState('');
55 | const [focus, setFocus] = useState(false);
56 | return (
57 |
58 |
Value: {value}
59 |
60 |
61 |
71 | }
72 | />
73 | alert('Searched')}
76 | />
77 |
78 | setValue(e.target.value)}
83 | onFocus={() => setFocus(true)}
84 | onBlur={() => setFocus(false)}
85 | />
86 |
87 | );
88 | };
89 |
90 | export const DisabledReadonlyError = () => (
91 |
92 |
93 |
94 |
95 |
96 | );
97 |
98 | export const AlignmentAndIcon = () => (
99 |
100 |
101 |
102 |
103 |
104 |
105 | );
106 |
--------------------------------------------------------------------------------
/src/components/Search/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 |
3 | import classnames from '../../utils/classNames';
4 |
5 | import './index.scss';
6 | import { IProps } from './types';
7 | import Field from '../Field';
8 |
9 | const baseClass = 'vant-search';
10 |
11 | const Search = ({
12 | label,
13 | shape = 'square',
14 | background,
15 | maxLength,
16 | placeholder,
17 | clearable = true,
18 | autofocus,
19 | showAction,
20 | disabled,
21 | readonly,
22 | error,
23 | inputAlign = 'left',
24 | leftIcon = 'search',
25 | rightIcon,
26 | actionText = 'Cancel',
27 | onSearch,
28 | onChange,
29 | onFocus,
30 | onBlur,
31 | onClear,
32 | onCancel,
33 | action,
34 | errorMessage,
35 | labelAlign,
36 | labelWidth
37 | }: IProps) => {
38 | const [value, setValue] = useState('');
39 |
40 | const handleSearch = (e) => {
41 | e.preventDefault();
42 | if (onSearch) onSearch(e);
43 | };
44 | const handleActionClick = (e) => {
45 | e.preventDefault();
46 | if (onCancel) onCancel(e);
47 | };
48 |
49 | const handleInput = (e) => {
50 | if (onChange) onChange(e);
51 | setValue(e.target.value);
52 | };
53 |
54 | const handleFocus = (e) => {
55 | if (onFocus) onFocus(e);
56 | };
57 |
58 | const handleBlur = (e) => {
59 | if (onBlur) onBlur(e);
60 | };
61 |
62 | const handleClear = (e) => {
63 | e.preventDefault();
64 | if (onClear) {
65 | onClear(e);
66 | }
67 | setValue('');
68 | };
69 |
70 | const searchProps = {
71 | className: classnames(baseClass, [
72 | { label },
73 | { [shape]: shape },
74 | { disabled },
75 | { showAction },
76 | { leftIcon }
77 | ]),
78 | style: {},
79 | onSubmit: handleSearch
80 | };
81 |
82 | if (background)
83 | Object.assign(searchProps, { style: { backgroundColor: background } });
84 |
85 | return (
86 |
121 | );
122 | };
123 |
124 | export default Search;
125 |
--------------------------------------------------------------------------------
/src/components/Search/types.ts:
--------------------------------------------------------------------------------
1 | import { ReactElement } from 'react';
2 | import TShape from '../../types/shapes';
3 | import { TAlignment } from '../Field/types';
4 |
5 | export interface IProps {
6 | label?: string;
7 | labelWidth?: string;
8 | labelAlign?: TAlignment;
9 | shape?: TShape;
10 | background?: string;
11 | maxLength?: number;
12 | placeholder?: string;
13 | errorMessage?: string;
14 | clearable?: boolean;
15 | autofocus?: boolean;
16 | showAction?: boolean;
17 | disabled?: boolean;
18 | readonly?: boolean;
19 | error?: boolean;
20 | inputAlign?: TAlignment;
21 | leftIcon?: string;
22 | rightIcon?: string;
23 | actionText?: string;
24 | onSearch?: Function;
25 | onChange?: Function;
26 | onFocus?: Function;
27 | onBlur?: Function;
28 | onClear?: Function;
29 | onCancel?: Function;
30 | action?: ReactElement;
31 | }
32 |
--------------------------------------------------------------------------------
/src/components/Slider/index.scss:
--------------------------------------------------------------------------------
1 | $baseClass: 'vant-slider';
2 | $baseWrapperClass: 'vant-slider__wrapper';
3 | $baseFillClass: 'vant-slider__fill';
4 | $baseSliderClass: 'vant-slider__slider';
5 |
6 | .#{$baseWrapperClass} {
7 | position: relative;
8 | display: flex;
9 | align-items: center;
10 | border-radius: 4px;
11 | cursor: pointer;
12 | -webkit-user-select: none;
13 | -moz-user-select: none;
14 | -ms-user-select: none;
15 | user-select: none;
16 |
17 | &__disabled {
18 | opacity: 0.6;
19 | cursor: not-allowed;
20 | }
21 | }
22 |
23 | .#{$baseFillClass} {
24 | display: flex;
25 | justify-content: flex-start;
26 | align-items: center;
27 | border-radius: 4px;
28 | cursor: pointer;
29 |
30 | &__disabled {
31 | cursor: not-allowed;
32 | }
33 | }
34 |
35 | .#{$baseSliderClass} {
36 | position: absolute;
37 | background-color: #fff;
38 | cursor: grab;
39 | border-radius: 50%;
40 | display: flex;
41 | justify-content: center;
42 | align-items: center;
43 |
44 | &__disabled {
45 | cursor: not-allowed;
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/components/Slider/index.stories.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import Slider from './';
3 |
4 | import '../../styles/stories.scss';
5 |
6 | export default {
7 | title: 'Slider',
8 | component: Slider
9 | };
10 |
11 | export const BasicUsage = () => {
12 | const [value, setValue] = useState(0);
13 |
14 | return (
15 |
16 |
17 |
{`Current Value : ${value}`}
18 |
19 |
20 |
21 | );
22 | };
23 |
24 | export const SlideRange = () => {
25 | const [value, setValue] = useState(0);
26 |
27 | return (
28 |
29 |
30 |
{`Current Value : ${value}`}
31 |
36 |
37 |
38 | );
39 | };
40 |
41 | export const SlideStep = () => {
42 | const [value, setValue] = useState(0);
43 |
44 | return (
45 |
46 |
47 |
{`Current Value : ${value}`}
48 |
49 |
50 |
51 | );
52 | };
53 |
54 | export const Disabled = () => {
55 | const [value, setValue] = useState(30);
56 |
57 | return (
58 |
59 |
60 |
{`Current Value : ${value}`}
61 |
62 |
63 |
64 | );
65 | };
66 |
67 | export const ShowValue = () => {
68 | const [value, setValue] = useState(30);
69 |
70 | return (
71 |
72 |
85 |
86 | );
87 | };
88 |
89 | export const CustomSize = () => {
90 | const [valueA, setValueA] = useState(80);
91 | const [valueB, setValueB] = useState(-50);
92 |
93 | return (
94 |
95 |
96 |
{`Current Value : ${valueA}`}
97 |
103 |
104 |
105 |
{`Current Value : ${valueB}`}
106 |
112 |
113 |
114 | );
115 | };
116 |
117 | export const CustomStyle = () => {
118 | const [valueA, setValueA] = useState(30);
119 | const [valueB, setValueB] = useState(-50);
120 |
121 | return (
122 |
123 |
124 |
{`Current Value : ${valueA}`}
125 |
132 |
133 |
134 |
{`Current Value : ${valueB}`}
135 |
142 |
143 |
144 | );
145 | };
146 |
--------------------------------------------------------------------------------
/src/components/Slider/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 |
3 | import classnames from '../../utils/classNames';
4 | import { IProps } from './types';
5 |
6 | import './index.scss';
7 |
8 | const baseClass = 'vant-slider';
9 |
10 | const Slider = ({
11 | range = { min: '0px', max: '100px' },
12 | size = { width: '400px', height: '5px' },
13 | sliderSize = { width: '20px', height: '20px' },
14 | sliderStyle = {
15 | color: '#000',
16 | fontSize: '10px',
17 | backgroundColor: '#fff',
18 | borderRadius: '50%',
19 | borderColor: '#000'
20 | },
21 | disabled,
22 | hasValue,
23 | activeColor = '#4169e1',
24 | inactiveColor = '#d3d3d3',
25 | id,
26 | step = 1,
27 | value = parseInt(range.min),
28 | onSetValue
29 | }: IProps) => {
30 | const { color, fontSize, backgroundColor, borderRadius, borderColor } =
31 | sliderStyle;
32 | const slideRange = parseInt(range.max) - parseInt(range.min);
33 | const sliderOffset = parseInt(sliderSize.width) / 2;
34 | const initialPosition =
35 | (Math.abs(value - parseInt(range.min)) / slideRange) *
36 | parseInt(size.width) -
37 | sliderOffset;
38 | const HIDE_ROUND = 4;
39 |
40 | const wrapperProps = {
41 | className: classnames(`${baseClass}__wrapper`, [{ disabled }]),
42 | style: {}
43 | };
44 | const fillProps = {
45 | className: classnames(`${baseClass}__fill`, [{ disabled }]),
46 | style: {}
47 | };
48 | const sliderProps = {
49 | className: classnames(`${baseClass}__slider`, [{ disabled }]),
50 | style: {}
51 | };
52 |
53 | const handleStep = (value) => {
54 | return (
55 | Math.round(
56 | Math.max(parseInt(range.min), Math.min(value, parseInt(range.max))) /
57 | step
58 | ) * step
59 | );
60 | };
61 |
62 | useEffect(() => {
63 | !disabled && handleSlide();
64 | }, [value]);
65 |
66 | const handleSlide = () => {
67 | const wrapper = document.getElementById(`wrapper${id}`);
68 | const fill = document.getElementById(`fill${id}`);
69 | const slider = document.getElementById(`slider${id}`);
70 | move(wrapper, slider, fill);
71 | };
72 |
73 | const getValue = (e, dom1) => {
74 | const result = Math.round(
75 | ((e.pageX - dom1.offsetLeft) / parseInt(size.width)) * slideRange +
76 | parseInt(range.min)
77 | );
78 | return handleStep(result);
79 | };
80 |
81 | const move = (dom1, dom2, dom3) => {
82 | const CLICKABLE = 1;
83 | const NON_CLICK = 0;
84 | let drag = NON_CLICK;
85 | dom1.addEventListener('click', function (e) {
86 | if (e.target === dom2) {
87 | } else {
88 | if (e.offsetX > parseInt(size.width)) {
89 | dom2.style.left = size.width;
90 | dom3.style.width = size.width;
91 | } else if (e.offsetX < sliderOffset) {
92 | dom2.style.left = '0px';
93 | dom3.style.width = '0px';
94 | } else {
95 | dom2.style.left = `${e.offsetX - sliderOffset}px`;
96 | dom3.style.width = `${e.offsetX - sliderOffset + HIDE_ROUND}px`;
97 | }
98 | }
99 | onSetValue(getValue(e, dom1));
100 | });
101 | dom2.addEventListener('mousedown', function () {
102 | drag = CLICKABLE;
103 | });
104 | document.addEventListener('mouseup', function (e) {
105 | drag = NON_CLICK;
106 | if (e.target === dom1 && e.target === dom3) {
107 | onSetValue(getValue(e, dom1));
108 | }
109 | });
110 | document.addEventListener('mousemove', function (e) {
111 | if (e.offsetX && drag === CLICKABLE) {
112 | if (e.pageX > dom1.offsetLeft + parseInt(size.width)) {
113 | dom2.style.left = `${parseInt(size.width) - sliderOffset}px`;
114 | dom3.style.width = `${
115 | parseInt(size.width) - sliderOffset + HIDE_ROUND
116 | }px`;
117 | onSetValue(handleStep(parseInt(range.max)));
118 | } else if (e.pageX < dom1.offsetLeft) {
119 | dom2.style.left = `${0 - sliderOffset}px`;
120 | dom3.style.width = '0px';
121 | onSetValue(handleStep(parseInt(range.min)));
122 | } else {
123 | dom2.style.left = `${e.pageX - dom1.offsetLeft - sliderOffset}px`;
124 | dom3.style.width = `${
125 | e.pageX - dom1.offsetLeft - sliderOffset + HIDE_ROUND
126 | }px`;
127 | onSetValue(getValue(e, dom1));
128 | }
129 | }
130 | });
131 | };
132 |
133 | if (disabled)
134 | Object.assign(wrapperProps, {
135 | disabled
136 | });
137 |
138 | return (
139 |
148 |
157 |
171 | {hasValue && (
172 |
178 | {value}
179 |
180 | )}
181 |
182 |
183 |
184 | );
185 | };
186 |
187 | export default Slider;
188 |
--------------------------------------------------------------------------------
/src/components/Slider/types.ts:
--------------------------------------------------------------------------------
1 | export interface IProps {
2 | disabled?: boolean;
3 | hasValue?: boolean;
4 | activeColor?: string;
5 | inactiveColor?: string;
6 | size?: { width: string; height: string };
7 | sliderSize?: { width: string; height: string };
8 | sliderStyle?: {
9 | color?: string;
10 | fontSize?: string;
11 | backgroundColor?: string;
12 | borderRadius?: string;
13 | borderColor?: string;
14 | };
15 | range?: { min: string; max: string };
16 | id?: string;
17 | step?: number;
18 | value: number;
19 | onSetValue: Function;
20 | }
21 |
--------------------------------------------------------------------------------
/src/components/Stepper/index.scss:
--------------------------------------------------------------------------------
1 | @import '../../styles/colors.scss';
2 | @import '../../styles/spacing.scss';
3 | @import '../../styles/typography.scss';
4 | @import '../../styles/opacity.scss';
5 | @import '../../styles/variables.scss';
6 | @keyframes donut-spin {
7 | 0% {
8 | transform: rotate(0deg);
9 | }
10 |
11 | 100% {
12 | transform: rotate(360deg);
13 | }
14 | }
15 | .stepper {
16 | display: flex;
17 | position: absolute;
18 | left: 50%;
19 | top: 50%;
20 | transform: translate(-50%, -50%);
21 | }
22 |
23 | $baseClass: 'vant-stepper';
24 | .vant-stepper-container {
25 | display: flex;
26 | justify-content: center;
27 | align-items: center;
28 | margin: 0px;
29 | padding: 0px;
30 |
31 | input {
32 | margin: 0px 2px;
33 | border-radius: 5px;
34 | font-size: 10px;
35 | border: none;
36 | display: flex;
37 | justify-content: center;
38 | align-items: center;
39 | text-align: center;
40 | background-color: rgba(128, 128, 128, 0.137);
41 | }
42 | button {
43 | border-radius: 5px;
44 | font-size: 20px;
45 | outline: none;
46 | background-color: rgba(128, 128, 128, 0.137);
47 | border: none;
48 | font-weight: 100;
49 | text-align: center;
50 | display: flex;
51 | justify-content: center;
52 | align-items: center;
53 | padding: 0px;
54 | margin: 0;
55 | position: relative;
56 | }
57 | button:first-child {
58 | ::before {
59 | content: '';
60 | position: absolute;
61 | border-top: 1px solid black;
62 |
63 | width: 10px;
64 | left: 50%;
65 | top: 50%;
66 | transform: translate(-50%, -50%);
67 | }
68 | }
69 | button:last-child {
70 | ::before {
71 | content: '';
72 | position: absolute;
73 | border-top: 1px solid black;
74 | width: 10px;
75 | left: 50%;
76 | top: 50%;
77 | transform: translate(-50%, -50%);
78 | }
79 | ::after {
80 | content: '';
81 | position: absolute;
82 | left: 50%;
83 | top: 50%;
84 | transform: translate(-50%, -50%);
85 | height: 10px;
86 | border-left: 1px solid black;
87 | }
88 | }
89 | }
90 |
91 | button.#{$baseClass} {
92 | width: 28px;
93 | height: 28px;
94 | text-align: center;
95 | display: flex;
96 | justify-content: center;
97 | align-items: center;
98 |
99 | &__disabled {
100 | cursor: not-allowed;
101 | opacity: $button-disabled-opacity;
102 | }
103 | &__theme {
104 | border-radius: 50%;
105 | outline: none;
106 | width: 25px;
107 | height: 25px;
108 | background-color: red;
109 | border-color: white;
110 | .minus::before {
111 | content: '';
112 | position: absolute;
113 | border-top: 1px solid white;
114 |
115 | width: 10px;
116 | left: 50%;
117 | top: 50%;
118 | transform: translate(-50%, -50%);
119 | }
120 | .add::before {
121 | content: '';
122 | position: absolute;
123 | border-top: 1px solid white;
124 | width: 10px;
125 | left: 50%;
126 | top: 50%;
127 | transform: translate(-50%, -50%);
128 | }
129 | .add::after {
130 | content: '';
131 | position: absolute;
132 | left: 50%;
133 | top: 50%;
134 | transform: translate(-50%, -50%);
135 | height: 10px;
136 | border-left: 1px solid white;
137 | }
138 | }
139 | }
140 | input.#{$baseClass} {
141 | width: 32px;
142 | height: 28px;
143 |
144 | &__disableInput {
145 | cursor: not-allowed;
146 | opacity: $button-disabled-opacity;
147 | }
148 | &__theme {
149 | border-radius: 50%;
150 | width: 25px;
151 | height: 25px;
152 | background-color: white;
153 | }
154 | }
155 | .load-background {
156 | display: inline-block;
157 | border: 4px solid rgba(0, 0, 0, 0.1);
158 | border-left-color: white;
159 | border-radius: 50%;
160 | width: 28px;
161 | height: 28px;
162 | animation: donut-spin 1.2s linear infinite;
163 | position: relative;
164 | opacity: 0;
165 | }
166 | .load {
167 | width: 85px;
168 | height: 85px;
169 | position: absolute;
170 | top: 30%;
171 | background-color: rgba(0, 0, 0, 0.836);
172 | display: flex;
173 | justify-content: center;
174 | align-items: center;
175 | border-radius: 10px;
176 | opacity: 0;
177 | pointer-events: none;
178 | }
179 | .load {
180 | position: absolute;
181 | bottom: 150px;
182 | }
183 |
--------------------------------------------------------------------------------
/src/components/Stepper/index.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Stepper from '.';
3 | import '../../styles/stories.scss';
4 |
5 | export default {
6 | title: 'Stepper',
7 | component: Stepper
8 | };
9 |
10 | export const BasicStepper = () => (
11 |
12 | console.log(value)} />
13 |
14 | );
15 |
16 | export const DisableStepper = () => (
17 |
18 | console.log(value)} />
19 |
20 | );
21 |
22 | export const StepStepper = () => (
23 |
24 | console.log(value)} />
25 |
26 | );
27 |
28 | export const RangeStepper = () => (
29 |
30 | console.log(value)} />
31 |
32 | );
33 |
34 | export const SizeStepper = () => (
35 |
36 | console.log(value)} />
37 |
38 | );
39 |
40 | export const RoundStepper = () => (
41 |
42 | console.log(value)} />
43 |
44 | );
45 |
46 | export const DisableInputStepper = () => (
47 |
48 | console.log(value)} />
49 |
50 | );
51 |
52 | export const AsyncStepper = () => (
53 |
54 | {}}
57 | onChange={(value) => console.log(value)}
58 | />
59 |
60 | );
61 |
--------------------------------------------------------------------------------
/src/components/Stepper/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, ReactElement, createRef } from 'react';
2 |
3 | import classnames from '../../utils/classNames';
4 |
5 | import './index.scss';
6 |
7 | const baseClass = 'vant-stepper';
8 |
9 | export interface IProps {
10 | value?: number;
11 | theme?: String | any;
12 | disabled?: Boolean;
13 | disableInput?: Boolean;
14 | min?: number | any;
15 | max?: number | any;
16 | step?: number | any;
17 | longPress?: Boolean;
18 | plus?: Boolean;
19 | minus?: Boolean;
20 | size?: number;
21 | loading?: Boolean;
22 | tag?: ReactElement;
23 | onChange: Function;
24 | onAsyncChange?: Function | any;
25 | }
26 |
27 | export default function Stepper({
28 | disabled,
29 | step,
30 | min,
31 | max,
32 | disableInput,
33 | size,
34 | theme,
35 | loading,
36 | onChange,
37 | onAsyncChange
38 | }: IProps) {
39 | const [value, setValue] = useState(0);
40 | const [isMinus, setIsMinus] = useState(false);
41 | const [isPlus, setIsPlus] = useState(false);
42 | const [isInput, setIsInput] = useState(false);
43 |
44 | const [minusBt, setMinusBt] = useState({});
45 | const [plusBt, setPlusBt] = useState({});
46 | const [inputBt, setInputBt] = useState({});
47 | const animationDiv = createRef();
48 | const animationBackgroundDiv = createRef();
49 |
50 | const handleIncrementBtProps = {
51 | className: classnames(baseClass, [{ disabled }, { theme }]),
52 | style: {}
53 | };
54 | const handleDecrementProps = {
55 | className: classnames(baseClass, [{ disabled }, { theme }]),
56 | style: {}
57 | };
58 | const inputProps = {
59 | className: classnames(baseClass, [{ disableInput }, { theme }]),
60 | style: {}
61 | };
62 |
63 | const handleIncrement = () => {
64 | if (loading) {
65 | const aniNode = animationDiv.current;
66 | const aniBgNode = animationBackgroundDiv.current;
67 | if (aniNode && aniBgNode) {
68 | aniNode.style.opacity = '1';
69 | aniBgNode.style.opacity = '1';
70 | }
71 |
72 | const handlePlus = () => {
73 | const nextValue = value + (step || 1);
74 | setValue(nextValue);
75 |
76 | if (aniNode && aniBgNode) {
77 | aniNode.style.opacity = '0';
78 | aniBgNode.style.opacity = '0';
79 | }
80 | onChange(nextValue);
81 | onAsyncChange();
82 | };
83 | setTimeout(handlePlus, 1000);
84 | } else {
85 | const nextValue = value + (step || 1);
86 | setValue(nextValue);
87 | onChange(nextValue);
88 | }
89 | };
90 |
91 | const handleDecrement = () => {
92 | setIsPlus(false);
93 | if (loading) {
94 | const aniNode = animationDiv.current;
95 | const aniBgNode = animationBackgroundDiv.current;
96 | if (aniNode && aniBgNode) {
97 | aniNode.style.opacity = '1';
98 | aniBgNode.style.opacity = '1';
99 | }
100 |
101 | const decrement = () => {
102 | const nextValue = value - (step || 1);
103 | setValue(nextValue);
104 | onChange(nextValue);
105 | if (aniNode && aniBgNode) {
106 | aniNode.style.opacity = '0';
107 | aniBgNode.style.opacity = '0';
108 | }
109 | };
110 | setTimeout(decrement, 1000);
111 | } else {
112 | const nextValue = value - (step || 1);
113 | if (nextValue >= 0) {
114 | setValue(nextValue);
115 | onChange(nextValue);
116 | }
117 | }
118 | };
119 |
120 | const handleInputChange = (e) => {
121 | const result = e.target.value;
122 | if (loading) {
123 | const aniNode = animationDiv.current;
124 | const aniBgNode = animationBackgroundDiv.current;
125 | if (aniNode && aniBgNode) {
126 | aniNode.style.opacity = '1';
127 | aniBgNode.style.opacity = '1';
128 | }
129 | const changeInput = () => {
130 | setValue(Number(result));
131 | onChange(Number(result));
132 | if (aniNode && aniBgNode) {
133 | aniNode.style.opacity = '0';
134 | aniBgNode.style.opacity = '0';
135 | }
136 | };
137 | setTimeout(changeInput, 2000);
138 | } else {
139 | setValue(Number(e.target.value));
140 | onChange(Number(e.target.value));
141 | }
142 | };
143 |
144 | useEffect(() => {
145 | if (disabled) {
146 | const btStyle = {
147 | cursor: 'not-allowed',
148 | opacity: '0.2'
149 | };
150 | setMinusBt(btStyle);
151 | setPlusBt(btStyle);
152 | setIsPlus(true);
153 | setIsMinus(true);
154 | Object.assign(handleDecrementProps, { disabled });
155 | Object.assign(handleIncrementBtProps, { disabled });
156 | } else if (size) {
157 | const Size = `${size}px`;
158 | setMinusBt({ width: Size, height: Size });
159 | setInputBt({ width: Size, height: Size });
160 | setPlusBt({ cursor: 'pointer' });
161 |
162 | if (value === 0 || value === min) {
163 | const btStyle = {
164 | cursor: 'not-allowed',
165 | width: Size,
166 | height: Size,
167 | opacity: '0.2'
168 | };
169 | const btnStyle = {
170 | cursor: 'pointer',
171 | width: Size,
172 | height: Size
173 | };
174 | setMinusBt(btStyle);
175 | setPlusBt(btnStyle);
176 | } else if (value === max) {
177 | const btStyle = {
178 | cursor: 'not-allowed',
179 | width: Size,
180 | height: Size,
181 | opacity: '0.2'
182 | };
183 | setPlusBt(btStyle);
184 | setIsPlus(true);
185 | } else {
186 | const btnStyle = {
187 | cursor: 'pointer',
188 | width: Size,
189 | height: Size
190 | };
191 | setMinusBt(btnStyle);
192 | setInputBt(btnStyle);
193 | setPlusBt(btnStyle);
194 | }
195 | } else {
196 | if (value === 0 || value === min) {
197 | const btStyle = { cursor: 'not-allowed', opacity: '0.2' };
198 | const btnStyle = { cursor: 'pointer' };
199 | setMinusBt(btStyle);
200 | setPlusBt(btnStyle);
201 | } else if (value === max) {
202 | const btStyle = { cursor: 'not-allowed', opacity: '0.2' };
203 | setPlusBt(btStyle);
204 | setIsPlus(true);
205 | } else {
206 | const btnStyle = {
207 | cursor: 'pointer'
208 | };
209 | setMinusBt(btnStyle);
210 | setPlusBt(btnStyle);
211 | }
212 | }
213 | }, [value]);
214 |
215 | useEffect(() => {
216 | if (disableInput) {
217 | setIsInput(true);
218 | Object.assign(inputProps, { disabled });
219 | }
220 | }, [disableInput]);
221 | return (
222 |
223 |
230 |
231 |
232 |
239 | {loading && (
240 |
243 | )}
244 |
250 |
251 |
252 |
253 | );
254 | }
255 |
--------------------------------------------------------------------------------
/src/components/Switch/index.scss:
--------------------------------------------------------------------------------
1 | @import '../../styles/colors.scss';
2 |
3 | @import '../../styles/variables.scss';
4 |
5 | $baseClass: 'vant-switch';
6 |
7 | .#{$baseClass} {
8 | width: 2em;
9 | height: 1em;
10 | border-radius: 1em;
11 | cursor: pointer;
12 | position: relative;
13 | box-sizing: content-box;
14 | display: inline-block;
15 | transition: background-color 0.3s cubic-bezier(0.3, 1.05, 0.4, 1.05);
16 | border: 1px solid rgba(0, 0, 0, 0.1);
17 |
18 | // 开关节点 switch node
19 | &__node {
20 | position: absolute;
21 | width: 1em;
22 | height: 1em;
23 | top: 0;
24 | border-radius: 100%;
25 | background: white;
26 | box-shadow: 0 3px 1px 0 #00000005, 0 2px 2px 0 #00000010, 0 3px 3px 0 #00000005;
27 | transition: transform 0.3s cubic-bezier(0.3, 1.05, 0.4, 1.05);
28 | .circular-loading {
29 | position: relative;
30 | font-size: 0;
31 | vertical-align: middle;
32 | top: 25%;
33 | left: 25%;
34 | width: 50%!important;
35 | height: 50%!important;
36 | line-height: 1;
37 | circle {
38 | stroke: $info
39 | }
40 | }
41 | }
42 |
43 | //开关loading
44 | &__loading {
45 | cursor: not-allowed;
46 | }
47 |
48 | // 开关禁用 switch disable
49 | &__disabled {
50 | cursor: not-allowed;
51 | opacity: .6;
52 | }
53 |
54 | // 开关开启 Switch on
55 | &__checked {
56 | .#{$baseClass}__node {
57 | transform: translateX(1em);
58 | }
59 | }
60 |
61 | }
62 |
63 |
64 |
65 |
--------------------------------------------------------------------------------
/src/components/Switch/index.stories.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import Switch from '.';
3 |
4 | import '../../styles/stories.scss';
5 |
6 | export default {
7 | title: 'Switch',
8 | component: Switch
9 | };
10 |
11 | export const BasicUsage = () => {
12 | const [checked, setChecked] = useState(false);
13 | const handleChange = (value) => {
14 | console.log(value);
15 | setChecked(!checked);
16 | };
17 |
18 | const handleClick = (e) => {
19 | console.log(e);
20 | };
21 |
22 | return (
23 |
24 |
25 |
26 | );
27 | };
28 |
29 | export const DisabledUsage = () => {
30 | return (
31 |
32 |
33 |
34 | );
35 | };
36 |
37 | export const LoadingUsage = () => {
38 | return (
39 |
40 |
41 |
42 |
43 | );
44 | };
45 |
46 | export const SizeUsage = () => {
47 | const [checked, setChecked] = useState(false);
48 | const handleChange = (value) => {
49 | console.log(value);
50 | setChecked(!checked);
51 | };
52 | return (
53 |
54 |
55 |
56 |
57 |
58 | );
59 | };
60 |
61 | export const ColorUsage = () => {
62 | const [checked, setChecked] = useState(false);
63 | const handleChange = (value) => {
64 | console.log(value);
65 | setChecked(!checked);
66 | };
67 | return (
68 |
69 |
77 |
83 |
89 |
90 | );
91 | };
92 |
--------------------------------------------------------------------------------
/src/components/Switch/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import classnames from '../../utils/classNames';
3 | import './index.scss';
4 | import { IProps } from './types';
5 | import CircularLoading from '../../assets/icons/loaders/Circular';
6 | const baseClass = 'vant-switch';
7 |
8 | const Switch = ({
9 | checked = false,
10 | disabled = false,
11 | size = '30px',
12 | activeColor = '#1989fa',
13 | inactiveColor = 'gray',
14 | activeValue = true,
15 | inactiveValue = false,
16 | loading,
17 | onClick,
18 | onChange
19 | }: IProps) => {
20 | const handleClick = (e) => {
21 | if (!disabled && !loading) {
22 | const value = !checked ? activeValue : inactiveValue;
23 | onChange && onChange(value);
24 | onClick && onClick(e);
25 | }
26 | };
27 |
28 | const containerProps = {
29 | onClick: (e) => handleClick(e),
30 | className: classnames(baseClass, [{ checked }, { disabled }, { loading }]),
31 | style: {
32 | fontSize: typeof size === 'number' ? size + 'px' : size,
33 | backgroundColor: checked ? activeColor : inactiveColor
34 | }
35 | };
36 |
37 | const renderLoading = () => {
38 | if (loading) {
39 | const color = checked ? activeColor : inactiveColor;
40 | return (
41 |
42 | );
43 | }
44 | return '';
45 | };
46 |
47 | return (
48 |
49 |
{renderLoading()}
50 |
51 | );
52 | };
53 |
54 | export default Switch;
55 |
--------------------------------------------------------------------------------
/src/components/Switch/types.ts:
--------------------------------------------------------------------------------
1 | export interface IProps {
2 | checked?: boolean;
3 | disabled?: boolean;
4 | size?: number | string;
5 | activeColor?: string;
6 | inactiveColor?: string;
7 | activeValue?: any;
8 | inactiveValue?: any;
9 | onClick?: Function;
10 | onChange?: Function;
11 | loading?: boolean;
12 | }
13 |
14 |
--------------------------------------------------------------------------------
/src/components/Tag/index.scss:
--------------------------------------------------------------------------------
1 | @import '../../styles/colors.scss';
2 |
3 | $baseClass: 'vant-tag';
4 |
5 | .#{$baseClass} {
6 | display: inline-flex;
7 | justify-content: center;
8 | align-items: center;
9 | padding: 0.2em 0.5em;
10 | font-size: 10px;
11 | color: $default;
12 | line-height: normal;
13 | border-radius: 0.2em;
14 | border: 1px solid $grey;
15 | background-color: $grey;
16 |
17 | &__primary {
18 | background-color: $primary;
19 | border-color: $primary;
20 | }
21 | &__info {
22 | background-color: $info;
23 | border-color: $info;
24 | }
25 | &__warning {
26 | background-color: $warning;
27 | border-color: $warning;
28 | }
29 | &__danger {
30 | background-color: $danger;
31 | border-color: $danger;
32 | }
33 |
34 | &__medium {
35 | font-size: 12px;
36 | }
37 |
38 | &__large {
39 | font-size: 14px;
40 | }
41 |
42 | &__plain {
43 | background-color: transparent;
44 | color: $grey;
45 |
46 | &.#{$baseClass}__primary {
47 | color: $primary;
48 | }
49 | &.#{$baseClass}__info {
50 | color: $info;
51 | }
52 | &.#{$baseClass}__warning {
53 | color: $warning;
54 | }
55 | &.#{$baseClass}__danger {
56 | color: $danger;
57 | }
58 | }
59 |
60 | &__round {
61 | border-radius: 999px;
62 | }
63 |
64 | &__mark {
65 | border-top-right-radius: 999px;
66 | border-bottom-right-radius: 999px;
67 | }
68 |
69 | &__closeable {
70 | span {
71 | margin-right: 0;
72 |
73 | .vant-icon {
74 | margin-left: 5px;
75 | cursor: pointer;
76 | }
77 | }
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/src/components/Tag/index.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Tag from './';
3 |
4 | import '../../styles/stories.scss';
5 |
6 | export default {
7 | title: 'Tag',
8 | component: Tag
9 | };
10 |
11 | export const Types = () => (
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | );
20 |
21 | export const Plain = () => (
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | );
30 |
31 | export const Sizes = () => (
32 |
33 |
34 |
35 |
36 |
37 | );
38 |
39 | export const CustomColors = () => (
40 |
41 |
42 |
43 |
44 |
45 | );
46 |
47 | export const Round = () => (
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 | );
56 |
57 | export const Mark = () => (
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 | );
66 |
67 | export const Closeable = () => (
68 |
69 |
70 |
71 |
72 | );
73 |
--------------------------------------------------------------------------------
/src/components/Tag/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useRef } from 'react';
2 |
3 | import Icon from '../Icons';
4 |
5 | import classnames from '../../utils/classNames';
6 |
7 | import './index.scss';
8 | import { getContrastTextColor } from '../Button/helper';
9 |
10 | export interface IProps {
11 | type?: 'default' | 'primary' | 'info' | 'danger' | 'warning';
12 | text: string;
13 | size?: 'small' | 'medium' | 'large';
14 | color?: string;
15 | children?: string;
16 | plain?: boolean;
17 | mark?: boolean;
18 | round?: boolean;
19 | closeable?: boolean;
20 | }
21 |
22 | const baseClass = 'vant-tag';
23 |
24 | // TODO: Fix closeable error
25 | // TODO: Fix tag padding when closeable is true
26 |
27 | const Tag = ({
28 | type,
29 | closeable,
30 | text,
31 | children,
32 | size = 'small',
33 | color,
34 | plain,
35 | round,
36 | mark
37 | }: IProps) => {
38 | const tagRef = useRef(null) || { current: {} };
39 | const contrastingColor = color ? getContrastTextColor(color) : 'ffffff';
40 | const props = {
41 | className: classnames(baseClass, [
42 | { type },
43 | { plain },
44 | { round },
45 | { mark },
46 | { closeable },
47 | {
48 | [size]: size
49 | }
50 | ]),
51 | style: {}
52 | };
53 |
54 | if (color)
55 | Object.assign(props, {
56 | style: {
57 | ...props.style,
58 | color: contrastingColor,
59 | backgroundColor: `#${color}`,
60 | borderColor: `#${color}`
61 | }
62 | });
63 |
64 | return (
65 |
66 | {children || text}
67 | {closeable && (
68 | {
70 | if (tagRef !== null) {
71 | const current = tagRef.current;
72 | if (current) {
73 | const style = (current as any).style;
74 | style.display = 'none';
75 | }
76 | }
77 | }}
78 | >
79 |
80 |
81 | )}
82 |
83 | );
84 | };
85 |
86 | export default Tag;
87 |
--------------------------------------------------------------------------------
/src/components/Toast/CreateToast.tsx:
--------------------------------------------------------------------------------
1 | /*
2 | * @Author: zhaohui
3 | * @Date: 2021-05-17 14:50:33
4 | * @LastEditTime: 2021-05-26 11:21:13
5 | * @LastEditors: zhaohui
6 | * @Description:
7 | * @FilePath: /vant-react/src/components/Toast/CreateToast.tsx
8 | */
9 | import ReactDom from 'react-dom';
10 | import React from 'react';
11 | import ToastContainer from './ToastContainer';
12 | import { ToastItemProps, ToastProps, LoadingOption } from './types';
13 |
14 | const createToast = () => {
15 | const div = document.createElement('div');
16 | document.body.appendChild(div);
17 | const toast = ReactDom.render(, div);
18 | let defaultProps: ToastProps = {
19 | type: 'message',
20 | position: 'top',
21 | duration: 2000
22 | };
23 | return {
24 | info: (info: ToastItemProps) => {
25 | toast.pushToastItem(Object.assign({}, defaultProps, info));
26 | },
27 | desdroy: () => {
28 | ReactDom.unmountComponentAtNode(div);
29 | },
30 | setDefaultOptions(info: ToastProps) {
31 | defaultProps = Object.assign({}, defaultProps, info);
32 | },
33 | Loading: (option: LoadingOption | string) => {
34 | if (typeof option === 'string') {
35 | return toast.pushToastItem(
36 | Object.assign({}, defaultProps, {
37 | message: option,
38 | type: 'loading',
39 | loadingType: 'circular'
40 | })
41 | );
42 | } else {
43 | return toast.pushToastItem(
44 | Object.assign({}, defaultProps, {
45 | type: 'loading',
46 | message: option.message,
47 | duration: option.duration,
48 | loadingType: option.type
49 | })
50 | );
51 | }
52 | }
53 | };
54 | };
55 | export default createToast();
56 |
--------------------------------------------------------------------------------
/src/components/Toast/Toast.tsx:
--------------------------------------------------------------------------------
1 | /*
2 | * @Author: zhaohui
3 | * @Date: 2021-05-14 09:30:56
4 | * @LastEditTime: 2021-05-26 11:30:38
5 | * @LastEditors: zhaohui
6 | * @Description:
7 | * @FilePath: /vant-react/src/components/Toast/Toast.tsx
8 | */
9 | import React from 'react';
10 | import classnames from '../../utils/classNames';
11 | import { baseClass, ToastProps } from './types';
12 | import Icon from '../Icons';
13 | import { renderLoadingIcon } from '../Button/helper';
14 |
15 | const Toast = ({
16 | message = '',
17 | position = 'center',
18 | type,
19 | icon,
20 | loadingType = 'spinner'
21 | }: ToastProps) => {
22 | const toastItem = {
23 | className: classnames(`${baseClass}`, [
24 | {
25 | toastItem: 'toastItem'
26 | },
27 | {
28 | [`position`]: 'position'
29 | },
30 | {
31 | [`position__${position}`]: `position__${position}`
32 | },
33 | { extra: type !== 'message' },
34 | {
35 | [type === 'message' && icon ? 'user__type' : '']:
36 | type === 'message' && icon ? 'user__type' : ''
37 | }
38 | ]),
39 | style: {}
40 | };
41 | switch (type) {
42 | case 'checked':
43 | case 'fail':
44 | icon = ;
45 | break;
46 | case 'loading':
47 | icon = renderLoadingIcon({
48 | loadingType,
49 | className: '',
50 | loadingSize: 'large'
51 | });
52 | break;
53 | default:
54 | break;
55 | }
56 | const contentStyle = {
57 | className: classnames(`${baseClass}__text`, []),
58 | style: {}
59 | };
60 | return (
61 |
62 | {icon}
63 |
{message}
64 |
65 | );
66 | };
67 | export default Toast;
68 |
--------------------------------------------------------------------------------
/src/components/Toast/ToastContainer.tsx:
--------------------------------------------------------------------------------
1 | /*
2 | * @Author: zhaohui
3 | * @Date: 2021-05-17 18:50:48
4 | * @LastEditTime: 2021-05-26 11:27:45
5 | * @LastEditors: zhaohui
6 | * @Description:
7 | * @FilePath: /vant-react/src/components/Toast/ToastContainer.tsx
8 | */
9 | import React, { Component } from 'react';
10 | import classnames from '../../utils/classNames';
11 | import Toast from './Toast';
12 | import './index.scss';
13 | import { ToastProps, baseClass, ToastItemProps } from './types';
14 |
15 | interface ToastContainerState {
16 | toastList: ToastProps[];
17 | }
18 | let timer;
19 | class ToastContainer extends Component {
20 | constructor(props) {
21 | super(props);
22 | this.state = {
23 | toastList: []
24 | };
25 | }
26 |
27 | pushToastItem = (info: ToastItemProps): void | Promise => {
28 | const toastItem = Object.assign({}, info, { id: getUid() });
29 | const { duration } = toastItem;
30 | clearTimeout(timer);
31 | if (toastItem.type === 'loading') {
32 | return new Promise((resolve) => {
33 | this.setState({ toastList: [toastItem] }, () => {
34 | timer = setTimeout(() => {
35 | this.popToast(toastItem.id);
36 | clearTimeout(timer);
37 | resolve();
38 | }, duration || 2000);
39 | });
40 | });
41 | } else {
42 | this.setState({ toastList: [toastItem] }, () => {
43 | timer = setTimeout(() => {
44 | this.popToast(toastItem.id);
45 | }, duration || 2000);
46 | });
47 | }
48 | };
49 |
50 | popToast = (id: string) => {
51 | const { toastList } = this.state;
52 | const newToastList: ToastItemProps[] = toastList.filter(
53 | (item: ToastItemProps) => item.id !== id
54 | );
55 | this.setState({
56 | toastList: newToastList
57 | });
58 | };
59 |
60 | render() {
61 | const toastContainerStyle = {
62 | className: classnames(`${baseClass}__container`, []),
63 | style: {}
64 | };
65 | const toastMaskStyle = {
66 | className: classnames(`${baseClass}__mask`, []),
67 | style: {}
68 | };
69 | return (
70 |
71 | {this.state.toastList.map((item: ToastItemProps) => (
72 |
73 | {item.overlay ?
: ''}
74 |
75 |
76 | ))}
77 |
78 | );
79 | }
80 | }
81 |
82 | let toastCount = 0;
83 | const getUid = () => {
84 | return `${baseClass}__container__${new Date().getTime()}__${toastCount++}`;
85 | };
86 | export default ToastContainer;
87 |
--------------------------------------------------------------------------------
/src/components/Toast/index.scss:
--------------------------------------------------------------------------------
1 | @import '../../styles/colors.scss';
2 |
3 | $baseClass: 'vant-toast';
4 |
5 | * {
6 | margin: 0;
7 | padding: 0;
8 | }
9 | .#{$baseClass}__container {
10 | position: relative;
11 | z-index: 10000;
12 | .#{$baseClass}__mask {
13 | position: fixed;
14 | left: 0;
15 | top: 0;
16 | width: 100%;
17 | height: 100%;
18 | z-index: 1;
19 | background-color: transparent;
20 | }
21 | .#{$baseClass}__toastItem {
22 | position: fixed;
23 | z-index: 2;
24 | background-color: rgba(0, 0, 0, 0.7);
25 | border-radius: 10px;
26 | color: white;
27 | }
28 | .#{$baseClass}__position {
29 | }
30 | .#{$baseClass}__position__center {
31 | text-align: center;
32 | top: 50%;
33 | left: 50%;
34 | transform: translate3d(-50%, -50%, 0);
35 | z-index: 2;
36 | }
37 | .#{$baseClass}__position__top {
38 | position: fixed;
39 | top: 10%;
40 | left: 50%;
41 | transform: translate3d(-50%, 0, 0);
42 | z-index: 2;
43 | }
44 | .#{$baseClass}__position__bottom {
45 | position: fixed;
46 | bottom: 10%;
47 | left: 50%;
48 | transform: translate3d(-50%, 0, 0);
49 | z-index: 2;
50 | }
51 | .#{$baseClass}__text {
52 | width: fit-content;
53 | min-width: 96px;
54 | min-height: 0;
55 | text-align: center;
56 | font-size: 14px;
57 | line-height: 20px;
58 | white-space: pre-wrap;
59 | color: white;
60 | word-wrap: break-word;
61 | border-radius: 8px;
62 | padding: 8px 12px;
63 | }
64 | .#{$baseClass}__extra {
65 | display: flex;
66 | flex-direction: column;
67 | justify-content: center;
68 | align-items: center;
69 | width: 120px;
70 | min-width: 120px;
71 | min-height: 120px;
72 | text-align: center;
73 | max-width: 70%;
74 | font-size: 14px;
75 | line-height: 20px;
76 | padding: 16px;
77 | white-space: pre-wrap;
78 | color: white;
79 | word-wrap: break-word;
80 | border-radius: 8px;
81 | .#{$baseClass}__text {
82 | width: 100%;
83 | min-width: 0;
84 | margin-top: 8px;
85 | padding: 0;
86 | }
87 | }
88 | .#{$baseClass}__user__type {
89 | display: flex;
90 | flex-direction: column;
91 | justify-content: center;
92 | align-items: center;
93 | width: 120px;
94 | min-width: 120px;
95 | min-height: 120px;
96 | text-align: center;
97 | max-width: 70%;
98 | font-size: 14px;
99 | line-height: 20px;
100 | padding: 16px;
101 | white-space: pre-wrap;
102 | color: white;
103 | word-wrap: break-word;
104 | border-radius: 8px;
105 | .#{$baseClass}__text {
106 | width: 100%;
107 | min-width: 0;
108 | margin-top: 8px;
109 | padding: 0;
110 | }
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/src/components/Toast/index.stories.tsx:
--------------------------------------------------------------------------------
1 | /*
2 | * @Author: zhaohui
3 | * @Date: 2021-05-17 14:21:43
4 | * @LastEditTime: 2021-05-26 11:25:59
5 | * @LastEditors: zhaohui
6 | * @Description:
7 | * @FilePath: /vant-react/src/components/Toast/index.stories.tsx
8 | */
9 | import React from 'react';
10 | import Toast from '.';
11 | import Cell from '../Cell';
12 | import Icon from '../Icons';
13 |
14 | import '../../styles/stories.scss';
15 |
16 | export default {
17 | title: 'Toast',
18 | component: Toast
19 | };
20 | export const BasicUsage = () => {
21 | return (
22 |
23 | Toast.info({ message: 'Base Usage' })
30 | }}
31 | />
32 |
39 | Toast.info({ message: 'Toast bottom', position: 'bottom' })
40 | }}
41 | />
42 |
49 | Toast.info({ message: 'Toast center', position: 'center' })
50 | }}
51 | />
52 | | | |
53 | );
54 | };
55 |
56 | export const ToastStatus = () => {
57 | return (
58 |
59 |
66 | Toast.info({ message: 'ToastSuccess', type: 'checked' })
67 | }}
68 | />
69 | Toast.info({ message: 'ToastFail', type: 'fail' })
76 | }}
77 | />
78 | | |
79 | );
80 | };
81 | export const ToastLoading = () => {
82 | return (
83 |
84 | Toast.Loading('Loading')
91 | }}
92 | />
93 |
100 | Toast.Loading({
101 | type: 'spinner',
102 | message: 'ToastLoadingWithSpinner'
103 | })
104 | }}
105 | />
106 |
113 | Toast.Loading({
114 | type: 'circular',
115 | message: 'ToastLoadingWithCpinner'
116 | })
117 | }}
118 | />
119 | toastSync()
126 | }}
127 | />
128 | | | | |
129 | );
130 | };
131 |
132 | export const ToastUserSet = () => {
133 | return (
134 |
135 |
142 | Toast.info({
143 | message: 'Toast user set icon',
144 | icon:
145 | })
146 | }}
147 | />
148 |
155 | Toast.info({
156 | message: 'Toast user set img',
157 | icon:
158 | })
159 | }}
160 | />
161 | | |
162 | );
163 | };
164 |
165 | export const ToastSetDefaultOptionGlobal = () => {
166 | return (
167 |
168 |
175 | Toast.setDefaultOptions({
176 | duration: 5000
177 | })
178 | }}
179 | />
180 |
187 | Toast.setDefaultOptions({
188 | duration: 1000
189 | })
190 | }}
191 | />
192 | | |
193 | );
194 | };
195 |
196 | const toastSync = () => {
197 | Toast.Loading({
198 | type: 'circular',
199 | message: 'ToastLoadingBackSync'
200 | }).then(() => {
201 | alert('finished');
202 | });
203 | };
204 |
--------------------------------------------------------------------------------
/src/components/Toast/index.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @Author: zhaohui
3 | * @Date: 2021-05-14 11:42:17
4 | * @LastEditTime: 2021-05-17 15:17:13
5 | * @LastEditors: zhaohui
6 | * @Description:
7 | * @FilePath: /vant-react/src/components/Toast/index.ts
8 | */
9 | import Toast from './CreateToast';
10 |
11 | export default Toast;
12 |
--------------------------------------------------------------------------------
/src/components/Toast/types.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @Author: zhaohui
3 | * @Date: 2021-05-14 09:32:24
4 | * @LastEditTime: 2021-05-26 11:17:15
5 | * @LastEditors: zhaohui
6 | * @Description:
7 | * @FilePath: /vant-react/src/components/Toast/types.ts
8 | */
9 | import React from 'react';
10 | import { LoadingTypes } from '../Button/types';
11 | export interface ToastProps {
12 | overlay?: boolean;
13 | message?: React.ReactNode;
14 | type?: 'message' | 'loading' | 'fail' | 'checked';
15 | position?: 'center' | 'top' | 'bottom';
16 | icon?: React.ReactNode;
17 | duration?: number;
18 | loadingType?: LoadingTypes;
19 | }
20 | export interface ToastItemProps extends ToastProps {
21 | id?: string;
22 | }
23 | export interface LoadingOption {
24 | type?: LoadingTypes;
25 | duration?: number;
26 | message?: string;
27 | }
28 | export const baseClass = 'vant-toast';
29 |
--------------------------------------------------------------------------------
/src/components/template/index.scss:
--------------------------------------------------------------------------------
1 | @import '../../styles/colors.scss';
2 |
3 | $baseClass: 'vant-template';
4 |
5 | .#{$baseClass} {
6 | }
7 |
--------------------------------------------------------------------------------
/src/components/template/index.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Template from './';
3 |
4 | import '../../styles/stories.scss';
5 |
6 | export default {
7 | title: 'Component Template',
8 | component: Template
9 | };
10 |
11 | export const BasicUsage = () => (
12 |
13 |
14 |
15 | );
16 |
--------------------------------------------------------------------------------
/src/components/template/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import classnames from '../../utils/classNames';
4 |
5 | import './index.scss';
6 |
7 | export interface IProps {
8 | foo: string;
9 | bar: number;
10 | }
11 |
12 | const baseClass = 'vant-template';
13 |
14 | const Template = ({ foo = 'foo', bar = 42 }: IProps) => {
15 | return (
16 |
17 |
18 | I am a template, {foo}, {bar}
19 |
20 |
21 | );
22 | };
23 |
24 | export default Template;
25 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import Button from './components/Button';
2 | import Icon from './components/Icons';
3 | import Tag from './components/Tag';
4 | import Navbar from './components/Navbar';
5 | import Field from './components/Field';
6 | import Search from './components/Search';
7 | import Popup from './components/Popup';
8 | import Cell from './components/Cell';
9 | import Rate from './components/Rate';
10 | import Image from './components/Image';
11 | import Slider from './components/Slider';
12 | import Checkbox from './components/Checkbox';
13 | import Radio from './components/Radio';
14 | import Stepper from './components/Stepper';
15 | import Toast from './components/Toast';
16 |
17 | export { default as Button } from './components/Button';
18 | export { default as Icon } from './components/Icons';
19 | export { default as Tag } from './components/Tag';
20 | export { default as Navbar } from './components/Navbar';
21 | export { default as Field } from './components/Field';
22 | export { default as Search } from './components/Search';
23 | export { default as Popup } from './components/Popup';
24 | export { default as Cell } from './components/Cell';
25 | export { default as Rate } from './components/Rate';
26 | export { default as Image } from './components/Image';
27 | export { default as Slider } from './components/Slider';
28 | export { default as Checkbox } from './components/Checkbox';
29 | export { default as Radio } from './components/Radio';
30 | export { default as Stepper } from './components/Stepper';
31 | export { default as Toast } from './components/Toast';
32 |
33 | const Vant = {
34 | Button,
35 | Icon,
36 | Tag,
37 | Navbar,
38 | Field,
39 | Search,
40 | Popup,
41 | Cell,
42 | Rate,
43 | Image,
44 | Slider,
45 | Checkbox,
46 | Radio,
47 | Stepper,
48 | Toast
49 | };
50 |
51 | export default Vant;
52 |
--------------------------------------------------------------------------------
/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/src/styles/animation.scss:
--------------------------------------------------------------------------------
1 | @keyframes vant-rotate {
2 | from {
3 | transform: rotate(0deg);
4 | }
5 |
6 | to {
7 | transform: rotate(360deg);
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/styles/colors.scss:
--------------------------------------------------------------------------------
1 | $default: #fff;
2 | $primary: #07c160;
3 | $info: #1989fa;
4 | $danger: #ee0a24;
5 | $warning: #ff976a;
6 |
7 | $black: #000;
8 | $grey: #969799;
9 |
10 | $dark-text: #323233;
11 | $light-text: #fff;
12 | $grey-text: #ebedf0;
13 | $grey-background: #f7f8fa;
14 |
15 | $placeholder: #c8c9cc;
16 | $word-limit: #656566;
17 |
--------------------------------------------------------------------------------
/src/styles/global.scss:
--------------------------------------------------------------------------------
1 | * {
2 | box-sizing: border-box;
3 | font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', Helvetica,
4 | Segoe UI, Arial, Roboto, 'PingFang SC', 'Hiragino Sans GB',
5 | 'Microsoft Yahei', sans-serif;
6 | }
7 |
--------------------------------------------------------------------------------
/src/styles/opacity.scss:
--------------------------------------------------------------------------------
1 | $button-disabled-opacity: 0.5;
2 |
--------------------------------------------------------------------------------
/src/styles/spacing.scss:
--------------------------------------------------------------------------------
1 | $space-sm: 4px;
2 | $space-md: 8px;
3 | $space-lg: 16px;
4 |
5 | $padding-base: 4px;
6 | $padding-xs: $padding-base * 2;
7 | $padding-sm: $padding-base * 3;
8 | $padding-md: $padding-base * 4;
9 | $padding-lg: $padding-base * 6;
10 | $padding-xl: $padding-base * 8;
--------------------------------------------------------------------------------
/src/styles/stories.scss:
--------------------------------------------------------------------------------
1 | @import './colors.scss';
2 |
3 | body {
4 | margin: 0;
5 | font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', Helvetica,
6 | Segoe UI, Arial, Roboto, 'PingFang SC', 'Hiragino Sans GB',
7 | 'Microsoft Yahei', sans-serif;
8 | }
9 |
10 | .storybook__container {
11 | height: 100vh;
12 | width: 100%;
13 | display: flex;
14 | justify-content: center;
15 | align-items: center;
16 |
17 | .image-container {
18 | span {
19 | text-align: center;
20 | display: block;
21 | margin: 15px 0;
22 | }
23 | }
24 |
25 | &.grey {
26 | background-color: $grey-text;
27 | }
28 |
29 | &.button {
30 | button,
31 | a {
32 | margin-bottom: 20px;
33 | margin-right: 20px;
34 | }
35 | i {
36 | margin-right: 5px;
37 | }
38 | }
39 |
40 | &.column {
41 | flex-direction: column;
42 | }
43 |
44 | &.icon {
45 | i {
46 | margin-right: 5px;
47 | }
48 | }
49 |
50 | nav {
51 | margin-bottom: 20px;
52 |
53 | .vant-icon__container {
54 | margin: 0;
55 | }
56 | }
57 |
58 | .vant-image {
59 | margin-right: 20px;
60 | img {
61 | width: 200px;
62 | height: 200px;
63 | }
64 | }
65 | }
66 | .slider-container {
67 | display: flex;
68 | flex-direction: column;
69 | justify-content: center;
70 | align-items: center;
71 | height: 100vh;
72 |
73 | .slider-block {
74 | display: flex;
75 | flex-direction: column;
76 | align-items: center;
77 | margin: 20px;
78 | p {
79 | margin-bottom: 20px;
80 | }
81 | }
82 | }
83 |
84 | .storybook__icons-container {
85 | padding: 15px;
86 | display: flex;
87 | flex-direction: column;
88 | background-color: $grey-background;
89 |
90 | h1 {
91 | font-size: 22px;
92 | margin: 15px 0 15px 15px;
93 | font-weight: 300;
94 | }
95 |
96 | .icon-group {
97 | display: flex;
98 | flex-wrap: wrap;
99 | }
100 |
101 | .icon-block {
102 | display: flex;
103 | flex-direction: column;
104 | justify-content: center;
105 | align-items: center;
106 | text-align: center;
107 | height: 125px;
108 | width: 125px;
109 | margin: 15px;
110 | border-radius: 25px;
111 | padding: 12px 8px;
112 | background-color: #fff;
113 | transition-duration: 0.4s;
114 | box-shadow: 2px 2px 5px #eee;
115 |
116 | * {
117 | transition-duration: 0.2s;
118 | }
119 |
120 | i {
121 | font-size: 42px !important;
122 | color: $black !important;
123 | }
124 |
125 | &:hover {
126 | background-color: #fff;
127 | box-shadow: 4px 4px 50px #eee;
128 | cursor: pointer;
129 |
130 | i {
131 | color: $primary !important;
132 | }
133 |
134 | .icon-name {
135 | color: $primary;
136 | }
137 | }
138 | }
139 |
140 | .storybook__icon-name {
141 | color: $black;
142 | font-size: 12px;
143 | display: block;
144 | margin-top: 15px;
145 | font-weight: 100;
146 | }
147 | }
148 |
--------------------------------------------------------------------------------
/src/styles/style.scss:
--------------------------------------------------------------------------------
1 | .vant-ellipsis {
2 | overflow: hidden;
3 | white-space: nowrap;
4 | text-overflow: ellipsis;
5 | }
6 |
7 | .vant-multi-ellipsis {
8 | display: -webkit-box;
9 | overflow: hidden;
10 | text-overflow: ellipsis;
11 | -webkit-box-orient: vertical;
12 |
13 | &--l2 {
14 | -webkit-line-clamp: 2;
15 | }
16 |
17 | &--l3 {
18 | -webkit-line-clamp: 3;
19 | }
20 | }
21 |
22 | .vant-hairline {
23 | border: 0 solid #ebedf0;
24 |
25 | &--top {
26 | border-top-width: 1px;
27 | }
28 | &--bottom {
29 | border-bottom-width: 1px;
30 | }
31 | &--left {
32 | border-left-width: 1px;
33 | }
34 | &--right {
35 | border-right-width: 1px;
36 | }
37 | &--top-bottom {
38 | border-top-width: 1px;
39 | border-bottom-width: 1px;
40 | }
41 | &--surround {
42 | border-width: 1px;
43 | }
44 | }
45 |
46 | .vant-fade {
47 | visibility: visible;
48 | transition: all 0.3s ease-out;
49 | }
50 |
51 | .vant-slide {
52 | visibility: visible;
53 | transition: all 0.3s ease-out;
54 |
55 | &-up {
56 | transform: translateY(-100%);
57 | }
58 |
59 | &-down {
60 | transform: translateY(100%);
61 | }
62 |
63 | &-left {
64 | transform: translateX(100%);
65 | }
66 |
67 | &-right {
68 | transform: translateX(-100%);
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/styles/typography.scss:
--------------------------------------------------------------------------------
1 | @import './global.scss';
2 | @import './colors.scss';
3 |
4 | @mixin normal {
5 | font-size: 14px;
6 | line-height: 1.2;
7 | text-align: center;
8 | }
9 |
10 | @mixin nav-title {
11 | font-size: 16px;
12 | text-transform: capitalize;
13 | font-weight: 500;
14 | color: $dark-text;
15 | overflow-x: hidden;
16 | white-space: nowrap;
17 | text-overflow: ellipsis;
18 | max-width: 60%;
19 | }
20 |
21 | @mixin nav-link {
22 | font-size: 14px;
23 | font-weight: 100;
24 | line-height: 21px;
25 | color: $info;
26 | }
27 |
28 | @mixin field-label {
29 | font-size: 14px;
30 | line-height: 24px;
31 | color: $dark-text;
32 | font-weight: 300;
33 | }
34 |
35 | @mixin search-action {
36 | font-size: 14px;
37 | line-height: 34px;
38 | font-weight: 300;
39 | }
40 |
41 | @mixin form-label {
42 | font-size: 16px;
43 | line-height: 20px;
44 | font-weight: normal;
45 | }
46 |
--------------------------------------------------------------------------------
/src/styles/variables.scss:
--------------------------------------------------------------------------------
1 |
2 | // Border
3 | $border-color: #ebedf0;
4 | $border-width-base: 1px;
5 | $border-radius-sm: 2px;
6 | $border-radius-md: 4px;
7 | $border-radius-lg: 8px;
8 | $border-radius-max: 999px;
9 |
10 | // loaders
11 | $loader-size: 20px;
12 | $loader-animation-duration: 2s;
13 | $loader-circular-duration: 1.4s;
14 |
15 | // buttons
16 | $button-height: 44px;
17 |
18 | // navbar
19 | $navbar-height: 56px;
20 |
21 | // icons
22 | $icon-container-size: 32px;
23 | $icon-dot-size: 8px;
24 |
25 | // popups
26 | $popup-alpha: 0.5;
27 | $popup-background-color: #000;
28 |
29 | // divider
30 | $divider-margin: 8px;
31 | $divider-text-color: #969799;
32 | $divider-font-size: 14px;
33 | $divider-line-height: 24px;
34 | $divider-border-color: #ebedf0;
35 | $divider-content-padding: 16px;
36 | $divider-content-left-width: 10%;
37 | $divider-content-right-width: 10%;
38 |
--------------------------------------------------------------------------------
/src/types/positions.ts:
--------------------------------------------------------------------------------
1 | export type TLabelPosition = 'left' | 'right';
2 |
--------------------------------------------------------------------------------
/src/types/shapes.ts:
--------------------------------------------------------------------------------
1 | type TShape = 'square' | 'round';
2 |
3 | export default TShape;
4 |
--------------------------------------------------------------------------------
/src/typings.d.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Default CSS definition for typescript,
3 | * will be overridden with file-specific definitions by rollup
4 | */
5 | declare module '*.css' {
6 | const content: { [className: string]: string };
7 | export default content;
8 | }
9 |
10 | interface SvgrComponent extends React.StatelessComponent> {}
11 |
12 | declare module '*.svg' {
13 | const svgUrl: string;
14 | const svgComponent: SvgrComponent;
15 | export default svgUrl;
16 | export { svgComponent as ReactComponent }
17 | }
18 |
--------------------------------------------------------------------------------
/src/utils/base.ts:
--------------------------------------------------------------------------------
1 | export const inBrowser = typeof window !== 'undefined';
2 |
3 | export function isDef(val: T): val is NonNullable {
4 | return val !== undefined && val !== null;
5 | }
6 |
--------------------------------------------------------------------------------
/src/utils/classNames.ts:
--------------------------------------------------------------------------------
1 | const classnames = (baseClass: string, names: object[]): string => {
2 | const normalizedNames: string[] = [];
3 | const specialKeys: string[] = ['type'];
4 | names.forEach((name: object) => {
5 | if (Object.prototype.toString.call(name) === '[object Object]') {
6 | const key = Object.keys(name)[0];
7 | const value = name[key];
8 | if (specialKeys.indexOf(key) !== -1) {
9 | normalizedNames.push(`${baseClass}__${value}`);
10 | } else if (value) {
11 | normalizedNames.push(`${baseClass}__${key}`);
12 | }
13 | }
14 | });
15 | return [baseClass, ...normalizedNames].join(' ');
16 | };
17 |
18 | export default classnames;
19 |
--------------------------------------------------------------------------------
/src/utils/format/unit.ts:
--------------------------------------------------------------------------------
1 | import { CSSProperties } from 'react';
2 | import { isDef } from '../base';
3 | import { isNumeric } from '../validate/number';
4 |
5 | export function addUnit(value?: string | number): string | undefined {
6 | if (!isDef(value)) {
7 | return undefined;
8 | }
9 |
10 | return isNumeric(value) ? `${value}px` : String(value);
11 | }
12 |
13 | export function getSizeStyle(
14 | originSize?: string | number
15 | ): CSSProperties | void {
16 | if (isDef(originSize)) {
17 | const size = addUnit(originSize);
18 | return {
19 | width: size,
20 | height: size
21 | };
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/utils/index.ts:
--------------------------------------------------------------------------------
1 | import classnames from './classNames';
2 |
3 | export * from './base';
4 | export * from './format/unit';
5 | export * from './validate/number';
6 |
7 | export { classnames };
8 |
--------------------------------------------------------------------------------
/src/utils/validate/number.ts:
--------------------------------------------------------------------------------
1 | export function isNumeric(val: string | number): val is string {
2 | return typeof val === 'number' || /^\d+(\.\d+)?$/.test(val);
3 | }
4 |
5 | export function isNaN(val: number): val is typeof NaN {
6 | if (Number.isNaN) {
7 | return Number.isNaN(val);
8 | }
9 |
10 | // eslint-disable-next-line no-self-compare
11 | return val !== val;
12 | }
13 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "outDir": "dist",
4 | "module": "esnext",
5 | "lib": [
6 | "dom",
7 | "esnext"
8 | ],
9 | "moduleResolution": "node",
10 | "jsx": "react",
11 | "sourceMap": true,
12 | "declaration": true,
13 | "esModuleInterop": true,
14 | "noImplicitReturns": true,
15 | "noImplicitThis": true,
16 | "noImplicitAny": false,
17 | "strictNullChecks": true,
18 | "suppressImplicitAnyIndexErrors": true,
19 | "noUnusedLocals": true,
20 | "noUnusedParameters": true,
21 | "allowSyntheticDefaultImports": true,
22 | "target": "es5",
23 | "allowJs": true,
24 | "skipLibCheck": true,
25 | "strict": true,
26 | "forceConsistentCasingInFileNames": true,
27 | "resolveJsonModule": true,
28 | "isolatedModules": true,
29 | "noEmit": true
30 | },
31 | "include": [
32 | "src"
33 | ],
34 | "exclude": [
35 | "node_modules",
36 | "dist",
37 | "demo"
38 | ]
39 | }
40 |
--------------------------------------------------------------------------------
/tsconfig.test.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "module": "commonjs"
5 | }
6 | }
--------------------------------------------------------------------------------